bug(release): the post-condition step's rt default-branch call gets neither override variable and no API token #1564

Open
opened 2026-10-06 12:05:11 +02:00 by claude · 0 comments
Member

#1160 found that default-branch layers 1 (RT_DEFAULT_BRANCH) and 3 (TOOLKIT_DEFAULT_BRANCH_HINT) were inert where recover-pending-cut calls rt default-branch, and wired them in. It compared that step with "the sibling callsite in reusable-release.yml", which is the act-on-decision step. A third callsite was not on #1160's list: the cut's post-condition step. That step sets neither variable, and it calls the API before the token is exported.

Measured

All reads are on live origin/main 6a36090, 2026-10-06.

The callsite.

  • .forgejo/workflows/reusable-release.yml:951, verify manifest bookkeeping (post-condition, #637), runs on every executed cut (:952: mode == 'cut' && cut_gate == 'execute').
  • At :983-986 it runs rt default-branch --config "$CONFIG_PATH_INPUT" --default-branch "${RT_DEFAULT_BRANCH:-}" --default-branch-hint "${TOOLKIT_DEFAULT_BRANCH_HINT:-}".
  • If that fails, it logs post-condition COULD NOT GRADE - default branch unresolvable and exits 2 (:987-988).
  • It is the only rt default-branch call in the file.

What each layer gets there.

Layer Source At this step
1 RT_DEFAULT_BRANCH empty. The step's env: (:954-967) does not set it, the file has no workflow- or job-level env:, and the only $GITHUB_ENV write is CUT_START_MS (:528). The act-on-decision step sets it (:615), but step env does not cross steps, as reusable-recover-pending-cut.yml:252-253 says (#1160).
2 default_branch: in the config file works if the adopter set it (--config)
3 TOOLKIT_DEFAULT_BRANCH_HINT empty, for the same reason (set only at :614)
4 Forgejo API anonymous. rt default-branch always calls the API (cmd/rt/default_branch.go, runDefaultBranch) with FORGEJO_TOKEN from the environment (newForge, cmd/rt/prep.go:905-908). This step exports FORGEJO_TOKEN at :1033, 50 lines after the call.
5 git symbolic-ref refs/remotes/origin/HEAD not measured: whether the consumer checkout (actions/checkout@v4, fetch-depth: 0, :183-187) creates that ref

An anonymous API read fails on a private repository. Measured on this forge: GET /api/v1/repos/frankenbit/cid-probe (private) returns 404 without a token and 200 with one. frankenbit/release-toolkit (public) returns 200 both ways. An API error falls through to layer 5 (internal/prep/git.go:309-313).

The .gitea twin has the same shape: the call is at :996-999 and the token export at :1046.

The comments say otherwise.

  • :979-982 calls this the "Same invocation as the path-α call at :685; the six-layer chain (#370/#379) is identical either way."
  • :685 is in the act-on-decision step, which no longer calls rt default-branch. The comments at :514 and :604 that say it does are stale as well.
  • The two steps' env is not identical.

Why toolkit-self never sees it. Its repository is public, so the anonymous API read answers at layer 4.

Why it matters

This is a hypothesis until layer 5 is measured (first AC). On a private adopter repository with no default_branch: in its config, layer 2 is empty too. If the checkout leaves origin/HEAD unset, every executed cut would then end with this step red (exit 2, could not grade), after the tag, the release and the manifest have already landed. The branch name does not matter: a repository on main is exposed the same way as one on master.

It also breaks a documented override. docs/integration.md tells adopters that a vars.RT_DEFAULT_BRANCH repo variable is picked up "automatically". At this step it is not.

Acceptance criteria

  • Measure, in a real Forgejo job, whether actions/checkout@v4 with fetch-depth: 0 leaves refs/remotes/origin/HEAD set. Record the run and the log line here. The ACs below do not wait on it; it decides how exposed past private-repo cuts were.
  • The post-condition step's env: wires RT_DEFAULT_BRANCH: ${{ vars.RT_DEFAULT_BRANCH }} and TOOLKIT_DEFAULT_BRANCH_HINT: ${{ github.event.repository.default_branch }}, as the act-on-decision step and recover-pending-cut do. The .gitea twin matches.
  • rt default-branch at this step reaches the API with the resolved token, not anonymously. The PR says how, e.g. by exporting the token before the call.
  • The comments at :514, :604 and :979-982 name the callsites as they are today.
  • A test fails when any step that runs rt default-branch in a reusable workflow lacks either variable in its env:. It is mutation-verified: it fails against 6a36090, where this step lacks both.
  • go test ./... and the bats suite pass.

Out of scope

  • rt prep falls back to "main" (cmd/rt/prep.go:731) when no layer resolves, where rt default-branch refuses. rt default-branch's own comment explains that refusal: a wrong guess is the #370 silent-corruption class. The act-on-decision step wires every layer, so the backstop is reached only through a misconfiguration. Whether it should refuse too is a separate decision.
  • rt manifest-postcondition fetches origin main when RT_DEFAULT_BRANCH is empty (cmd/rt/manifest_postcondition.go:280-284). In CI its result is then replaced by --landed-sha / --branch-readable (:157-160), so this costs one wasted fetch, not a wrong verdict.
  • The adopter docs for the default branch: #1565.
#1160 found that default-branch layers 1 (`RT_DEFAULT_BRANCH`) and 3 (`TOOLKIT_DEFAULT_BRANCH_HINT`) were inert where recover-pending-cut calls `rt default-branch`, and wired them in. It compared that step with "the sibling callsite in `reusable-release.yml`", which is the act-on-decision step. A third callsite was not on #1160's list: the cut's post-condition step. That step sets neither variable, and it calls the API before the token is exported. ## Measured All reads are on live `origin/main` `6a36090`, 2026-10-06. **The callsite.** - `.forgejo/workflows/reusable-release.yml:951`, `verify manifest bookkeeping (post-condition, #637)`, runs on every executed cut (`:952`: `mode == 'cut' && cut_gate == 'execute'`). - At `:983-986` it runs `rt default-branch --config "$CONFIG_PATH_INPUT" --default-branch "${RT_DEFAULT_BRANCH:-}" --default-branch-hint "${TOOLKIT_DEFAULT_BRANCH_HINT:-}"`. - If that fails, it logs `post-condition COULD NOT GRADE - default branch unresolvable` and exits 2 (`:987-988`). - It is the only `rt default-branch` call in the file. **What each layer gets there.** | Layer | Source | At this step | |---|---|---| | 1 | `RT_DEFAULT_BRANCH` | **empty**. The step's `env:` (`:954-967`) does not set it, the file has no workflow- or job-level `env:`, and the only `$GITHUB_ENV` write is `CUT_START_MS` (`:528`). The act-on-decision step sets it (`:615`), but step env does not cross steps, as `reusable-recover-pending-cut.yml:252-253` says (#1160). | | 2 | `default_branch:` in the config file | works if the adopter set it (`--config`) | | 3 | `TOOLKIT_DEFAULT_BRANCH_HINT` | **empty**, for the same reason (set only at `:614`) | | 4 | Forgejo API | **anonymous**. `rt default-branch` always calls the API (`cmd/rt/default_branch.go`, `runDefaultBranch`) with `FORGEJO_TOKEN` from the environment (`newForge`, `cmd/rt/prep.go:905-908`). This step exports `FORGEJO_TOKEN` at `:1033`, 50 lines after the call. | | 5 | `git symbolic-ref refs/remotes/origin/HEAD` | **not measured**: whether the consumer checkout (`actions/checkout@v4`, `fetch-depth: 0`, `:183-187`) creates that ref | An anonymous API read fails on a private repository. Measured on this forge: `GET /api/v1/repos/frankenbit/cid-probe` (private) returns 404 without a token and 200 with one. `frankenbit/release-toolkit` (public) returns 200 both ways. An API error falls through to layer 5 (`internal/prep/git.go:309-313`). The `.gitea` twin has the same shape: the call is at `:996-999` and the token export at `:1046`. **The comments say otherwise.** - `:979-982` calls this the "Same invocation as the path-α call at :685; the six-layer chain (#370/#379) is identical either way." - `:685` is in the act-on-decision step, which no longer calls `rt default-branch`. The comments at `:514` and `:604` that say it does are stale as well. - The two steps' env is not identical. **Why toolkit-self never sees it.** Its repository is public, so the anonymous API read answers at layer 4. ## Why it matters This is a hypothesis until layer 5 is measured (first AC). On a private adopter repository with no `default_branch:` in its config, layer 2 is empty too. If the checkout leaves `origin/HEAD` unset, every executed cut would then end with this step red (exit 2, could not grade), after the tag, the release and the manifest have already landed. The branch name does not matter: a repository on `main` is exposed the same way as one on `master`. It also breaks a documented override. `docs/integration.md` tells adopters that a `vars.RT_DEFAULT_BRANCH` repo variable is picked up "automatically". At this step it is not. ## Acceptance criteria - [ ] Measure, in a real Forgejo job, whether `actions/checkout@v4` with `fetch-depth: 0` leaves `refs/remotes/origin/HEAD` set. Record the run and the log line here. The ACs below do not wait on it; it decides how exposed past private-repo cuts were. - [ ] The post-condition step's `env:` wires `RT_DEFAULT_BRANCH: ${{ vars.RT_DEFAULT_BRANCH }}` and `TOOLKIT_DEFAULT_BRANCH_HINT: ${{ github.event.repository.default_branch }}`, as the act-on-decision step and recover-pending-cut do. The `.gitea` twin matches. - [ ] `rt default-branch` at this step reaches the API with the resolved token, not anonymously. The PR says how, e.g. by exporting the token before the call. - [ ] The comments at `:514`, `:604` and `:979-982` name the callsites as they are today. - [ ] A test fails when any step that runs `rt default-branch` in a reusable workflow lacks either variable in its `env:`. It is mutation-verified: it fails against `6a36090`, where this step lacks both. - [ ] `go test ./...` and the bats suite pass. ## Out of scope - `rt prep` falls back to `"main"` (`cmd/rt/prep.go:731`) when no layer resolves, where `rt default-branch` refuses. `rt default-branch`'s own comment explains that refusal: a wrong guess is the #370 silent-corruption class. The act-on-decision step wires every layer, so the backstop is reached only through a misconfiguration. Whether it should refuse too is a separate decision. - `rt manifest-postcondition` fetches `origin main` when `RT_DEFAULT_BRANCH` is empty (`cmd/rt/manifest_postcondition.go:280-284`). In CI its result is then replaced by `--landed-sha` / `--branch-readable` (`:157-160`), so this costs one wasted fetch, not a wrong verdict. - The adopter docs for the default branch: #1565.
Sign in to join this conversation.
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#1564
No description provided.