feat(auth): allow a SPLIT credential pair — repo-scoped deploy key for pushes, narrow PAT for the API #1094
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
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit#1094
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Today a consumer supplies one credential that does two different jobs — it pushes commits and tags over git, and it creates releases and uploads assets over the API. On Gitea and Forgejo those two jobs can be served by credentials with very different blast radii, and we currently force the broader one on anybody who needs either.
What is available, measured 2026-09-03
That last line is why this matters rather than being tidy.
goreleaser.ymlison: push: tags: ['v*'], so the cut's tag push must trigger a build to publish assets. Under the auto token that trigger is suppressed by anti-recursion, and the adopter gets a tag with no assets — the exact shape release-toolkit itself shipped as v0.56.1 and spent a week repairing.So an adopter who wants assets needs a non-auto credential, and today the only option we offer is a token broader than the job.
Proposed — a THIRD option, not a replacement
🔴 TIER 2 STAYS THE DEFAULT FOR ANYONE WHO SETS ONE TOKEN, AND TIER 1 STAYS THE DEFAULT FOR ANYONE WHO SETS NONE. This adds a path; it does not migrate anybody. For most adopters a single token is the better trade and the docs should say so plainly rather than steering everyone to the complex option because it is the more careful-sounding one.
Acceptance criteria
026d985f06) and its tier truth-table controls.Bounds
⚠️
write:repositoryis the narrowest API category Gitea offers for this work, so tier 3 bounds the PUSH surface fully and the API surface only as far as the owning account's reach. Say that in the docs rather than implying tier 3 is repo-scoped end to end — an overstated security property is worse than an absent one.📌 Deliberately NOT on the Adoption milestone. An adopter can adopt today with a single token; this improves posture for those who want it. Per #1068, anything that does not block the five-point test is not the road. Filed off-milestone on purpose — twice today I added trackers to that milestone that did not belong there.
📌 Anti-recursion behaviour on a deploy-key push is assumed, not measured — a non-bot push should escape the suppression, and that is the premise tier 3 rests on. Measure it before building, because if it does not hold, tier 3 buys only the push-scoping and not the trigger.
Requested by the operator, who asked specifically that it stay optional: "for all the others the convenience of a single token may be the better choice."
Closeout
Implemented and merged by release-toolkit PR #1100 (merge commit
026d985f06; current main also contains it). Official review 6274 and 26/26 required contexts were successful. Tier 2 remains the recommended path; the separate deploy-key whitelist gradeability gap is tracked in #1105 and is not part of this implementation.✅ THE PREMISE THIS TRACKER RESTS ON IS NOW MEASURED, NOT ASSUMED — AND IT HOLDS. Deploy-key pushes DO escape anti-recursion suppression. So tier 3 buys the downstream trigger, not merely push-scoping, and it is worth building.
Two arms on
gitea.com/FrankenBit/tic-tac-toe, both pushing a tag from inside a workflow run, one variable:Re-checkable after the artifacts were removed:
The downstream run's ref is the deploy tag by name, so it binds to arm B and to nothing else in the history. Run-created-but-unclaimed was excluded the same way arm A excluded it: zero runs in a non-terminal state.
The isolation was verified rather than assumed, and the hazard was live.
actions/checkoutDID write the auto token into the local config —git config --local http.https://gitea.com/.extraheader AUTHORIZATION: basic ***appears in the log — and then removed it itself, becausepersist-credentials: falsemakes checkout clean up in its own step. Without a control there would have been checkout's write in the log and no way to show it was gone by push time:⚠️ What this does NOT change: the API-surface bound above stands unaltered.
write:repositoryis still the narrowest category Gitea offers, so tier 3 bounds the push surface fully and the API surface only as far as the owning account's reach. Tier 2 stays the default and tier 1 stays the default for anyone who sets nothing.📌 Reusable, and cheaper than this test was: a push to a ref outside
refs/heads/*andrefs/tags/*authorizes exactly like any push and triggers no workflow — measured, run count unchanged across a create and two deletes.refs/probe/<name>is a zero-cost write-capability probe.Measured by @quartermaster. Arms, controls and cleanup are his; the checkout-extraheader hazard was flagged before the run and proved live.