chore(release): v0.20.0 #278

Merged
bosun merged 2 commits from i/271-fragment-check-reusable into main 2026-07-02 10:16:32 +02:00

Added

A push-whitelist pre-flight now runs at the start of a path-α cut, before draft-release.sh does any work. It probes the consumer repo's default-branch protection and fails fast with an actionable message if the release-bot identity (the push token's owner) isn't in push_whitelist_usernames — converting the late, mid-cut Not allowed to push to protected branch rejection (which leaves the manifest desynced behind the tag/release, as the tmux-tell first toolkit cut hit) into an early, clear failure naming the missing identity. It fails only on a definitive misconfig (whitelist enabled + identity absent + no covering team) and warns-and-proceeds on anything it can't determine, so a probe limitation never blocks a cut that would actually succeed. Path-γ cuts (PR-mediated manifest, no direct-push) skip the check. docs/integration.md now states the push_whitelist membership as an explicit path-α-only precondition.

Fixed

The rolling-PR chore(release): prepare vX.Y.Z push now authenticates with FORGEJO_TOKEN (the path-α RELEASE_TOOLKIT_TOKEN PAT) instead of the actions/checkout-persisted system token. Previously the prepare-commit push was a plain git push that reused the workflow's system token, which Forgejo Actions' anti-recursion safeguard suppresses — so no CI ran on the rolling PR and its required checks could never be satisfied, blocking the merge (observed on the first toolkit-driven tmux-tell cut). The push now injects the token as an HTTP Authorization header (mirroring the manifest push), so a real-user PAT triggers downstream CI; local/standalone runs with no FORGEJO_TOKEN set push plainly as before. Note: this is necessary but not sufficient — the consumer must still set RELEASE_TOOLKIT_TOKEN to a real-user PAT (the documented path-α setup) for CI to fire.

  • Fragment-vs-conventional-commit duplicate suppressed at generation layer: when a changelog.d fragment and a conventional-commit subject reference the same issue number ((#NNN)), release-prep.sh now suppresses the CC-subject bullet in favour of the fragment prose. Previously both entries appeared (the v0.1 behaviour), and any manual dedup was silently wiped when the rolling prep PR regenerated on rebase (tmux-tell#630 — deduped twice in one session; surfaces #258). Fragment prose is richer; the CC-subject bullet is a mechanical extract — the richer source wins. A [changelog-dedup] line is logged to stderr for auditability. (#258)

release-decide now detects the chore(release): prepare vX.Y.Z commit anywhere in the merged rolling-PR range, not only at HEAD. Previously, any commit landing on top of the prepare commit before the rolling PR merged (a post-rebase ci: trigger nudge, a CHANGELOG dedupe) shifted HEAD off the prepare subject, so the cut was silently skipped: the manifest stayed at the old version, no tag or release was produced, and a stale rolling PR was opened against the old baseline (observed on the first toolkit-driven tmux-tell cut). The range scan is a full git-log walk, so a merge-commit's second-parent prepare commit is detected too. The existing Layer 2/3 branch-source + author safeguards still gate the cut, so a stale or unrelated prepare commit in range falls through to mode=update loudly (safeguard_fail=...) rather than silently. Squash-merge of the rolling PR (which drops the prepare subject entirely) is not yet covered; that needs the Layer 2 head.ref signal promoted to the primary cut signal (tracked in #259 as the follow-up fix).

Post-cut bake-reset commit no longer fails with "nothing to commit" on external consumers. The reusable-release.yml's bake-reset blocks (path-α and path-γ) now wrap the git commit + push in a git diff --cached --quiet guard — when the consumer doesn't have .forgejo/workflows/reusable-release.yml / reusable-manifest-check.yml in its own checkout (true for every external consumer; the bake-reset is toolkit-self-only by design), the staging area is empty after the git add loop and the commit/push are cleanly skipped instead of halting the workflow. This was the substrate-bug behind every external cut since v0.22.0 needing manual manifest recovery: the bake-reset step's failure cascaded into the manifest update never landing, so release-decide.sh couldn't advance its walk window on the next run. Toolkit-self continues to operate unchanged — the toolkit's own workflow files ARE present in its own checkout, so git diff --cached reports staged changes and the commit + push proceed normally.

release-prep.sh PR body assembly now re-normalizes through changelog_merge_sections so any surviving duplicate ### Kind headings collapse to one section per kind. Belt-and-suspenders on top of the merge already done in changelog_transition; ensures the rolling-PR body stays idempotent-clean at the assembly step regardless of upstream drift. Closes #272.

forgejo_find_pr_by_head now filters response client-side by exact head.label / head.ref match. Forgejo's head= query filter is empirically ignored (returns every open PR); without the client-side guard, .[0].number picked whichever PR the API sorted first — mutating a random feature PR's title + body instead of the actual rolling PR (release-toolkit#274, reproduced on tmux-tell v0.29.0 cycle 2026-07-01). No consumer-side change required. Closes #274.

### Added A push-whitelist pre-flight now runs at the start of a path-α cut, before `draft-release.sh` does any work. It probes the consumer repo's default-branch protection and fails fast with an actionable message if the release-bot identity (the push token's owner) isn't in `push_whitelist_usernames` — converting the late, mid-cut `Not allowed to push to protected branch` rejection (which leaves the manifest desynced behind the tag/release, as the tmux-tell first toolkit cut hit) into an early, clear failure naming the missing identity. It fails only on a *definitive* misconfig (whitelist enabled + identity absent + no covering team) and warns-and-proceeds on anything it can't determine, so a probe limitation never blocks a cut that would actually succeed. Path-γ cuts (PR-mediated manifest, no direct-push) skip the check. `docs/integration.md` now states the `push_whitelist` membership as an explicit path-α-only precondition. ### Fixed The rolling-PR `chore(release): prepare vX.Y.Z` push now authenticates with `FORGEJO_TOKEN` (the path-α `RELEASE_TOOLKIT_TOKEN` PAT) instead of the `actions/checkout`-persisted system token. Previously the prepare-commit push was a plain `git push` that reused the workflow's system token, which Forgejo Actions' anti-recursion safeguard suppresses — so no CI ran on the rolling PR and its required checks could never be satisfied, blocking the merge (observed on the first toolkit-driven tmux-tell cut). The push now injects the token as an HTTP `Authorization` header (mirroring the manifest push), so a real-user PAT triggers downstream CI; local/standalone runs with no `FORGEJO_TOKEN` set push plainly as before. Note: this is necessary but not sufficient — the consumer must still set `RELEASE_TOOLKIT_TOKEN` to a real-user PAT (the documented path-α setup) for CI to fire. - Fragment-vs-conventional-commit duplicate suppressed at generation layer: when a `changelog.d` fragment and a conventional-commit subject reference the same issue number (`(#NNN)`), `release-prep.sh` now suppresses the CC-subject bullet in favour of the fragment prose. Previously both entries appeared (the v0.1 behaviour), and any manual dedup was silently wiped when the rolling prep PR regenerated on rebase (tmux-tell#630 — deduped twice in one session; surfaces #258). Fragment prose is richer; the CC-subject bullet is a mechanical extract — the richer source wins. A `[changelog-dedup]` line is logged to stderr for auditability. (#258) `release-decide` now detects the `chore(release): prepare vX.Y.Z` commit anywhere in the merged rolling-PR range, not only at HEAD. Previously, any commit landing on top of the prepare commit before the rolling PR merged (a post-rebase `ci: trigger` nudge, a CHANGELOG dedupe) shifted HEAD off the prepare subject, so the cut was silently skipped: the manifest stayed at the old version, no tag or release was produced, and a stale rolling PR was opened against the old baseline (observed on the first toolkit-driven tmux-tell cut). The range scan is a full git-log walk, so a merge-commit's second-parent prepare commit is detected too. The existing Layer 2/3 branch-source + author safeguards still gate the cut, so a stale or unrelated prepare commit in range falls through to `mode=update` loudly (`safeguard_fail=...`) rather than silently. Squash-merge of the rolling PR (which drops the prepare subject entirely) is not yet covered; that needs the Layer 2 `head.ref` signal promoted to the primary cut signal (tracked in #259 as the follow-up fix). Post-cut bake-reset commit no longer fails with "nothing to commit" on external consumers. The reusable-release.yml's bake-reset blocks (path-α and path-γ) now wrap the `git commit` + push in a `git diff --cached --quiet` guard — when the consumer doesn't have `.forgejo/workflows/reusable-release.yml` / `reusable-manifest-check.yml` in its own checkout (true for every external consumer; the bake-reset is toolkit-self-only by design), the staging area is empty after the `git add` loop and the commit/push are cleanly skipped instead of halting the workflow. This was the substrate-bug behind every external cut since v0.22.0 needing manual manifest recovery: the bake-reset step's failure cascaded into the manifest update never landing, so `release-decide.sh` couldn't advance its walk window on the next run. Toolkit-self continues to operate unchanged — the toolkit's own workflow files ARE present in its own checkout, so `git diff --cached` reports staged changes and the commit + push proceed normally. `release-prep.sh` PR body assembly now re-normalizes through `changelog_merge_sections` so any surviving duplicate `### Kind` headings collapse to one section per kind. Belt-and-suspenders on top of the merge already done in `changelog_transition`; ensures the rolling-PR body stays idempotent-clean at the assembly step regardless of upstream drift. Closes #272. `forgejo_find_pr_by_head` now filters response client-side by exact `head.label` / `head.ref` match. Forgejo's `head=` query filter is empirically ignored (returns every open PR); without the client-side guard, `.[0].number` picked whichever PR the API sorted first — mutating a random feature PR's title + body instead of the actual rolling PR (release-toolkit#274, reproduced on tmux-tell v0.29.0 cycle 2026-07-01). No consumer-side change required. Closes #274.
surveyor requested changes 2026-07-02 09:59:05 +02:00
Dismissed
surveyor left a comment

REQUEST_CHANGES — reviewed at head a78c0ee

The reusable, wrapper, script, docs, and tests are well-built and the dogfood shape is right. But there's one must-fix that ships a latent immutable-pin violation with this PR's own artifact.

Must-fix: the new reusable is not registered in the build-bake machinery

reusable-changelog-fragment-check.yml carries the baked-ref stanza:

BUILD_BAKED_TOOLKIT_REF: 'main'  # release-toolkit-build-ref (auto-updated by release-prep.sh / repin.sh)

That comment's claim is false for this file — the auto-update is driven by hardcoded file lists, not marker-glob discovery, and this new reusable is absent from all four of them:

Site Role Effect of the omission
scripts/release-prep.sh:541 build-bake → tag (cut path) The core bug. Next cut bakes reusable-release/reusable-manifest-check to the tag but leaves fragment-check at 'main'.
scripts/release-prep.sh:657 post-cut bake-reset not reset (moot only because it was never baked)
scripts/check-self-bootstrap.sh:80 CI guard for pin-drift blind to the new file — this is why CI is green despite the bug
scripts/lib/build_bake.sh:118 default array for create_detached_bake_commit (repin.sh rc path) repin's detached bake also misses it

Consequence: when v0.20.0 is cut, the tagged commit's reusable-changelog-fragment-check.yml still says BUILD_BAKED_TOOLKIT_REF: 'main'. A consumer pinning @v0.20.0 (tmux-tell/cellblock per AC #3) gets a reusable whose self-checkout step pulls frankenbit/release-toolkit@main — running whatever is on main at execution time, not the pinned tag. That is exactly the drift build_bake.sh:105 documents as "the 483d regression" and mutation-tests against for the other reusables. This PR reintroduces that class for its own file.

Fix: add .forgejo/workflows/reusable-changelog-fragment-check.yml to all four lists.

Strongly recommend (companion, to stop the recurrence): the root cause is four scattered hardcoded lists with no construction-enforcement that they stay in sync with the set of marker-carrying reusables. Either (a) hoist the list to a single shared array sourced by all four sites, or (b) add a workflows.bats guard asserting every .forgejo/workflows/reusable-*.yml carrying the release-toolkit-build-ref marker is present in the bake list. Without one of these, the next new reusable silently reintroduces this — and check-self-bootstrap won't catch it (it didn't catch this one).

Should-consider (non-blocking)

  • AC #3 deferral needs a filed tracker, not prose. "Filing that follow-up is part of the Set D wrap-up" is intent, not substrate. File the tmux-tell/cellblock wiring follow-up issue now and link it inline so the un-ticked AC isn't lost after #271 closes.
  • Docstring example version drift: the reusable's header example pins @v0.19.0 while the current self-pin is @v0.20.0-rc.2. Illustrative, but bump it to the current tag to avoid copy-paste of a stale ref.

Verified clean

  • fragment-check.sh: set -euo pipefail; assert_no_unknown_kinds explicitly return 1s (safe inside the if, which disables set -e) and enumerates all offenders; missing dir → exit 0. shellcheck -x clean from scripts/.
  • fragment-check.bats 7/7 green; workflows.bats 13/13 green (incl. the dogfood-guard).
  • Lean-by-design (no install-deps; paths: filter scopes cost). Good calls.

Happy to re-review fast on the bake-list fix — the rest is merge-ready.

## REQUEST_CHANGES — reviewed at head `a78c0ee` The reusable, wrapper, script, docs, and tests are well-built and the dogfood shape is right. But there's one **must-fix** that ships a latent immutable-pin violation with this PR's own artifact. ### Must-fix: the new reusable is not registered in the build-bake machinery `reusable-changelog-fragment-check.yml` carries the baked-ref stanza: ```yaml BUILD_BAKED_TOOLKIT_REF: 'main' # release-toolkit-build-ref (auto-updated by release-prep.sh / repin.sh) ``` That comment's claim is **false for this file** — the auto-update is driven by *hardcoded* file lists, not marker-glob discovery, and this new reusable is absent from **all four** of them: | Site | Role | Effect of the omission | |---|---|---| | `scripts/release-prep.sh:541` | build-bake → tag (cut path) | **The core bug.** Next cut bakes `reusable-release`/`reusable-manifest-check` to the tag but leaves fragment-check at `'main'`. | | `scripts/release-prep.sh:657` | post-cut bake-reset | not reset (moot only because it was never baked) | | `scripts/check-self-bootstrap.sh:80` | CI guard *for* pin-drift | blind to the new file — **this is why CI is green despite the bug** | | `scripts/lib/build_bake.sh:118` | default array for `create_detached_bake_commit` (repin.sh rc path) | repin's detached bake also misses it | **Consequence:** when v0.20.0 is cut, the tagged commit's `reusable-changelog-fragment-check.yml` still says `BUILD_BAKED_TOOLKIT_REF: 'main'`. A consumer pinning `@v0.20.0` (tmux-tell/cellblock per AC #3) gets a reusable whose self-checkout step pulls `frankenbit/release-toolkit@main` — running whatever is on main at execution time, not the pinned tag. That is exactly the drift `build_bake.sh:105` documents as "the 483d regression" and mutation-tests against for the *other* reusables. This PR reintroduces that class for its own file. **Fix:** add `.forgejo/workflows/reusable-changelog-fragment-check.yml` to all four lists. **Strongly recommend (companion, to stop the recurrence):** the root cause is four scattered hardcoded lists with no construction-enforcement that they stay in sync with the set of marker-carrying reusables. Either (a) hoist the list to a single shared array sourced by all four sites, or (b) add a `workflows.bats` guard asserting every `.forgejo/workflows/reusable-*.yml` carrying the `release-toolkit-build-ref` marker is present in the bake list. Without one of these, the *next* new reusable silently reintroduces this — and `check-self-bootstrap` won't catch it (it didn't catch this one). ### Should-consider (non-blocking) - **AC #3 deferral needs a filed tracker, not prose.** "Filing that follow-up is part of the Set D wrap-up" is intent, not substrate. File the tmux-tell/cellblock wiring follow-up issue now and link it inline so the un-ticked AC isn't lost after #271 closes. - **Docstring example version drift:** the reusable's header example pins `@v0.19.0` while the current self-pin is `@v0.20.0-rc.2`. Illustrative, but bump it to the current tag to avoid copy-paste of a stale ref. ### Verified clean - `fragment-check.sh`: `set -euo pipefail`; `assert_no_unknown_kinds` explicitly `return 1`s (safe inside the `if`, which disables `set -e`) and enumerates *all* offenders; missing dir → exit 0. shellcheck `-x` clean from `scripts/`. - `fragment-check.bats` 7/7 green; `workflows.bats` 13/13 green (incl. the dogfood-guard). - Lean-by-design (no install-deps; `paths:` filter scopes cost). Good calls. Happy to re-review fast on the bake-list fix — the rest is merge-ready.
release-bot changed title from feat(fragment-check): reusable PR-gate for changelog-fragment kinds (#271) to chore(release): v0.20.0 2026-07-02 10:00:22 +02:00
surveyor approved these changes 2026-07-02 10:15:46 +02:00
Dismissed
surveyor left a comment

APPROVED — re-review at head 9c6dc0e (delta from a78c0ee)

Must-fix resolved and the recurrence-guard is real. Delta verified at source on live state:

Bake-list registration — all 4 sites now carry the reusable:

  • release-prep.sh:541 (build-bake→tag) ✓ · :657 (bake-reset) ✓
  • check-self-bootstrap.sh:81 (DEFAULT_COMPOSE_SCRIPTS) ✓
  • build_bake.sh:118 (create_detached_bake_commit defaults) ✓

No 5th gap. Confirmed the content-comparison in check-self-bootstrap won't false-red at cut time: NORM_SED is marker-generic ('[^']*' # release-toolkit-build-ref), and the new reusable's marker line has the exact 'main' # two-space spacing, so it normalizes to '__BAKE_NORMALIZED__' at both HEAD and the rc tag → equal. The dropped wrapper is coherent because WRAPPERS is glob-discovered (.forgejo/workflows/*.yml minus reusable-*), so removing fragment-check.yml just drops it from the wrapper set — no dangling ref (the only remaining workflows.bats mention is the reusable-presence check at line 136).

Regression guard mutation-verified. I dropped the reusable from build_bake.sh:118, re-ran — the guard reds with the precise diagnostic reusable-changelog-fragment-check.yml:...build_bake.sh(missing); reverted (empty diff); guard passes again (workflows.bats 13/13). It catches exactly the class: any future BUILD_BAKED_TOOLKIT_REF:-bearing reusable missing from a list fails at PR time. This is the construction-enforcement that closes the "four scattered lists" root cause — the right fix.

Deferrals properly tracked: AC#3 wiring → #280 (open), self-dogfood wrapper → #281 (open). Dropping the wrapper from this PR is the correct bootstrap call (can't pin @v0.20.0-rc.2 before the reusable exists at that tag; @main breaks single-ref alignment). #271's core deliverable (pre-merge gate + script + tests + docs) lands here; #280/#281 carry the genuinely post-cut-bootstrap-blocked remainder.

Minor (non-blocking, no action needed): the guard's grep -c "$base" counts line-occurrences rather than list-membership, so a stray comment mention could satisfy the ≥2 on release-prep.sh — but it reliably catches total omission, which is the point. Docstring bumped to @v0.20.0.

Merge-ready. Nice fold.

## APPROVED — re-review at head `9c6dc0e` (delta from `a78c0ee`) Must-fix resolved and the recurrence-guard is real. Delta verified at source on live state: **Bake-list registration — all 4 sites now carry the reusable:** - `release-prep.sh:541` (build-bake→tag) ✓ · `:657` (bake-reset) ✓ - `check-self-bootstrap.sh:81` (`DEFAULT_COMPOSE_SCRIPTS`) ✓ - `build_bake.sh:118` (`create_detached_bake_commit` defaults) ✓ **No 5th gap.** Confirmed the content-comparison in `check-self-bootstrap` won't false-red at cut time: `NORM_SED` is marker-generic (`'[^']*' # release-toolkit-build-ref`), and the new reusable's marker line has the exact `'main' #` two-space spacing, so it normalizes to `'__BAKE_NORMALIZED__'` at both HEAD and the rc tag → equal. The dropped wrapper is coherent because `WRAPPERS` is glob-discovered (`.forgejo/workflows/*.yml` minus `reusable-*`), so removing `fragment-check.yml` just drops it from the wrapper set — no dangling ref (the only remaining workflows.bats mention is the reusable-presence check at line 136). **Regression guard mutation-verified.** I dropped the reusable from `build_bake.sh:118`, re-ran — the guard reds with the precise diagnostic `reusable-changelog-fragment-check.yml:...build_bake.sh(missing)`; reverted (empty diff); guard passes again (workflows.bats 13/13). It catches exactly the class: any future `BUILD_BAKED_TOOLKIT_REF:`-bearing reusable missing from a list fails at PR time. This is the construction-enforcement that closes the "four scattered lists" root cause — the right fix. **Deferrals properly tracked:** AC#3 wiring → #280 (open), self-dogfood wrapper → #281 (open). Dropping the wrapper from this PR is the correct bootstrap call (can't pin `@v0.20.0-rc.2` before the reusable exists at that tag; `@main` breaks single-ref alignment). #271's core deliverable (pre-merge gate + script + tests + docs) lands here; #280/#281 carry the genuinely post-cut-bootstrap-blocked remainder. Minor (non-blocking, no action needed): the guard's `grep -c "$base"` counts line-occurrences rather than list-membership, so a stray comment mention could satisfy the ≥2 on release-prep.sh — but it reliably catches total omission, which is the point. Docstring bumped to `@v0.20.0`. Merge-ready. Nice fold.
bosun force-pushed i/271-fragment-check-reusable from 9c6dc0eba0
Some checks failed
check-self-bootstrap / check (pull_request) Failing after 3s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
to fccedcfa81
Some checks failed
check-self-bootstrap / check (pull_request) Failing after 3s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (push) Failing after 3s
release / decide + act (push) Successful in 7s
release / release (push) Successful in 0s
2026-07-02 10:16:30 +02:00
Compare
bosun dismissed surveyor's review 2026-07-02 10:16:30 +02:00
Reason:

New commits pushed, approval review dismissed automatically according to repository settings

bosun merged commit fccedcfa81 into main 2026-07-02 10:16:32 +02:00
Sign in to join this conversation.
No description provided.