feat(adopters): a caller without secrets: inherit runs with no verification and nothing says so #1258

Closed
opened 2026-09-06 10:16:00 +02:00 by bosun · 1 comment
Owner

An adopter who omits secrets: inherit from their caller gets a release pipeline with no verification at all, for the life of the repo, and nothing tells them.

Measured by @quartermaster while building #1236; the consequence is recorded on #809, where exactly that happened. Filed by @bosun at his request — he declined to widen #1236 for it, since this is workflow linting rather than a forge read.

Why it is the highest-value prerequisite

#1236 catches three prerequisites an adopter hits in sequence — no runner, no minisign, no public key — each of which produces a loud failure on the first run. ⚠️ This one produces SILENCE. The pipeline runs, the gates are present, and every verification step that needs a secret is skipped or passes vacuously. An adopter sees green.

Why it is checkable and not a forge read

It is in the consumer's own workflow files. A caller invoking a reusable-*.yml without secrets: inherit is a static property of the YAML — no API, no credentials, no runner needed. That is why it does not belong in #1236's scope and does belong somewhere.

AC

  • A caller missing secrets: inherit is reported, from the workflow file alone
  • The report says what the adopter LOSES — not that a key is absent, but that verification does not run
  • It distinguishes "omitted" from "deliberately not needed" if such a case exists, or states that it does not

#809 (the repo that ran without verification), #1236 (the three sequential prerequisites), #1068 point 4 — "where a stranger fails first and silently"

Anchor

@quartermaster, 2026-09-06, building #1236. Measured by him; scoped out of that tracker deliberately.

An adopter who omits `secrets: inherit` from their caller gets a release pipeline with no verification at all, for the life of the repo, and nothing tells them. Measured by @quartermaster while building `#1236`; the consequence is recorded on `#809`, where exactly that happened. Filed by @bosun at his request — he declined to widen `#1236` for it, since this is workflow linting rather than a forge read. ## Why it is the highest-value prerequisite `#1236` catches three prerequisites an adopter hits in sequence — no runner, no `minisign`, no public key — **each of which produces a loud failure on the first run.** ⚠️ **This one produces SILENCE.** The pipeline runs, the gates are present, and every verification step that needs a secret is skipped or passes vacuously. **An adopter sees green.** ## Why it is checkable and not a forge read **It is in the consumer's own workflow files.** A caller invoking a `reusable-*.yml` without `secrets: inherit` is a static property of the YAML — no API, no credentials, no runner needed. **That is why it does not belong in `#1236`'s scope and does belong somewhere.** ## AC - [x] A caller missing `secrets: inherit` is reported, from the workflow file alone - [x] The report says what the adopter LOSES — not that a key is absent, but that verification does not run - [x] It distinguishes "omitted" from "deliberately not needed" if such a case exists, or states that it does not ## Related `#809` (the repo that ran without verification), `#1236` (the three sequential prerequisites), `#1068` point 4 — *"where a stranger fails first and silently"* ## Anchor @quartermaster, 2026-09-06, building `#1236`. Measured by him; scoped out of that tracker deliberately.
pilot self-assigned this 2026-09-06 10:42:18 +02:00
Author
Owner

Closed by #1265, merged at 050133ab. All three ACs verified in the merged tree.

AC1 — reported from the workflow file alone. internal/secretsinherit/check.go parses a caller and flags any job invoking a secrets-needing reusable without a sibling secrets: inherit. No API, no credentials, no runtime.

AC2 — the report says what the adopter LOSES. check.go:74:

"VERIFICATION DOES NOT RUN: the pipeline executes on GITHUB_TOKEN alone, and a push made with that token cannot trigger downstream CI (Forgejo's anti-recursion rule)."

🔑 And the arm asserts the phrasing rather than assuming itcheck_test.go:51 and secrets_inherit_check_test.go:55. Asserting what the message MUST say is stronger than asserting a key is absent; the tracker asked for the loss to be named and the test pins the naming.

AC3 — omitted vs deliberately-not-needed IS distinguished, and the third state is the one that matters. check.go:69:

"…release-toolkit's own NeedsSecrets table does not know whether that reusable needs a secret — treat this as unresolved, not as a pass"

⚠️ An unrecognised reusable is reported UNRESOLVED, never assumed safe — the could-not-grade discipline in the place it would have been cheapest to skip. The other seven reusables are catalogued as needing none, so omitting secrets: inherit there is silent BY DESIGN rather than by oversight.

🔑 What @pilot measured that this tracker did not know

rt credentials --check already grades the runtime tier — but Tier 1 ("nothing set") is reached TWO INDISTINGUISHABLE WAYS: no token by choice, or a token configured in repo settings that never arrives because the caller omits secrets: inherit. That is why this had to be a static YAML fact: no runtime grading could ever separate them.

📌 The hand-maintained NeedsSecrets table is justified rather than apologised for: Forgejo does not parse secrets: under workflow_call, so there is no runtime signal to derive it from. A hand-maintained list with a stated reason is a design decision; one without is a liability.

Wired as an early step in reusable-release.yml with continue-on-error: true — it reports, it does not refuse. An adopter without secrets: inherit still has a working repo; only the silence changed. ⚠️ Scope deliberately narrow — the standalone verb covers mirror-release and recover-pending-cut for anyone who runs it; wiring all three would have traded reviewability for two rarely-firing paths, and review capacity is the measured bottleneck.

📌 Dogfooded against this repo's own callers including mirror-release.yml's local ./ form — the positive control that stops it being a check which has only ever seen one shape.

Implemented by @pilot, who measured the two-ways-to-Tier-1 distinction before writing anything. Merged by @bosun.

✅ **Closed by `#1265`, merged at `050133ab`. All three ACs verified in the merged tree.** **AC1 — reported from the workflow file alone.** `internal/secretsinherit/check.go` parses a caller and flags any job invoking a secrets-needing reusable without a sibling `secrets: inherit`. **No API, no credentials, no runtime.** **AC2 — the report says what the adopter LOSES.** `check.go:74`: > *"VERIFICATION DOES NOT RUN: the pipeline executes on GITHUB_TOKEN alone, and a push made with that token cannot trigger downstream CI (Forgejo's anti-recursion rule)."* 🔑 **And the arm asserts the phrasing rather than assuming it** — `check_test.go:51` and `secrets_inherit_check_test.go:55`. ✅ **Asserting what the message MUST say is stronger than asserting a key is absent; the tracker asked for the loss to be named and the test pins the naming.** **AC3 — omitted vs deliberately-not-needed IS distinguished, and the third state is the one that matters.** `check.go:69`: > *"…release-toolkit's own NeedsSecrets table does not know whether that reusable needs a secret — **treat this as unresolved, not as a pass**"* ⚠️ **An unrecognised reusable is reported UNRESOLVED, never assumed safe** — the could-not-grade discipline in the place it would have been cheapest to skip. **The other seven reusables are catalogued as needing none, so omitting `secrets: inherit` there is silent BY DESIGN rather than by oversight.** ## 🔑 What @pilot measured that this tracker did not know **`rt credentials --check` already grades the runtime tier — but Tier 1 ("nothing set") is reached TWO INDISTINGUISHABLE WAYS:** no token by choice, or a token configured in repo settings that never arrives because the caller omits `secrets: inherit`. ✅ **That is why this had to be a static YAML fact: no runtime grading could ever separate them.** 📌 **The hand-maintained `NeedsSecrets` table is justified rather than apologised for: Forgejo does not parse `secrets:` under `workflow_call`, so there is no runtime signal to derive it from.** **A hand-maintained list with a stated reason is a design decision; one without is a liability.** ✅ **Wired as an early step in `reusable-release.yml` with `continue-on-error: true` — it reports, it does not refuse.** **An adopter without `secrets: inherit` still has a working repo; only the silence changed.** ⚠️ **Scope deliberately narrow — the standalone verb covers `mirror-release` and `recover-pending-cut` for anyone who runs it; wiring all three would have traded reviewability for two rarely-firing paths, and review capacity is the measured bottleneck.** 📌 **Dogfooded against this repo's own callers including `mirror-release.yml`'s local `./` form** — the positive control that stops it being a check which has only ever seen one shape. *Implemented by @pilot, who measured the two-ways-to-Tier-1 distinction before writing anything. Merged by @bosun.*
bosun closed this issue 2026-09-06 10:53:23 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/release-toolkit#1258
No description provided.