fix(ci): explain fork PR workflow approval gate (#645) #993
No reviewers
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit!993
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/645-fork-approval-notice"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implement release-toolkit#645 option (c): add a base-only fork pull-request workflow notice, preserve required checks and credential boundaries, and document the measured historical exposure. The notice is informational and does not manufacture a status or approval.
APPROVE @
4bee0302897bc8ef95838c77cd811cb088c3c4f4— the head @pullings named matches the API and my local fetch, all three. CI re-read at the stamp: 21/21success. Purely additive,+345 −0.🔑 This is a
pull_request_targetworkflow with a write token, so the security claim is the reviewThe script asserts "never reads or executes the pull-request head." That claim lives in the workflow, not the script, so I verified it there — mechanically, not by reading:
Point 3 is the strongest of the five. Many
pull_request_targetmistakes check out the base and then reference the head somewhere later; herehead.sha/head.refappear zero times, so there is no path to executing fork content even by accident. And the script itself comes from the base checkout, so a fork cannot modify what runs.Point 1 is the one that usually goes wrong.
head.repo.full_nameis attacker-chosen — a fork's name is whatever they call it — and it reachesbashthroughenv:rather than through${{ }}insiderun:. That is the difference between a value and a code fragment.✅ And I tested the validation against hostile names rather than trusting the regex
^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+$, run against candidates an attacker could actually register:The control matters as much as the rejections: a validator that rejects everything would have passed the hostile arms and broken every real fork.
✅ Design details that are right and easy to get wrong
marker_count > 1refuses rather than choosing. Two notices is an ambiguous state and it dies instead of guessing which to update — could-not-grade treated as its own outcome.synchronizere-run is a no-op read rather than an edit.persist-credentials: falsekeeps the token out of.git/configin a workflow that has a write-scoped token.Tests: 6 ok / 0 not-ok, arm count stated.
📌 Two notes, neither blocking
① The page walk has a silent cap.
for page in $(seq 1 100)atlimit=50terminates correctly on a short page, but a PR with more than 5000 comments would stop at 100 pages and report no marker — then post a duplicate. Unreachable in practice; worth one line so a later reader knows the bound is deliberate rather than assumed infinite.②
permissions:is an assumption I could not verify. Thecontents: read · issues: writeblock is exactly right if this Forgejo honours it. If it does not,secrets.GITHUB_TOKENin apull_request_targetjob carries whatever the default grant is. I did not measure whether Forgejo enforces the block — flagging it as unverified rather than implying I checked. The rest of the design does not depend on it: the head is never fetched, so a wider token still has nothing untrusted to act on.📌 And the docs half is worth having — "that is an approval gate, not a code failure" names the exact misreading. A pending required check with no visible cause is indistinguishable from a broken one, which is the silence-is-not-health shape on a surface a contributor sees before anyone else does.