feat(release): report a caller missing secrets: inherit, not silence #1265
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!1265
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/1258-secrets-inherit-check"
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?
Refs frankenbit/release-toolkit#1258
AC-by-AC
rt secrets-inherit-check(internal/secretsinherit) statically parses a caller workflow file and names any job whoseuses:invokes a secrets-needing release-toolkit reusable without a siblingsecrets: inherit— file, job name, and reusable name.TestCheck_MessageNamesLossNotAbsence, which asserts the forbidden phrasing is absent.NeedsSecretstable (hand-maintained — Forgejo doesn't parse asecrets:schema underworkflow_call, perreusable-release.yml's own trigger comment, so there's no runtime signal to derive it from) marksac-closure-check,changelog-body-check,changelog-fragment-check,manifest-check,prepared-uncut-check,register-check,workflow-parse-checkas needing no secret — omittingsecrets: inheritthere is never flagged. A reusable name absent from the table is reported as unresolved, never silently assumed safe.Why this is a distinct check, not a duplicate of
rt credentials --checkinternal/credentials.Resolvealready grades runtime credential tiers, and correctly treats "nothing set" (Tier 1) as valid — that's the documented no-token default. But Tier 1 is reached two ways that look identical at runtime: no token configured (fine, by choice), or a token is configured in the repo's secrets but the caller'ssecrets: inheritis missing, so it never arrives.rt credentials --checkcannot tell these apart; this new check reads the caller's own YAML text instead, which can.Wiring
Added as an early step in
reusable-release.yml, right before the existing "grade the credential configuration" step, withcontinue-on-error: true— this discloses, it does not refuse. Nothing about whether a cut can proceed onGITHUB_TOKENalone has changed; the defect this closes is that the omission was silent, not that it exists. Regenerated the.gitea/workflowstwin (rt gitea-twin --write) and validated both files withforgejo-runner validate --workflow.Excluded from the C5 contract the same way
post-cutis (cli-surface.md's own wording for that row): it grades the adopter's caller YAML, but the reusable calls it — no adopter types or wiresrt secrets-inherit-checkthemselves.Scope note, flagged for review: this PR wires the check into
reusable-release.ymlonly (the highest-traffic path).reusable-mirror-release.ymlandreusable-recover-pending-cut.ymlare also inNeedsSecretsand the standalone command already covers their caller wrappers if run directly — I did not wire an early step into those two reusables themselves, to keep this PR reviewable. Natural follow-up if wanted.Also flagged: while wiring the new step,
forgejo-runner validatecaught a real YAML bug in my own draft — an unquoted stepname:containingsecrets: inheritparses as a nested mapping key ("mapping values are not allowed in this context"). Fixed by quoting the name. Left as a comment in the diff rather than silently fixed, since it's exactly the kind of caller-YAML footgun this ticket is about, just at the wrong layer (this repo's own step name, not an adopter's job).Verification
go build ./.../go vet ./.../go test ./... -count=1cleanbats tests/*.bats— 163/163 passgofmt -lcleanforgejo-runner validate --workflowclean on both.forgejo/workflows/reusable-release.ymland its.gitea/twinrt gitea-twin --check— PASS, twins matchrt fragment-check changelog.d— density PASSrt pre-push— 6 PASS / 0 FAIL / 22 could-not-gradert secrets-inherit-check .forgejo/workflows— PASS against this repo's own callers (dogfooded, including the local./formmirror-release.ymluses)secrets: inheritguard, the not-needed guard, and the unknown-reusable guard each redden exactly the test that names them, confirmed via actualgo testoutput, restored clean after each.🤖 Generated with Claude Code
https://claude.ai/code/session_01M8RrscAu123S6gXTGruwnq
APPROVED at
217d15b8. The table is correct today — I verified all ten entries independently — and the fail-closed design on unknown names is right. One finding: the table's own staleness is unpinned, and the fix is a derivation I can hand you.The table is 10/10 correct, derived rather than read
I enumerated every reusable in the repo and asked which actually consume a secret beyond
GITHUB_TOKEN:Ten reusables, ten entries, exact correspondence. No reusable is missing and no verdict is wrong.
✅ And
Unknownis the right shape. An absent name reports "this table is stale, not that the caller is safe" — a third state rather than a boolean, which is the distinction that keeps a hand-maintained catalogue honest as it drifts forward. The message says it in those words.🔴 The finding: the table's
falseandtrueverdicts are pinned only by accidentI mutated each entry separately:
⚠️
releaseandmirror-releaseare pinned only because the fixtures happen to name them.recover-pending-cutgenuinely needs three secrets, and marking itfalsemakes the checker pass every caller of it — exactly the silence this tracker exists to end, arriving through the checker rather than around it.⚠️ M4 is the drift-forward case: someone adds a reusable and pre-declares it safe in the same commit.
Unknownnever fires, because the name IS in the table — the third state protects against omission, not against a wrong entry.The fix is a derivation, and it closes all three at once
The truth is already in the tree. I built this to check your table and it took one pass:
An arm that globs
.forgejo/workflows/reusable-*.yml, derivesneeds a secretfrom whether anything beyondGITHUB_TOKENappears, and asserts set-equality withNeedsSecretswould redden on M2, M3 and M4. It also fails loudly when a reusable is added and not catalogued at all — turningUnknownfrom a runtime disclosure into a build-time refusal for this repo's own reusables.🔑 That does not make the table derivable in general — an adopter's checker still needs the shipped catalogue, and your reasoning for why Forgejo offers no runtime signal is sound and I am not disputing it. It makes the SHIPPED table checkable against the repo it ships from, which is the only place the truth exists.
📌 Not blocking, same line I drew on
#1244: the code is correct and what is missing is coverage. ⚠️ But I want to be straight that @bosun noted the cost of that call there —#1244's gap reachedmain— so if he would rather hold this for a 15-line arm, I would not argue. It wants a tracker either way and I would rather it were this PR.The
continue-on-errorcall is right, and this is the case the rule carves out/srv/CLAUDE.mdsays a disclosure that cannot change the exit status is punctuation — and it names the exception this lands in: PASS BUT DISCLOSE, where the mechanism's scope has narrowed. Refusing here would fail every adopter's release on a static parse of their workflow, which is the shape a refusal gets routed around. The comment at:323states the reasoning at the point of use rather than in the tracker.✅ And the message is exactly AC2: "VERIFICATION DOES NOT RUN", "a push made with that token cannot trigger downstream CI", never "secret not found". It says what the adopter LOSES. The
#809cross-reference is what makes it actionable rather than alarming.On the bug your own validator caught
📌 An unquoted step
name:containingsecrets: inheritparsing as a nested mapping is worth the paragraph you gave it. It is this ticket's own hazard one layer up — a config whose text is indistinguishable from its structure — and- name: "check the caller's secrets: inherit wiring"is quoted at:327, so the fix is in. Finding it with a validator rather than at runtime is the whole argument for running one.Scope
Not graded: the
.giteatwin beyondgitea-twin --checkpassing, and whether#1258AC3 ("distinguishes omitted from deliberately-not-needed") is fully answered — you have thefalseentries as the "deliberately not needed" case, which I read as satisfying it, but that is your AC to tick, not mine to interpret.