Skip to content

fix(sell,swap): surface the real wallet error instead of a generic message #1646

fix(sell,swap): surface the real wallet error instead of a generic message

fix(sell,swap): surface the real wallet error instead of a generic message #1646

Workflow file for this run

name: PR Review Bot
on:
pull_request:
branches:
- main
- develop
types: [opened, synchronize, reopened, labeled, unlabeled]
workflow_dispatch:
inputs:
pr_number:
description: PR number when kicked by ci-on-ready (required for a useful run)
required: false
type: string
base_ref:
description: PR base branch when kicked by ci-on-ready
required: false
type: string
# Draft-skip runs must not share the working group: concurrency fires before
# the job `if:` and would cancel a live Ready dispatch.
concurrency:
group: review-pr-${{ github.event.pull_request.number || inputs.pr_number || github.ref }}-${{ (github.event_name == 'workflow_dispatch' || github.event.pull_request.draft == false || contains(github.event.pull_request.labels.*.name, 'ci') || contains(github.event.pull_request.labels.*.name, 'ci:full')) && 'run' || 'skip' }}
cancel-in-progress: true
permissions:
pull-requests: write
contents: read
jobs:
review:
# Draft skip only: a skipped required check counts as passing, which is
# acceptable only because GitHub cannot merge a draft. Ready is handled
# exclusively by ci-on-ready.yaml. Never listen to ready_for_review here.
if: >
github.event_name != 'pull_request' ||
github.event.pull_request.draft == false ||
contains(github.event.pull_request.labels.*.name, 'ci') ||
contains(github.event.pull_request.labels.*.name, 'ci:full')
runs-on: ubuntu-latest
env:
PR_NUMBER: ${{ github.event.pull_request.number || inputs.pr_number }}
BASE_REF: ${{ github.base_ref || inputs.base_ref }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Check for unverified commits
id: verify-commits
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const prNumber = Number(process.env.PR_NUMBER || context.issue.number);
const commits = await github.rest.pulls.listCommits({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
const unverified = commits.data.filter(c => !c.commit.verification?.verified);
if (unverified.length > 0) {
const list = unverified.map(c =>
`- \`${c.sha.substring(0,7)}\` ${c.commit.message.split('\n')[0]} (${c.commit.author?.name || 'unknown'})`
).join('\n');
fs.writeFileSync('unverified-commits.txt', list);
core.setOutput('unverified', 'true');
core.setOutput('unverified_count', unverified.length);
} else {
core.setOutput('unverified', 'false');
}
- name: Run ESLint
id: eslint
continue-on-error: true
run: |
npm run --silent lint 2>&1 | tee eslint-output.txt || true
# Count only ESLint message lines (" 12:3 warning ..."). A substring match would also
# hit npm's echoed command line and file names such as transaction-document-error.test.tsx.
WARNINGS=$(grep -cE '^ +[0-9]+:[0-9]+ +warning' eslint-output.txt) || WARNINGS=0
ERRORS=$(grep -cE '^ +[0-9]+:[0-9]+ +error' eslint-output.txt) || ERRORS=0
echo "warnings=$WARNINGS" >> $GITHUB_OUTPUT
echo "errors=$ERRORS" >> $GITHUB_OUTPUT
- name: Run TypeScript check
id: typescript
continue-on-error: true
run: |
npx tsc -p tsconfig.build.json --noEmit 2>&1 | tee tsc-output.txt || true
ERRORS=$(grep -c "error TS" tsc-output.txt) || ERRORS=0
echo "errors=$ERRORS" >> $GITHUB_OUTPUT
- name: Security Audit
id: audit
continue-on-error: true
run: |
npm audit --json > audit-output.json 2> audit-stderr.txt || true
# Retried once, and gated on the output rather than the exit code: npm audit exits non-zero
# whenever it finds vulnerabilities, so retrying on that would re-run it on every normal PR.
# This is the only network-dependent check here, and a transient failure posts a warning that
# a later clean run cannot clear.
if ! jq -se '.[0].metadata.vulnerabilities' audit-output.json >/dev/null 2>&1; then
npm audit --json > audit-output.json 2> audit-stderr.txt || true
fi
# No `// 0` fallback: a missing key path must read as "audit did not run", not as "clean".
# -s so two concatenated documents cannot produce a multi-line value.
if HIGH=$(jq -se '.[0].metadata.vulnerabilities.high' audit-output.json 2>/dev/null) &&
CRITICAL=$(jq -se '.[0].metadata.vulnerabilities.critical' audit-output.json 2>/dev/null); then
echo "status=ok" >> $GITHUB_OUTPUT
else
HIGH=0; CRITICAL=0
echo "status=failed" >> $GITHUB_OUTPUT
echo "::warning::npm audit produced no usable report"
head -c 300 audit-stderr.txt audit-output.json 2>/dev/null || true
fi
echo "high=$HIGH" >> $GITHUB_OUTPUT
echo "critical=$CRITICAL" >> $GITHUB_OUTPUT
- name: Check for new TODOs/FIXMEs
id: todos
run: |
set -euo pipefail
if [ -z "${BASE_REF:-}" ]; then
echo "::error::BASE_REF is empty; cannot diff TODOs against the PR base."
exit 1
fi
git fetch --quiet origin "$BASE_REF"
git diff "origin/${BASE_REF}"...HEAD -- '*.ts' '*.tsx' | grep -E '^\+.*\b(TODO|FIXME|HACK|XXX)\b' | head -20 > new-todos.txt || true
COUNT=$(wc -l < new-todos.txt | tr -d ' ')
echo "count=$COUNT" >> $GITHUB_OUTPUT
- name: Post PR comment
# Fork PRs run with a read-only GITHUB_TOKEN, so the issue-comment API returns 403 and fails the whole job.
# The bot can only post on same-repo PRs; skip cleanly on forks instead of turning the check red.
# workflow_dispatch is the in-repo ready kick, so it must post.
if: github.event_name == 'workflow_dispatch' || github.event.pull_request.head.repo.full_name == github.repository
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const prNumber = Number(process.env.PR_NUMBER || context.issue.number);
const unverified = '${{ steps.verify-commits.outputs.unverified }}' === 'true';
const unverifiedCount = '${{ steps.verify-commits.outputs.unverified_count }}';
let unverifiedList = '';
try { unverifiedList = fs.readFileSync('unverified-commits.txt', 'utf8'); } catch (e) {}
const eslintWarnings = parseInt('${{ steps.eslint.outputs.warnings }}') || 0;
const eslintErrors = parseInt('${{ steps.eslint.outputs.errors }}') || 0;
const tscErrors = parseInt('${{ steps.typescript.outputs.errors }}') || 0;
const auditHigh = parseInt('${{ steps.audit.outputs.high }}') || 0;
const auditCritical = parseInt('${{ steps.audit.outputs.critical }}') || 0;
const todoCount = parseInt('${{ steps.todos.outputs.count }}') || 0;
let comments = [];
// Unverified commits warning
if (unverified) {
comments.push(`## ⚠️ Unverified Commits (${unverifiedCount})
The following commits are not signed/verified:
${unverifiedList}
<details>
<summary>How to sign commits</summary>
\`\`\`bash
# SSH signing (recommended)
git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true
# Re-sign last commit
git commit --amend -S --no-edit
git push --force-with-lease
\`\`\`
</details>`);
}
// ESLint warnings
if (eslintWarnings > 0 || eslintErrors > 0) {
comments.push(`## ${eslintErrors > 0 ? '❌' : '⚠️'} ESLint: ${eslintErrors} errors, ${eslintWarnings} warnings`);
}
// TypeScript errors
if (tscErrors > 0) {
comments.push(`## ❌ TypeScript: ${tscErrors} errors`);
}
// Security audit (only report critical vulnerabilities)
if ('${{ steps.audit.outputs.status }}' !== 'ok') {
comments.push('## ⚠️ Security: `npm audit` produced no usable output — vulnerability status unknown');
} else if (auditCritical > 0) {
comments.push(`## ❌ Security: ${auditCritical} critical vulnerabilities`);
}
// New TODOs
if (todoCount > 0) {
let todoList = '';
try {
todoList = fs.readFileSync('new-todos.txt', 'utf8').trim();
} catch (e) {}
comments.push(`## ℹ️ New TODOs/FIXMEs (${todoCount})
\`\`\`diff
${todoList}
\`\`\``);
}
// Only post if there are issues
if (comments.length === 0) {
console.log('No issues found, skipping comment');
return;
}
const body = `# 🤖 PR Review Bot
${comments.join('\n\n---\n\n')}
---
<sub>This is an automated review. Please address the issues above.</sub>`;
// Find existing bot comment
const existingComments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber
});
const botComment = existingComments.data.find(c =>
c.user?.type === 'Bot' && c.body?.includes('🤖 PR Review Bot')
);
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body
});
}