bug(release): the Gitea twin's secrets-inherit-check reads .forgejo/workflows and matches only .forgejo spellings, so a Gitea adopter is never graded #1535

Open
opened 2026-10-03 14:52:37 +02:00 by claude · 0 comments
Member

The Gitea twin of the release workflow runs rt secrets-inherit-check with no directory argument. The verb then reads .forgejo/workflows, which a Gitea adopter does not have, so the step exits 2 on every Gitea cut and never grades the caller. Even pointed at .gitea/workflows, the check passes a caller that lacks secrets: inherit, because its uses: pattern knows only the .forgejo/ spelling.

Measured

At origin/main 8086700:

.gitea/workflows/reusable-release.yml:456   - name: "check the caller's secrets: inherit wiring"
.gitea/workflows/reusable-release.yml:457     continue-on-error: true
.gitea/workflows/reusable-release.yml:461     rt secrets-inherit-check            <- no WORKFLOWS_DIR argument
.forgejo/workflows/reusable-release.yml:448   rt secrets-inherit-check            <- the source, identical
cmd/rt/secrets_inherit_check.go:44           dir := ".forgejo/workflows"         <- the default
internal/secretsinherit/check.go:53          usesReusable = ^(?:frankenbit/release-toolkit/\.forgejo/workflows/|\./\.forgejo/workflows/)reusable-([a-z0-9-]+)\.yml(?:@.+)?$

Why the twin did not catch it. internal/giteatwin/giteatwin.go:44-45,107 rewrites the YAML literal .forgejo/workflows/ to .gitea/workflows/. The default directory lives in Go, not in the YAML, so the generator has nothing to rewrite. The step (050133a, #1258, 2026-09-06) landed three days after the generator (c31d531, #1092, 2026-09-03).

Probes. I built rt from origin/main 8086700 and ran it natively on Windows and as linux/amd64 in golang:1.25. Each tree holds one caller release.yml with a job that uses: the release reusable and has no secrets: inherit.

Caller Command Result
Gitea: .gitea/workflows/release.yml, uses: https://gitea.com/FrankenBit/release-toolkit/.gitea/workflows/reusable-release.yml@v0.63.1 (the form in docs/integration.md:146) rt secrets-inherit-check, as the twin runs it rc=2, Error: read .forgejo/workflows: open .forgejo/workflows: no such file or directory
same rt secrets-inherit-check .gitea/workflows rc=0, PASS — every caller of a secrets-needing release-toolkit reusable under .gitea/workflows passes secrets: inherit (false pass)
Gitea, short form frankenbit/release-toolkit/.gitea/workflows/reusable-release.yml@v0.63.1 rt secrets-inherit-check .gitea/workflows rc=0, PASS (false pass)
Control: Forgejo, .forgejo/workflows/release.yml, uses: frankenbit/release-toolkit/.forgejo/workflows/reusable-release.yml@v0.63.1 rt secrets-inherit-check rc=1, ::warning::… job "release" calls reusable-release.yml without secrets: inherit …
Forgejo, absolute form from docs/integration.md:117, uses: https://gitea.com/FrankenBit/release-toolkit/.forgejo/workflows/reusable-release.yml@v0.63.1 rt secrets-inherit-check rc=0, PASS (false pass)

The last row is the same regex at check.go:53, which is anchored on the short frankenbit/… form and is case-sensitive. The guide recommends the absolute form for "the normal case" of an adopter whose instance does not carry the toolkit.

What a Gitea adopter would see. On every release run, the step "check the caller's secrets: inherit wiring" prints the could-not-grade line above and exits 2. Because of continue-on-error: true the job goes on. The ::warning:: that #1258 exists to deliver never appears, whether or not the caller passes secrets: inherit:

  • A Gitea adopter who forgot secrets: inherit is never told. By the warning's own text (#809), a pipeline on GITHUB_TOKEN alone pushes with a token that cannot trigger downstream CI. That text was written for Forgejo; I did not measure Gitea's anti-recursion behaviour.
  • A correctly wired Gitea adopter gets an error line on every cut that looks like a defect in its own setup.

I did not measure how Gitea renders a failed step under continue-on-error.

Who is affected today: nobody (latent). I read the five adopters' trees over the API on 2026-10-03:

  • storey, ember, cellblock and purser have only .forgejo/workflows.
  • tmux-tell also has .gitea/workflows, but it holds only deploy.yml. Its release.yml is under .forgejo/workflows.

Where Gitea reads workflows. Gitea's quickstart puts workflows "in the directory .gitea/workflows/ of the repository" (https://docs.gitea.com/usage/actions/quickstart, read 2026-10-03). The toolkit's own Gitea guidance (docs/integration.md §②) and the twin set assume the same.

Not covered by #1530. That consolidation keeps reusable-release.yml and lists retiring the Gitea twins as out of scope.

Why it matters

Gitea is a supported forge: the twin set, gitea-twin-check and docs/integration.md §② exist for it. #1258 made a missing secrets: inherit visible because the failure it causes is silent. Every release looks like one that passed, but verification never ran. On Gitea that visibility is gone, and in its place is an error line that blames nothing in particular.

Decision (for the implementer)

  • (a) Pass the directory in the source YAML with the trailing slash: rt secrets-inherit-check .forgejo/workflows/. Right when the twin generator should carry it. The generator rewrites exactly that literal, so the twin would read .gitea/workflows/ with no Go change for the directory. The verb accepts the trailing slash: on the control tree, rt secrets-inherit-check .forgejo/workflows/ gave rc=1 with the warning.
  • (b) Have the verb find the directory when none is given, for example .forgejo/workflows, then .gitea/workflows. Right if the verb is also run by hand on Gitea. It needs a rule for a tree that has both, as tmux-tell does.
  • Either way, the uses: pattern at check.go:53 needs the .gitea/ spelling. Without it, (a) and (b) only turn an rc=2 into a false PASS.

Acceptance criteria

  • On a Gitea-shaped tree, with the caller under .gitea/workflows/ and no secrets: inherit, the .gitea twin's step exits 1 with the ::warning::. With secrets: inherit present it passes. Both the short and the absolute .gitea/ spellings are graded.
  • usesReusable (check.go:53) recognises the .gitea/workflows/ spelling and the absolute https://<host>/<owner>/release-toolkit/… form, with the owner matched case-insensitively. Alternatively, a uses: that names release-toolkit/…/reusable-*.yml in a form the pattern cannot pin is reported as could-not-grade, never as PASS.
  • Go arms cover every spelling in the table above. Each is mutation-verified: reverting the change reddens it (AGENTS.md section 8, Axis B).
  • An arm grades the shipped .gitea twin's step, not a copy of it: run as written against a Gitea-shaped tree, it grades the caller.
  • rt gitea-twin --check passes after regeneration.

Out of scope

  • Other verbs that default to .forgejo/workflows (consumer-wrappers at cmd/rt/consumer_wrappers.go:19, and check-self-bootstrap's --workflows-dir). Neither is invoked from a .gitea/ twin on origin/main, so neither reaches a Gitea adopter.
  • Making the step refuse. It stays continue-on-error, a disclosure, as #1258 decided.
  • Callers under .github/workflows/. Whether this forge would run one is not established (AGENTS.md section 14).

Anchor

Found 2026-10-03 while reviewing PR #1531 / issue #1529, in the research on GitHub portability. The adopter-tree reads and the probes above were made the same day.

The Gitea twin of the release workflow runs `rt secrets-inherit-check` with no directory argument. The verb then reads `.forgejo/workflows`, which a Gitea adopter does not have, so the step exits 2 on every Gitea cut and never grades the caller. Even pointed at `.gitea/workflows`, the check passes a caller that lacks `secrets: inherit`, because its `uses:` pattern knows only the `.forgejo/` spelling. ## Measured At origin/main `8086700`: ``` .gitea/workflows/reusable-release.yml:456 - name: "check the caller's secrets: inherit wiring" .gitea/workflows/reusable-release.yml:457 continue-on-error: true .gitea/workflows/reusable-release.yml:461 rt secrets-inherit-check <- no WORKFLOWS_DIR argument .forgejo/workflows/reusable-release.yml:448 rt secrets-inherit-check <- the source, identical cmd/rt/secrets_inherit_check.go:44 dir := ".forgejo/workflows" <- the default internal/secretsinherit/check.go:53 usesReusable = ^(?:frankenbit/release-toolkit/\.forgejo/workflows/|\./\.forgejo/workflows/)reusable-([a-z0-9-]+)\.yml(?:@.+)?$ ``` **Why the twin did not catch it.** `internal/giteatwin/giteatwin.go:44-45,107` rewrites the YAML literal `.forgejo/workflows/` to `.gitea/workflows/`. The default directory lives in Go, not in the YAML, so the generator has nothing to rewrite. The step (050133a, #1258, 2026-09-06) landed three days after the generator (c31d531, #1092, 2026-09-03). **Probes.** I built `rt` from origin/main `8086700` and ran it natively on Windows and as linux/amd64 in `golang:1.25`. Each tree holds one caller `release.yml` with a job that `uses:` the release reusable and has no `secrets: inherit`. | Caller | Command | Result | |---|---|---| | Gitea: `.gitea/workflows/release.yml`, `uses: https://gitea.com/FrankenBit/release-toolkit/.gitea/workflows/reusable-release.yml@v0.63.1` (the form in `docs/integration.md:146`) | `rt secrets-inherit-check`, as the twin runs it | rc=2, `Error: read .forgejo/workflows: open .forgejo/workflows: no such file or directory` | | same | `rt secrets-inherit-check .gitea/workflows` | rc=0, `PASS — every caller of a secrets-needing release-toolkit reusable under .gitea/workflows passes secrets: inherit` (**false pass**) | | Gitea, short form `frankenbit/release-toolkit/.gitea/workflows/reusable-release.yml@v0.63.1` | `rt secrets-inherit-check .gitea/workflows` | rc=0, PASS (**false pass**) | | **Control:** Forgejo, `.forgejo/workflows/release.yml`, `uses: frankenbit/release-toolkit/.forgejo/workflows/reusable-release.yml@v0.63.1` | `rt secrets-inherit-check` | rc=1, `::warning::… job "release" calls reusable-release.yml without secrets: inherit …` | | Forgejo, absolute form from `docs/integration.md:117`, `uses: https://gitea.com/FrankenBit/release-toolkit/.forgejo/workflows/reusable-release.yml@v0.63.1` | `rt secrets-inherit-check` | rc=0, PASS (**false pass**) | The last row is the same regex at check.go:53, which is anchored on the short `frankenbit/…` form and is case-sensitive. The guide recommends the absolute form for "the normal case" of an adopter whose instance does not carry the toolkit. **What a Gitea adopter would see.** On every release run, the step "check the caller's secrets: inherit wiring" prints the could-not-grade line above and exits 2. Because of `continue-on-error: true` the job goes on. The `::warning::` that #1258 exists to deliver never appears, whether or not the caller passes `secrets: inherit`: - A Gitea adopter who forgot `secrets: inherit` is never told. By the warning's own text (#809), a pipeline on `GITHUB_TOKEN` alone pushes with a token that cannot trigger downstream CI. That text was written for Forgejo; I did not measure Gitea's anti-recursion behaviour. - A correctly wired Gitea adopter gets an error line on every cut that looks like a defect in its own setup. I did not measure how Gitea renders a failed step under `continue-on-error`. **Who is affected today: nobody (latent).** I read the five adopters' trees over the API on 2026-10-03: - storey, ember, cellblock and purser have only `.forgejo/workflows`. - tmux-tell also has `.gitea/workflows`, but it holds only `deploy.yml`. Its `release.yml` is under `.forgejo/workflows`. **Where Gitea reads workflows.** Gitea's quickstart puts workflows "in the directory .gitea/workflows/ of the repository" (https://docs.gitea.com/usage/actions/quickstart, read 2026-10-03). The toolkit's own Gitea guidance (`docs/integration.md` §②) and the twin set assume the same. **Not covered by #1530.** That consolidation keeps `reusable-release.yml` and lists retiring the Gitea twins as out of scope. ## Why it matters Gitea is a supported forge: the twin set, `gitea-twin-check` and `docs/integration.md` §② exist for it. #1258 made a missing `secrets: inherit` visible because the failure it causes is silent. Every release looks like one that passed, but verification never ran. On Gitea that visibility is gone, and in its place is an error line that blames nothing in particular. ## Decision (for the implementer) - **(a) Pass the directory in the source YAML** with the trailing slash: `rt secrets-inherit-check .forgejo/workflows/`. Right when the twin generator should carry it. The generator rewrites exactly that literal, so the twin would read `.gitea/workflows/` with no Go change for the directory. The verb accepts the trailing slash: on the control tree, `rt secrets-inherit-check .forgejo/workflows/` gave rc=1 with the warning. - **(b) Have the verb find the directory** when none is given, for example `.forgejo/workflows`, then `.gitea/workflows`. Right if the verb is also run by hand on Gitea. It needs a rule for a tree that has both, as tmux-tell does. - Either way, the `uses:` pattern at check.go:53 needs the `.gitea/` spelling. Without it, (a) and (b) only turn an rc=2 into a false PASS. ## Acceptance criteria - [ ] On a Gitea-shaped tree, with the caller under `.gitea/workflows/` and no `secrets: inherit`, the `.gitea` twin's step exits 1 with the `::warning::`. With `secrets: inherit` present it passes. Both the short and the absolute `.gitea/` spellings are graded. - [ ] `usesReusable` (check.go:53) recognises the `.gitea/workflows/` spelling and the absolute `https://<host>/<owner>/release-toolkit/…` form, with the owner matched case-insensitively. Alternatively, a `uses:` that names `release-toolkit/…/reusable-*.yml` in a form the pattern cannot pin is reported as could-not-grade, never as PASS. - [ ] Go arms cover every spelling in the table above. Each is mutation-verified: reverting the change reddens it (AGENTS.md section 8, Axis B). - [ ] An arm grades the shipped `.gitea` twin's step, not a copy of it: run as written against a Gitea-shaped tree, it grades the caller. - [ ] `rt gitea-twin --check` passes after regeneration. ## Out of scope - Other verbs that default to `.forgejo/workflows` (`consumer-wrappers` at `cmd/rt/consumer_wrappers.go:19`, and `check-self-bootstrap`'s `--workflows-dir`). Neither is invoked from a `.gitea/` twin on origin/main, so neither reaches a Gitea adopter. - Making the step refuse. It stays `continue-on-error`, a disclosure, as #1258 decided. - Callers under `.github/workflows/`. Whether this forge would run one is not established (AGENTS.md section 14). ## Anchor Found 2026-10-03 while reviewing PR #1531 / issue #1529, in the research on GitHub portability. The adopter-tree reads and the probes above were made the same day.
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#1535
No description provided.