From b08a84bbe43ac36bc2cdb2a85b783695ba74becc Mon Sep 17 00:00:00 2001 From: Kumar Anirudha Date: Mon, 15 Jun 2026 12:50:45 +0530 Subject: [PATCH] ci: post validation comment via workflow_run to fix fork PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Validate plugin submission" workflow runs its checks and posts the result comment from the same job. On pull requests from forks, GitHub forces `GITHUB_TOKEN` to read-only regardless of the workflow's `permissions:` block, so `issues.createComment` returned 403 "Resource not accessible by integration" and the job failed — even though every validation check passed. This broke the run on every external contributor's PR (e.g. #29, #30). Split comment posting out of the validation job: - `validate-plugin.yml` now runs read-only (`contents: read`). Instead of commenting, it renders the comment body and PR number to a `pr-comment` artifact. Step outputs are passed via env vars rather than interpolated into the script, and the literal `\n` sequences the shell steps emit are converted to real newlines. - `comment-on-pr.yml` is a new `workflow_run` workflow that fires when validation completes. It runs in the base-repo context with a writable token, downloads the artifact, and creates or updates the comment. Because it only reads an artifact and never checks out or runs PR code, granting it write permissions is safe. Note: `workflow_run` only takes effect from the default branch, so this fix activates after merge to `main`; the validation workflow must re-run on a PR afterward to produce the comment. --- .github/workflows/comment-on-pr.yml | 80 +++++++++++++++++++++++++++ .github/workflows/validate-plugin.yml | 73 +++++++++++++----------- 2 files changed, 120 insertions(+), 33 deletions(-) create mode 100644 .github/workflows/comment-on-pr.yml diff --git a/.github/workflows/comment-on-pr.yml b/.github/workflows/comment-on-pr.yml new file mode 100644 index 0000000..c85ef28 --- /dev/null +++ b/.github/workflows/comment-on-pr.yml @@ -0,0 +1,80 @@ +name: Comment on plugin PR + +# Posts the validation result produced by "Validate plugin submission" back to +# the PR. That workflow runs in the PR (fork) context with a read-only token +# and therefore can't comment itself. This workflow runs via workflow_run in +# the base-repo context, where the token can write to issues/PRs. It only reads +# an artifact — it never checks out or executes PR code — so it is safe to grant +# write permissions even though the triggering run was for untrusted fork code. + +on: + workflow_run: + workflows: ["Validate plugin submission"] + types: + - completed + +permissions: + contents: read + actions: read + issues: write + pull-requests: write + +jobs: + comment: + runs-on: ubuntu-latest + # Only act on runs triggered by pull requests. + if: github.event.workflow_run.event == 'pull_request' + steps: + - name: Download comment artifact + id: download + continue-on-error: true + uses: actions/download-artifact@v4 + with: + name: pr-comment + path: pr-comment + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Post or update comment + if: steps.download.outcome == 'success' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + const body = fs.readFileSync('pr-comment/body.md', 'utf8'); + const issue_number = parseInt( + fs.readFileSync('pr-comment/pr.txt', 'utf8').trim(), + 10, + ); + if (!Number.isInteger(issue_number)) { + core.setFailed('No valid PR number in artifact.'); + return; + } + + const { owner, repo } = context.repo; + + const { data: comments } = await github.rest.issues.listComments({ + owner, + repo, + issue_number, + }); + + const marker = '## Plugin Validation'; + const existing = comments.find((c) => c.body.startsWith(marker)); + + if (existing) { + await github.rest.issues.updateComment({ + owner, + repo, + comment_id: existing.id, + body, + }); + } else { + await github.rest.issues.createComment({ + owner, + repo, + issue_number, + body, + }); + } diff --git a/.github/workflows/validate-plugin.yml b/.github/workflows/validate-plugin.yml index 7735e71..c1d8f19 100644 --- a/.github/workflows/validate-plugin.yml +++ b/.github/workflows/validate-plugin.yml @@ -5,8 +5,12 @@ on: paths: - 'plugins/**' +# This workflow runs in the PR's (fork) context, so it gets a read-only token +# and must not need write access. The validation result comment is posted by +# the separate comment-on-pr.yml workflow, which runs in the base-repo context +# with write permissions. See that file for why the split is necessary. permissions: - pull-requests: write + contents: read jobs: validate: @@ -225,18 +229,35 @@ jobs: exit 1 fi - - name: Comment on PR + # We can't post the result comment from here: on fork PRs the + # GITHUB_TOKEN is read-only, so any issues/PR write returns 403. Instead + # we render the comment body to a file and hand it off as an artifact to + # comment-on-pr.yml, which runs via workflow_run with a writable token. + - name: Render PR comment + id: render if: always() && steps.changes.outputs.has_changes == 'true' uses: actions/github-script@v7 + env: + DUP_OUTCOME: ${{ steps.duplicates.outcome }} + VAL_OUTCOME: ${{ steps.validation.outcome }} + BUILD_OUTCOME: ${{ steps.build.outcome }} + DUP_RESULT: ${{ steps.duplicates.outputs.result }} + VAL_RESULT: ${{ steps.validation.outputs.result }} + BUILD_RESULT: ${{ steps.build.outputs.result }} with: script: | - const dup = `${{ steps.duplicates.outcome }}`; - const val = `${{ steps.validation.outcome }}`; - const build = `${{ steps.build.outcome }}`; + const fs = require('fs'); - const dupResult = `${{ steps.duplicates.outputs.result }}`; - const valResult = `${{ steps.validation.outputs.result }}`; - const buildResult = `${{ steps.build.outputs.result }}`; + const dup = process.env.DUP_OUTCOME; + const val = process.env.VAL_OUTCOME; + const build = process.env.BUILD_OUTCOME; + + // The shell steps emit literal "\n" sequences; turn them into real + // newlines so the error list renders as Markdown bullets. + const fixup = (s) => (s || '').replace(/\\n/g, '\n'); + const dupResult = fixup(process.env.DUP_RESULT); + const valResult = fixup(process.env.VAL_RESULT); + const buildResult = fixup(process.env.BUILD_RESULT); const allPassed = dup === 'success' && val === 'success' && build === 'success'; const icon = (s) => s === 'success' ? ':white_check_mark:' : s === 'skipped' ? ':next_track_button:' : ':x:'; @@ -259,28 +280,14 @@ jobs: body += `**HTML:**\n\`\`\`html\n\n AgentHub\n\n\`\`\`\n`; } - // Find and update existing bot comment, or create new one - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - const marker = '## Plugin Validation'; - const existing = comments.find(c => c.body.startsWith(marker)); - - 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, - }); - } + fs.mkdirSync('pr-comment', { recursive: true }); + fs.writeFileSync('pr-comment/body.md', body); + fs.writeFileSync('pr-comment/pr.txt', String(context.issue.number)); + + - name: Upload PR comment artifact + if: always() && steps.changes.outputs.has_changes == 'true' + uses: actions/upload-artifact@v4 + with: + name: pr-comment + path: pr-comment/ + retention-days: 1