Secure release-note checks for fork pull requests - #20081
Conversation
|
@T-Gro I dug into why this suddenly started failing across the hot reload PRs. GitHub announced safer defaults for This workflow itself had not recently changed. Its last repository change was June 8, but it uses the floating I also verified that I do not think contributors should stop using forks, and I do not think I validated it with |
This comment has been minimized.
This comment has been minimized.
|
The explanation makes sense. Let me verify any security risks (should be fewer, just need to check against common exploits). |
This comment has been minimized.
This comment has been minimized.
|
🔍 Tooling Safety Check — Affects-Build-Infra
|
The check_release_notes workflow (rewritten in #20081) reduced the token to pull-requests: read. issues.createComment (POST) on a pull request requires pull-requests: write, so brand-new PRs that have no existing bot comment -- for example dependency updates opened by dotnet-maestro (#20134) -- failed with 403 'Resource not accessible by integration'. Only PRs that already had a comment from before #20081 passed, because the updateComment (PATCH) path works with issues: write. The security fix in #20081 was removing the untrusted fork-head checkout; that protection is unchanged. No untrusted code runs and the comment body is passed via an environment variable, so restoring pull-requests: write does not reintroduce the fork-checkout risk. Also wrap the comment logic in try/catch so posting the informational comment can never fail the release-notes gate, which is enforced by the previous step's exit code. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…t 403 Rewriting the release-notes check in #20081 reduced the token to pull-requests: read, but posting a NEW PR comment (issues.createComment POST) requires pull-requests: write. Brand-new PRs with no existing bot comment (like #20130 and dependency PRs) therefore fail with 403 'Resource not accessible by integration', while PRs with a pre-existing comment pass via the updateComment PATCH path (allowed by issues: write). Restore pull-requests: write (the fork-head checkout removed in #20081 stays removed, so no untrusted code runs and the comment body is passed via env var) and wrap the comment write in try/catch so the best-effort informational comment can never fail the release-notes gate. Supersedes the abandoned PR #20136. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ing the comment non-fatal The check_release_notes job posts an informational comment on the PR. Creating a PR comment requires pull-requests: write, but #20081 reduced the token to pull-requests: read. Because the job runs via pull_request_target (GitHub executes the workflow from the default branch), this turned the required check red with HTTP 403 'Resource not accessible by integration' on any PR that needed to create (not update) the comment - e.g. the Maestro/darc PR #20133 targeting release/dev18.0 - even though release-notes validation itself passed. Restore pull-requests: write so the comment can actually be posted, and additionally guard the comment step with continue-on-error plus a try/catch that downgrades any failure to a warning. The real gate (exit 1 when release notes are genuinely missing) is unchanged, and no PR content is evaluated as JavaScript. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ing the comment non-fatal (#20198) check_release_notes runs via pull_request_target, so GitHub executes the workflow from the default branch (main). Creating the informational PR comment requires pull-requests: write, but #20081 reduced the token to read, turning the check red with HTTP 403 on any PR that had to create (not update) the comment - e.g. Maestro/darc PR #20133. Restore pull-requests: write so the comment posts, and guard the comment step with continue-on-error plus try/catch so posting can never fail the release-notes verdict. Supersedes #20200.
PR #20081 (Secure release-note checks for fork pull requests) downgraded the check_release_notes workflow permissions from 'pull-requests: write' to 'pull-requests: read' while keeping 'issues: write'. Commenting on a pull request via GitHub Actions requires 'pull-requests: write' (issues: write alone is insufficient for PR conversation comments), so the final 'Create or update comment' step began failing with 'Resource not accessible by integration' (HTTP 403). PR #20135 is the first codeflow PR to run the new workflow and surfaced the regression. Restore 'pull-requests: write' at both workflow and job level while keeping the rest of the #20081 hardening (contents: read, explicit env, stale-head guards). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Summary
pull_request_targetworkflowactions/github-script@v9GitHub backported safer
actions/checkoutdefaults to floating supported major tags on July 20, 2026. The currentactions/checkout@v2step now rejects the fork checkout before the release-note logic runs, as seen in https://github.com/dotnet/fsharp/actions/runs/30010753188/job/89217916202?pr=20031.Using
allow-unsafe-pr-checkout: truewould restore the old behavior but would preserve the privileged fork-checkout risk. This change keeps fork contributions working without opting out of that protection.Validation
actionlint .github/workflows/check_release_notes.ymlNO_RELEASE_NOTESand produced the expected opt-out result with exit code 0Expected check state
The
check_release_notesresult on this PR is expected to remain red. Thepull_request_targetevent loads its workflow definition from the base branch, so this PR runs the old workflow frommainand fails before it can see the fix. The PR cannot validate its own replacement through this event. After this change merges, subsequent pull request events will use the fixed workflow.Refresh status (2026-07-24)
abb68322f3, with dotnet/fsharp main1dc395ad34merged.actionlint, live release-note and opt-out replays, and stale-head rejection.check_release_notesresult is expected to stay red becausepull_request_targetloads the old workflow frommain. That exception is explained in the Expected check state section above.