CI failure nudge #24
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # "Fix this and we merge" nudge (InsForge `agent-zhang-beihai` pattern, part 3). | |
| # When a PR's CI finishes red, posts ONE sticky comment listing exactly which | |
| # jobs failed and the local one-liner that reproduces/fixes each (the automated | |
| # version of the hand-written "run `npm run format` and you're green" review | |
| # comments). The comment flips to a green confirmation once all checks pass. | |
| # | |
| # Runs in base-repo context via workflow_run — no PR code is checked out, so | |
| # fork PRs are safe. State is recomputed from check-runs each time, so the two | |
| # CI workflows (CI + Test Coverage) can complete in any order. | |
| # | |
| # Bot identity: same App-token-first / GITHUB_TOKEN-fallback pattern as | |
| # pr-triage.yml (the App needs the "Pull requests: Read & write" permission to | |
| # post as testsprite-hob[bot]; until then comments come from github-actions[bot]). | |
| name: CI failure nudge | |
| on: | |
| workflow_run: | |
| workflows: ['CI', 'Test Coverage'] | |
| types: [completed] | |
| permissions: | |
| checks: read # read the head SHA's check-run state | |
| pull-requests: write | |
| issues: write # PR comments ride the issues API | |
| env: | |
| MARKER: '<!-- hob:ci-nudge -->' | |
| jobs: | |
| nudge: | |
| # Public repo only; PR-triggered runs only (pushes to main have no PR to nudge). | |
| if: >- | |
| github.repository == 'TestSprite/testsprite-cli' && | |
| github.event.workflow_run.event == 'pull_request' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - id: app-token | |
| continue-on-error: true | |
| uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 | |
| with: | |
| app-id: ${{ secrets.TESTSPRITE_HOB_APP_ID || secrets.ALFHEIM_AGENT_APP_ID }} | |
| private-key: ${{ secrets.TESTSPRITE_HOB_PRIVATE_KEY || secrets.ALFHEIM_AGENT_PRIVATE_KEY }} | |
| - uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.0.1 | |
| env: | |
| APP_TOKEN: ${{ steps.app-token.outputs.token }} | |
| with: | |
| script: | | |
| const { owner, repo } = context.repo; | |
| const run = context.payload.workflow_run; | |
| const marker = process.env.MARKER; | |
| const appClient = process.env.APP_TOKEN | |
| ? require('@actions/github').getOctokit(process.env.APP_TOKEN) | |
| : null; | |
| async function write(fn) { | |
| if (appClient) { | |
| try { return await fn(appClient.rest); } | |
| catch (e) { if (e.status !== 403 && e.status !== 404) throw e; } | |
| } | |
| return await fn(github.rest); | |
| } | |
| // Resolve the PR for this run. `workflow_run.pull_requests` is empty | |
| // for fork PRs, so fall back to the commit→PRs lookup. | |
| let pr = (run.pull_requests || [])[0]; | |
| if (!pr) { | |
| const { data } = await github.rest.repos.listPullRequestsAssociatedWithCommit( | |
| { owner, repo, commit_sha: run.head_sha }); | |
| pr = data.find(p => p.state === 'open'); | |
| } else { | |
| pr = (await github.rest.pulls.get({ owner, repo, pull_number: pr.number })).data; | |
| } | |
| if (!pr || pr.state !== 'open' || pr.user.type === 'Bot') return; | |
| // Job name → the local command that reproduces/fixes it. Also the | |
| // allowlist of CI jobs this nudge watches — keep in sync with ci.yml. | |
| const FIX = { | |
| 'Lint & Format': 'run `npm run lint:fix && npm run format`, then commit', | |
| 'Typecheck': 'run `npm run typecheck` and fix the reported type errors', | |
| 'Unit Tests': 'run `npm test` and fix the failing tests', | |
| 'Build': 'run `npm run build` and fix the compile errors', | |
| 'Local E2E Tests': 'run `npm run test:e2e` (it builds first)', | |
| 'Coverage (>= 80%)': 'run `npm run test:coverage` — new code needs tests until every metric is back at 80%', | |
| }; | |
| // Recompute full CI state from this SHA's check runs, narrowed to the | |
| // CI jobs above — other workflows (e.g. pr-triage) also attach | |
| // github-actions check runs to the PR head and must not count here. | |
| const checks = await github.paginate(github.rest.checks.listForRef, | |
| { owner, repo, ref: run.head_sha, per_page: 100 }); | |
| const ours = checks.filter(c => c.app && c.app.slug === 'github-actions' && FIX[c.name]); | |
| const failing = ours.filter(c => ['failure', 'timed_out'].includes(c.conclusion)); | |
| const pending = ours.filter(c => c.status !== 'completed'); | |
| const comments = await github.paginate(github.rest.issues.listComments, | |
| { owner, repo, issue_number: pr.number, per_page: 100 }); | |
| const sticky = comments.find(c => (c.body || '').includes(marker)); | |
| if (failing.length === 0) { | |
| // Only speak up on success if we previously flagged a failure, and | |
| // only once everything has actually finished. | |
| if (sticky && pending.length === 0 && !sticky.body.includes('all green')) { | |
| await write(rest => rest.issues.updateComment({ owner, repo, comment_id: sticky.id, | |
| body: `${marker}\n✅ CI is **all green** now — thanks, @${pr.user.login}!` })); | |
| } | |
| return; | |
| } | |
| const lines = failing | |
| .sort((a, b) => a.name.localeCompare(b.name)) | |
| .map(c => { | |
| const fix = FIX[c.name] || 'see the logs for details'; | |
| return `- **${c.name}** — ${fix} ([logs](${c.html_url}))`; | |
| }); | |
| const body = `${marker}\nThanks, @${pr.user.login}! CI is red on this PR — ` | |
| + `here's what failed and how to reproduce it locally:\n\n${lines.join('\n')}\n\n` | |
| + `Everything runs on Node 22 after \`npm ci\`. Push a fix and this comment ` | |
| + `flips green automatically once all checks pass.`; | |
| if (sticky) { | |
| if (sticky.body !== body) { | |
| await write(rest => rest.issues.updateComment( | |
| { owner, repo, comment_id: sticky.id, body })); | |
| } | |
| } else { | |
| await write(rest => rest.issues.createComment( | |
| { owner, repo, issue_number: pr.number, body })); | |
| } |