name: PR Comment on: workflow_run: workflows: ["PR Check"] types: [completed] pull_request_review: types: [submitted] permissions: actions: read checks: read contents: read pull-requests: write jobs: comment: name: Post PR comment runs-on: ubuntu-latest if: github.event.workflow_run.event == 'pull_request' steps: - uses: actions/checkout@v4 - name: Download PR metadata id: download continue-on-error: true uses: actions/download-artifact@v4 with: name: pr-meta path: pr-meta run-id: ${{ github.event.workflow_run.id }} github-token: ${{ secrets.GITHUB_TOKEN }} - name: Resolve PR context id: context uses: actions/github-script@v7 with: github-token: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require('fs'); // Determine the PR number let prNumber; const numberFile = 'pr-meta/number.txt'; if (fs.existsSync(numberFile)) { prNumber = parseInt(fs.readFileSync(numberFile, 'utf8').trim()); } if (!prNumber) { const prs = context.payload.workflow_run.pull_requests; if (prs && prs.length > 0) { prNumber = prs[0].number; } else { const headSha = context.payload.workflow_run.head_sha; const { data: prList } = await github.rest.pulls.list({ owner: context.repo.owner, repo: context.repo.repo, state: 'open', sort: 'updated', direction: 'desc', per_page: 100, }); const match = prList.find(pr => pr.head.sha === headSha); if (!match) { console.log(`No open PR found for SHA ${headSha} — skipping.`); return; } prNumber = match.number; } } // Fetch existing bot comment (if any) and write to file for Python const marker = ''; const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, per_page: 100, }); const existing = comments.find(c => c.body.includes(marker)); if (existing) { fs.mkdirSync('pr-meta', { recursive: true }); fs.writeFileSync('pr-meta/existing-comment.md', existing.body); fs.writeFileSync('pr-meta/existing-comment-id.txt', String(existing.id)); } core.setOutput('pr_number', prNumber); - name: Prepare comment if: steps.context.outputs.pr_number env: CHECK_RUN_ID: ${{ github.event.workflow_run.id }} GITHUB_REPOSITORY: ${{ github.repository }} run: python lib/checks/prepare-comment.py - name: Post or update comment if: steps.context.outputs.pr_number uses: actions/github-script@v7 with: github-token: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require('fs'); const actionFile = 'pr-meta/action.txt'; if (!fs.existsSync(actionFile)) return; const action = fs.readFileSync(actionFile, 'utf8').trim(); if (action === 'skip') { console.log('Nothing to do — skipping.'); return; } const bodyFile = 'pr-meta/final-comment.md'; if (!fs.existsSync(bodyFile)) { console.log('No comment body found — skipping.'); return; } const body = fs.readFileSync(bodyFile, 'utf8').trim(); const prNumber = parseInt('${{ steps.context.outputs.pr_number }}'); if (action === 'create') { await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, body, }); console.log('Created bot comment.'); } else if (action === 'update') { const commentId = parseInt(fs.readFileSync('pr-meta/existing-comment-id.txt', 'utf8').trim()); await github.rest.issues.updateComment({ owner: context.repo.owner, repo: context.repo.repo, comment_id: commentId, body, }); console.log('Updated bot comment.'); } review-notify: name: Notify maintainer runs-on: ubuntu-latest if: github.event_name == 'pull_request_review' steps: - uses: actions/checkout@v4 - name: Fetch review and CI context id: context uses: actions/github-script@v7 with: github-token: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require('fs'); const pr = context.payload.pull_request; const { owner, repo } = context.repo; // Fetch reviews — extract user login and state const { data: reviews } = await github.rest.pulls.listReviews({ owner, repo, pull_number: pr.number, per_page: 100, }); const reviewData = reviews.map(r => ({ user: r.user.login, state: r.state, })); // Fetch check runs for the PR head SHA const { data: { check_runs } } = await github.rest.checks.listForRef({ owner, repo, ref: pr.head.sha, per_page: 100, }); const checkData = check_runs.map(cr => ({ status: cr.status, conclusion: cr.conclusion, })); // Check if we already posted the notification const marker = ''; const { data: comments } = await github.rest.issues.listComments({ owner, repo, issue_number: pr.number, per_page: 100, }); const alreadyNotified = comments.some(c => c.body.includes(marker)); // Write data for Python fs.mkdirSync('pr-meta', { recursive: true }); fs.writeFileSync('pr-meta/reviews.json', JSON.stringify(reviewData)); fs.writeFileSync('pr-meta/check-runs.json', JSON.stringify(checkData)); fs.writeFileSync('pr-meta/already-notified.txt', String(alreadyNotified)); core.setOutput('pr_number', pr.number); - name: Check review readiness run: python lib/checks/check-review-ready.py - name: Post notification uses: actions/github-script@v7 with: github-token: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require('fs'); const actionFile = 'pr-meta/action.txt'; if (!fs.existsSync(actionFile)) return; const action = fs.readFileSync(actionFile, 'utf8').trim(); if (action !== 'notify') { console.log('Not ready for review — skipping.'); return; } const body = [ '', 'This PR is now ready to be merged, pending maintainer review. All checks are passing and it has been peer-reviewed.', '', '@Lissy93 - Please evaluate, and either merge or leave feedback.', ].join('\n'); await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: parseInt('${{ steps.context.outputs.pr_number }}'), body, }); console.log('Posted maintainer notification.');