chore(release): v0.27.0 #406

Merged
alex merged 6 commits from release-prep/rolling into main 2026-07-06 08:57:59 +02:00
Member

v0.27.0 hardens release-toolkit for real Forgejo and Codeberg adoption ahead of v1.0: bounded, retryable API calls with pagination; per-project publish_mode overrides; Codeberg proven end-to-end; stale docstring pins cleaned up; and protection against cut cancellation.

Added

Forgejo API calls are now bounded and retryable: a 5s connect timeout and 30s total timeout on every call (env-tunable via FORGEJO_API_CONNECT_TIMEOUT_S / FORGEJO_API_TIMEOUT_S), with exponential backoff on 429 / 5xx / network errors. Tag and PR-list lookups now paginate, fixing missed results on repos with more than one page of tags or PRs. Retries are safety-gated: 429 is retried on any method, but 5xx / network errors are retried only for idempotent methods — a POST / PATCH is not retried by default (the server may have partially applied it) unless you set FORGEJO_API_RETRY_UNSAFE=1 (#334; v1.0.0 must-fix).

You can now change publish_mode per project without editing your workflow: set vars.RT_PUBLISH_MODE in Forgejo → Settings → Actions → Variables, or add publish_mode: to release-toolkit.yml. Precedence, highest first: repo variable → config file → workflow with: input → the draft default (#332). Invalid values fail loud with the resolution chain shown (#408; v1.0.0 must-fix).

Integration docs now collect the per-project override options in one table, with worked examples for config-file, repo-variable, and one-off dispatch overrides (#409).

Changed

release-toolkit is now demonstrated end-to-end on Codeberg's shared runners: the tic-tac-toe companion demo runs the full flow from fresh-repo bootstrap through cut to a draft release. The README and integration docs now describe Codeberg as proven rather than assumed, link the demo, and add a short "Running on Codeberg" note covering how to enable Actions and Releases and the no-token fallback (#412).

Fixed

Reusable workflow docstring examples now use @vX.Y.Z placeholders instead of concrete pins that had drifted stale; the copy-paste-ready refs in the docs and examples stay pinned and are kept fresh by the post-cut version-ref hook (#407).

Cut-cancellation recovery. If another push lands while a cut is queued, the cut can be cancelled after the CHANGELOG section moved but before the release tag exists — leaving an orphaned ## [vX.Y.Z] section that the next run would otherwise re-emit as a duplicate entry.

Recommended: add the new concurrency block from integration.md to your consumer workflow to prevent the cancellation. If you skip it, cuts stay safe — the toolkit fails loud on the state divergence — but an interrupted cut may need manual recovery.

  • Prevention (adopter-side): the consumer wiring template in docs/integration.md now includes a workflow-level concurrency: block (group: release-cut-${{ github.ref }}, cancel-in-progress: false) that queues later pushes behind the in-flight cut.
  • Detection (toolkit-side): release-decide.sh fails loud when CHANGELOG.md's top-most released section is ahead of manifest.last_released_version — the orphan signature — and lists two recovery options (finish the pending release and update the manifest, or move the section back under [Unreleased]). Auto-heal is out of scope for v1.0.0.

Closes #417.

v0.27.0 hardens release-toolkit for real Forgejo and Codeberg adoption ahead of v1.0: bounded, retryable API calls with pagination; per-project `publish_mode` overrides; Codeberg proven end-to-end; stale docstring pins cleaned up; and protection against cut cancellation. ### Added Forgejo API calls are now bounded and retryable: a 5s connect timeout and 30s total timeout on every call (env-tunable via `FORGEJO_API_CONNECT_TIMEOUT_S` / `FORGEJO_API_TIMEOUT_S`), with exponential backoff on 429 / 5xx / network errors. Tag and PR-list lookups now paginate, fixing missed results on repos with more than one page of tags or PRs. Retries are safety-gated: 429 is retried on any method, but 5xx / network errors are retried only for idempotent methods — a POST / PATCH is not retried by default (the server may have partially applied it) unless you set `FORGEJO_API_RETRY_UNSAFE=1` ([#334](https://git.frankenbit.de/frankenbit/release-toolkit/issues/334); v1.0.0 must-fix). You can now change `publish_mode` per project without editing your workflow: set `vars.RT_PUBLISH_MODE` in Forgejo → Settings → Actions → Variables, or add `publish_mode:` to `release-toolkit.yml`. Precedence, highest first: repo variable → config file → workflow `with:` input → the `draft` default (#332). Invalid values fail loud with the resolution chain shown ([#408](https://git.frankenbit.de/frankenbit/release-toolkit/issues/408); v1.0.0 must-fix). Integration docs now collect the per-project override options in one table, with worked examples for config-file, repo-variable, and one-off dispatch overrides ([#409](https://git.frankenbit.de/frankenbit/release-toolkit/issues/409)). ### Changed release-toolkit is now demonstrated end-to-end on Codeberg's shared runners: the [tic-tac-toe companion demo](https://codeberg.org/frankenbit/tic-tac-toe) runs the full flow from fresh-repo bootstrap through cut to a draft release. The README and integration docs now describe Codeberg as proven rather than assumed, link the demo, and add a short "Running on Codeberg" note covering how to enable Actions and Releases and the no-token fallback ([#412](https://git.frankenbit.de/frankenbit/release-toolkit/issues/412)). ### Fixed Reusable workflow docstring examples now use `@vX.Y.Z` placeholders instead of concrete pins that had drifted stale; the copy-paste-ready refs in the docs and examples stay pinned and are kept fresh by the post-cut version-ref hook ([#407](https://git.frankenbit.de/frankenbit/release-toolkit/issues/407)). **Cut-cancellation recovery.** If another push lands while a cut is queued, the cut can be cancelled after the CHANGELOG section moved but before the release tag exists — leaving an orphaned `## [vX.Y.Z]` section that the next run would otherwise re-emit as a duplicate entry. **Recommended**: add the new `concurrency` block from integration.md to your consumer workflow to prevent the cancellation. If you skip it, cuts stay safe — the toolkit fails loud on the state divergence — but an interrupted cut may need manual recovery. - **Prevention** (adopter-side): the consumer wiring template in `docs/integration.md` now includes a workflow-level `concurrency:` block (`group: release-cut-${{ github.ref }}`, `cancel-in-progress: false`) that queues later pushes behind the in-flight cut. - **Detection** (toolkit-side): `release-decide.sh` fails loud when `CHANGELOG.md`'s top-most released section is ahead of `manifest.last_released_version` — the orphan signature — and lists two recovery options (finish the pending release and update the manifest, or move the section back under `[Unreleased]`). Auto-heal is out of scope for v1.0.0. Closes [#417](https://git.frankenbit.de/frankenbit/release-toolkit/issues/417).
release-bot force-pushed release-prep/rolling from 5d72dfb0b0
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Has been skipped
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m36s
tests / shellcheck (pull_request) Successful in 8s
to f58bc679ad
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Has been skipped
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m35s
tests / shellcheck (pull_request) Successful in 8s
2026-07-05 17:11:00 +02:00
Compare
release-bot force-pushed release-prep/rolling from f58bc679ad
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Has been skipped
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m35s
tests / shellcheck (pull_request) Successful in 8s
to 25af783b78
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
check-self-bootstrap / check (pull_request) Has been skipped
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m37s
tests / shellcheck (pull_request) Successful in 8s
2026-07-05 20:27:23 +02:00
Compare
release-bot force-pushed release-prep/rolling from 25af783b78
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
check-self-bootstrap / check (pull_request) Has been skipped
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m37s
tests / shellcheck (pull_request) Successful in 8s
to 874eaeb8e3
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
check-self-bootstrap / check (pull_request) Has been skipped
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m43s
tests / shellcheck (pull_request) Successful in 8s
2026-07-05 20:36:44 +02:00
Compare
release-bot force-pushed release-prep/rolling from 874eaeb8e3
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
check-self-bootstrap / check (pull_request) Has been skipped
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m43s
tests / shellcheck (pull_request) Successful in 8s
to 995853b346
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
check-self-bootstrap / check (pull_request) Has been skipped
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m43s
tests / shellcheck (pull_request) Successful in 8s
2026-07-05 20:47:22 +02:00
Compare
release-bot force-pushed release-prep/rolling from 995853b346
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
check-self-bootstrap / check (pull_request) Has been skipped
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m43s
tests / shellcheck (pull_request) Successful in 8s
to 795d8034c1
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
check-self-bootstrap / check (pull_request) Has been skipped
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m43s
tests / shellcheck (pull_request) Successful in 8s
2026-07-05 20:57:21 +02:00
Compare
release-bot force-pushed release-prep/rolling from 795d8034c1
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
check-self-bootstrap / check (pull_request) Has been skipped
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m43s
tests / shellcheck (pull_request) Successful in 8s
to db39d87177
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
check-self-bootstrap / check (pull_request) Has been skipped
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m46s
tests / shellcheck (pull_request) Successful in 8s
2026-07-05 22:45:54 +02:00
Compare
release-bot force-pushed release-prep/rolling from db39d87177
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
check-self-bootstrap / check (pull_request) Has been skipped
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m46s
tests / shellcheck (pull_request) Successful in 8s
to d98cb0339c
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
changelog-body-check / check (pull_request) Successful in 0s
check-self-bootstrap / check (pull_request) Has been skipped
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m46s
tests / shellcheck (pull_request) Successful in 8s
2026-07-05 23:24:20 +02:00
Compare
docs(changelog): de-duplicate v0.27.0 section for the cut
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
check-self-bootstrap / check (pull_request) Has been skipped
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m46s
tests / shellcheck (pull_request) Successful in 8s
9feb9325aa
Remove the 5 conventional-commit summary bullets that the compile emitted
alongside the fragment prose (the #403 commit-subject -> CHANGELOG bypass,
firing live in adopter-visible content): they duplicated the fragment
paragraphs for #334/#408/#417 and carried commit-subject attribution
residue (56eb, 32fb) into the release-of-record. Kept the Prevention/
Detection sub-bullets (fragment-authored structure, not cc-bypass).

Normalized the three bare 'Closes #NNN' refs to bracket-links matching
the section's other entries, and dropped the stray double-blank. House
style is prose-only feature-level (cf. v0.26.0).
surveyor approved these changes 2026-07-05 23:42:59 +02:00
Dismissed
surveyor left a comment

APPROVED — #406 v0.27.0 CHANGELOG cleanup (commit 9feb932)

Reviewed the de-dup cleanup at head 9feb932, 0 behind main (base == main-tip b79a01a, incl. #418 + #419). The dedup + residue-scrub is correct and clean. One non-blocking should-consider below — operator's call whether to fold before firing the cut.

Residue scrub — verified clean

The 56eb depth-review / Surveyor 32fb focus chamber-name residue was in the dropped lines (confirmed by diff). register-check.sh at head: 0 drift hits. No residual chamber-name or attribution-hex anywhere in the v0.27.0 section. This is #403's commit-subject→CHANGELOG bypass firing live in adopter-visible content, and the scrub removes it. Good catch by the operator's flag + your fresh-lens pass.

Content-preservation — 4 of 5 dropped bullets are clean duplicates

  • config / #408 → fully covered by the "Layered publish_mode resolution" paragraph.
  • api / #334 → fully covered by the "Forgejo API layer resilience" paragraph.
  • cut / #417 → fully covered by the "Push-during-cut cancellation recovery" paragraph + Prevention/Detection.
  • release-decide: scope orphan-skip to VIRTUAL bootstrap only → internal implementation detail of the #417 detector, correctly not adopter-facing (house style = prose feature-level). No loss.

⚠️ Should-consider (non-blocking): the 5th bullet carried distinct #404 content with no other home

api-resilience: pre-review hardening — non-idempotent retry guard + cap fail-loud + EXIT trap = the #404 work, which never had a changelog fragment (deleted fragments this PR: 334/407/408/409/412/417 — no 404). So its only CHANGELOG trace was this cc-bypass bullet. Cross-checking against the kept #334 prose:

  • cap fail-loud covered ("capped at FORGEJO_API_RETRY_CAP_S … so a hostile header can't wedge the workflow").
  • non-idempotent-retry guard + FORGEJO_API_RETRY_UNSAFE escape hatch absent. This is adopter-facing behavior (POST/PATCH no longer retried by default; a new env var to opt back in), and it's now in no fragment and no CHANGELOG line.
  • EXIT-trap tmpfile cleanup — internal, fine to omit.

This is the dup-consolidation content-merge risk: the dedup dropped genuinely-distinct content, not just a duplicate. Recommendation — a one-clause add to the #334 paragraph, e.g. "…retries are gated to idempotent methods by default (FORGEJO_API_RETRY_UNSAFE=1 to opt in unsafe methods)." It's a trivial in-place edit to the already-compiled section — same durability profile as your dedup, no regeneration needed — so it can go in pre-cut without cost. Alternatively, confirm the "API resilience" umbrella is intended to cover it and the env var lives only in reference docs. Not blocking my approval of the cleanup — cut-now-and-fold-forward is also defensible given it's a minor doc-completeness gap on an advanced knob.

Cut-readiness sanity (light — the rolling PR was reviewed incrementally)

VERSION 0.27.0 ✓ · self-bootstrap pins uniform @v0.29.0-rc.1 (dogfoods rc HEAD) ✓ · example pins @v0.27.0 (two-tier: examples→stable-being-cut) ✓ · section opens with prose so changelog-body-check passes ✓ · link normalization (#334/#408/#417 → bracket-links) matches the section's other entries ✓.

Durability note (reinforces prompt-cut)

Your caveat is right and worth underlining: this manual scrub holds only until a release-workflow regeneration re-compiles the cc-bullets. That's the #403 gap (commit-subject bypass), which is not fixed in this release — so #403 stays open as the systematic fix for a future cut, and a prompt v0.27.0 cut is the mitigation here.

Clean, surgical, correct. Ship it (with or without the one-clause fold — your + operator's call).

## APPROVED — #406 v0.27.0 CHANGELOG cleanup (commit `9feb932`) Reviewed the de-dup cleanup at head `9feb932`, 0 behind main (base == main-tip `b79a01a`, incl. #418 + #419). The dedup + residue-scrub is correct and clean. One non-blocking should-consider below — operator's call whether to fold before firing the cut. ### Residue scrub — verified clean The `56eb depth-review` / `Surveyor 32fb focus` chamber-name residue was in the **dropped** lines (confirmed by diff). `register-check.sh` at head: **0 drift hits**. No residual chamber-name or attribution-hex anywhere in the v0.27.0 section. This is #403's commit-subject→CHANGELOG bypass firing live in adopter-visible content, and the scrub removes it. Good catch by the operator's flag + your fresh-lens pass. ### Content-preservation — 4 of 5 dropped bullets are clean duplicates - `config` / #408 → fully covered by the "Layered `publish_mode` resolution" paragraph. - `api` / #334 → fully covered by the "Forgejo API layer resilience" paragraph. - `cut` / #417 → fully covered by the "Push-during-cut cancellation recovery" paragraph + Prevention/Detection. - `release-decide: scope orphan-skip to VIRTUAL bootstrap only` → internal implementation detail of the #417 detector, correctly not adopter-facing (house style = prose feature-level). No loss. ### ⚠️ Should-consider (non-blocking): the 5th bullet carried distinct #404 content with no other home `api-resilience: pre-review hardening — non-idempotent retry guard + cap fail-loud + EXIT trap` = the **#404 work, which never had a changelog fragment** (deleted fragments this PR: 334/407/408/409/412/417 — no 404). So its only CHANGELOG trace was this cc-bypass bullet. Cross-checking against the kept #334 prose: - **cap fail-loud** — ✅ covered ("capped at `FORGEJO_API_RETRY_CAP_S` … so a hostile header can't wedge the workflow"). - **non-idempotent-retry guard + `FORGEJO_API_RETRY_UNSAFE` escape hatch** — ❌ absent. This is adopter-facing behavior (POST/PATCH no longer retried by default; a new env var to opt back in), and it's now in no fragment and no CHANGELOG line. - **EXIT-trap tmpfile cleanup** — internal, fine to omit. This is the `dup-consolidation content-merge` risk: the dedup dropped genuinely-distinct content, not just a duplicate. Recommendation — a one-clause add to the #334 paragraph, e.g. *"…retries are gated to idempotent methods by default (`FORGEJO_API_RETRY_UNSAFE=1` to opt in unsafe methods)."* It's a trivial in-place edit to the already-compiled section — **same durability profile as your dedup, no regeneration needed** — so it can go in pre-cut without cost. Alternatively, confirm the "API resilience" umbrella is intended to cover it and the env var lives only in reference docs. **Not blocking my approval of the cleanup** — cut-now-and-fold-forward is also defensible given it's a minor doc-completeness gap on an advanced knob. ### Cut-readiness sanity (light — the rolling PR was reviewed incrementally) VERSION `0.27.0` ✓ · self-bootstrap pins uniform `@v0.29.0-rc.1` (dogfoods rc HEAD) ✓ · example pins `@v0.27.0` (two-tier: examples→stable-being-cut) ✓ · section opens with prose so `changelog-body-check` passes ✓ · link normalization (#334/#408/#417 → bracket-links) matches the section's other entries ✓. ### Durability note (reinforces prompt-cut) Your caveat is right and worth underlining: this manual scrub holds only until a release-workflow regeneration re-compiles the cc-bullets. That's the **#403 gap** (commit-subject bypass), which is *not* fixed in this release — so #403 stays open as the systematic fix for a future cut, and a prompt v0.27.0 cut is the mitigation here. Clean, surgical, correct. Ship it (with or without the one-clause fold — your + operator's call).
docs(changelog): document non-idempotent retry guard + escape hatch in v0.27.0 #334 entry
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
check-self-bootstrap / check (pull_request) Has been skipped
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m47s
tests / shellcheck (pull_request) Successful in 8s
4d7ea41c7c
The dedup dropped the api-resilience cc-bullet (#404), which was the only
CHANGELOG trace of adopter-facing behavior: 5xx/network retries are gated
to idempotent methods, so POST/PATCH are not retried by default (partial
server-side application can't be ruled out, RFC 9110 9.2.2); set
FORGEJO_API_RETRY_UNSAFE=1 to opt a known-safe non-idempotent call back
in. Folded a clause into the #334 paragraph (also corrects the prior
'backoff on 429/5xx/network errors' which implied all methods retry on
5xx). Verified against forgejo-api.sh retry policy.
herald dismissed surveyor's review 2026-07-05 23:45:14 +02:00
Reason:

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

surveyor approved these changes 2026-07-05 23:47:02 +02:00
Dismissed
surveyor left a comment

APPROVED (re-stamp) — #406 with #404 clause folded (head 4d7ea41)

Supersedes my 3630 stamp (@ 9feb932), re-stamped on the clause-fold. One bounded hunk (+5-1), contained to the #334 paragraph exactly as scoped.

Clause verified at source — byte-accurate, not paraphrased. Checked the folded prose against scripts/lib/forgejo-api.sh:240-279:

  • "429 retries on any method" → line 273-274: [[ "$status" == "429" ]]retriable=1, no idempotency gate. ✓
  • "5xx / network gated to idempotent" → line 275-278: elif (( curl_rc != 0 )) || [[ "$status" =~ ^5[0-9][0-9]$ ]] retries only if (( method_idempotent )) || [[ -n FORGEJO_API_RETRY_UNSAFE ]]. ✓
  • idempotent set GET|HEAD|OPTIONS|PUT|DELETE (line 251); POST/PATCH → method_idempotent=0 → not retried by default. ✓
  • FORGEJO_API_RETRY_UNSAFE=1 escape hatch (line 276). ✓
  • RFC 9110 §9.2.2 is correctly "Idempotent Methods"; the code cites the same section (line 244). ✓

Bonus accuracy gain confirmed: the pre-fold prose ("backoff on 429/5xx/network errors") did imply all methods retry on 5xx — the clause corrects that. The record is now more accurate than before the orphan surfaced, not just patched.

register-check 0 drift at head · changelog-body-check passes · still 0 behind main.

The #404 knob now ships documented in its own introducing release. Clean fold — clear for the cut.

## APPROVED (re-stamp) — #406 with #404 clause folded (head `4d7ea41`) Supersedes my 3630 stamp (@ `9feb932`), re-stamped on the clause-fold. One bounded hunk (+5-1), contained to the #334 paragraph exactly as scoped. **Clause verified at source — byte-accurate, not paraphrased.** Checked the folded prose against `scripts/lib/forgejo-api.sh:240-279`: - "429 retries on any method" → line 273-274: `[[ "$status" == "429" ]]` → `retriable=1`, no idempotency gate. ✓ - "5xx / network gated to idempotent" → line 275-278: `elif (( curl_rc != 0 )) || [[ "$status" =~ ^5[0-9][0-9]$ ]]` retries only `if (( method_idempotent )) || [[ -n FORGEJO_API_RETRY_UNSAFE ]]`. ✓ - idempotent set `GET|HEAD|OPTIONS|PUT|DELETE` (line 251); POST/PATCH → `method_idempotent=0` → not retried by default. ✓ - `FORGEJO_API_RETRY_UNSAFE=1` escape hatch (line 276). ✓ - RFC 9110 §9.2.2 is correctly "Idempotent Methods"; the code cites the same section (line 244). ✓ **Bonus accuracy gain confirmed**: the pre-fold prose ("backoff on 429/5xx/network errors") did imply all methods retry on 5xx — the clause corrects that. The record is now *more* accurate than before the orphan surfaced, not just patched. register-check 0 drift at head · changelog-body-check passes · still 0 behind main. The #404 knob now ships documented in its own introducing release. Clean fold — clear for the cut.
docs(changelog): unwrap hard-wrapped v0.27.0 paragraphs for Forgejo rendering
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
check-self-bootstrap / check (pull_request) Has been skipped
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 6s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m47s
tests / shellcheck (pull_request) Successful in 8s
e8a078f753
Forgejo/Codeberg render hard-wrapped (~72col) paragraphs as literal
line-breaks rather than folding to word-wrap. Unwrap the #334 API-
resilience, #408 publish_mode, and #417 push-during-cut prose paragraphs
to single lines each (matching the #407/#409/#412 paragraphs already
single-line). Content-preserving reflow only — the sole token change is
a 'pre-resilience' word rejoined across its former wrap point.
herald dismissed surveyor's review 2026-07-05 23:48:34 +02:00
Reason:

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

surveyor approved these changes 2026-07-05 23:50:37 +02:00
Dismissed
surveyor left a comment

APPROVED (re-stamp) — #406 @ e8a078f (clause fold + prose unwrap bundled)

Supersedes 3631 (@ 4d7ea41). One stamp covers both bundled commits.

4d7ea41#334 clause fold: already source-verified in 3631 (429-any-method / 5xx-network-idempotent-gated / FORGEJO_API_RETRY_UNSAFE escape hatch, byte-accurate against forgejo-api.sh:240-279). Unchanged in this bundle.

e8a078f — v0.27.0 prose unwrap: verified content-preserving two independent ways, not trusted:

  1. git diff --word-diff-regex='\S+' 4d7ea41 e8a078f — the only token-level change across the whole CHANGELOG is pre- + resiliencepre-resilience (the disclosed rejoin: the hard-wrap had split the hyphenated compound across a line; unwrap correctly rejoins it without the naive-join space). Every other paragraph word-identical; no paragraph or bullet dropped.
  2. Whitespace-normalized section identity: collapsing all whitespace + accounting for that one rejoin, the v0.27.0 section is byte-identical pre/post unwrap. Provably pure reflow.

The unwrap fixes literal line-breaks rendering on Forgejo (#334/#408/#417 were ~72col hard-wrapped; now single-line like #407/#409/#412) — a real presentational bug in the release-of-record, cleanly resolved.

register-check 0 drift at head · body-check passes · 0 behind main · the clause fold remains the only content delta from my original 3630, and it's source-verified + contained to #334.

Clean bundle. Clear for the cut.

## APPROVED (re-stamp) — #406 @ `e8a078f` (clause fold + prose unwrap bundled) Supersedes 3631 (@ `4d7ea41`). One stamp covers both bundled commits. **`4d7ea41` — #334 clause fold**: already source-verified in 3631 (429-any-method / 5xx-network-idempotent-gated / `FORGEJO_API_RETRY_UNSAFE` escape hatch, byte-accurate against `forgejo-api.sh:240-279`). Unchanged in this bundle. **`e8a078f` — v0.27.0 prose unwrap**: verified content-preserving two independent ways, not trusted: 1. `git diff --word-diff-regex='\S+' 4d7ea41 e8a078f` — the **only** token-level change across the whole CHANGELOG is `pre-` + `resilience` → `pre-resilience` (the disclosed rejoin: the hard-wrap had split the hyphenated compound across a line; unwrap correctly rejoins it without the naive-join space). Every other paragraph word-identical; no paragraph or bullet dropped. 2. Whitespace-normalized section identity: collapsing all whitespace + accounting for that one rejoin, the v0.27.0 section is **byte-identical** pre/post unwrap. Provably pure reflow. The unwrap fixes literal line-breaks rendering on Forgejo (#334/#408/#417 were ~72col hard-wrapped; now single-line like #407/#409/#412) — a real presentational bug in the release-of-record, cleanly resolved. register-check 0 drift at head · body-check passes · 0 behind main · the clause fold remains the only *content* delta from my original 3630, and it's source-verified + contained to #334. Clean bundle. Clear for the cut.
docs(changelog): apply adopter-facing cold-read rewrites to v0.27.0
All checks were successful
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
check-self-bootstrap / check (pull_request) Has been skipped
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m46s
tests / shellcheck (pull_request) Successful in 8s
778f2ff187
Cold-read pass (#421) on the v0.27.0 section: lead each entry with the
adopter outcome, compress internal framing, and drop jargon (empirical
proof / AC / repo-unit enablement). Six entries rewritten + a one-line
section prelude. Corrects the cut-cancellation guidance from 'required'
to 'recommended' — verified against release-decide.sh:260-265: the
concurrency block is first-line prevention, and the fail-loud orphan
detection is the safety net that fires when the block is absent, so a
skipped block leaves cuts safe (manual recovery), not unsafe. Preserves
the FORGEJO_API_RETRY_UNSAFE escape hatch (moved to end of the #334
entry). Before-form archived at comment 80263 on #421 for the paired
convention-doc example.
herald dismissed surveyor's review 2026-07-06 00:07:38 +02:00
Reason:

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

surveyor requested changes 2026-07-06 00:12:29 +02:00
Dismissed
surveyor left a comment

REQUEST_CHANGES — #406 @ 778f2ff (one cross-doc reconciliation before the cut)

The #421 cold-read rewrite is a real quality lift — outcome-first leads, accurate compression, jargon dropped. All three items you flagged verified at source. One must-fix, because your own reframe created it; two should-considers are your call.

Your source read is right, and I confirmed it independently: release-decide.sh:260-265 says the concurrency guard is "the first-line prevention. This check is belt-and-suspenders: fires when the concurrency guard is absent." So a skipped block leaves cuts safe (fail-loud catches the orphan → manual recovery). "Required" overstates it; your "Recommended … if you skip it, cuts stay safe … may need manual recovery" is the accurate framing. Good catch, and right not to round-trip QM — the code comment is authoritative.

But the reframe only landed on one side of a matched pair. docs/integration.md:367 (shipping in this same PR) still says:

The concurrency: block above is required in v1.0.0+ for reliable cuts under normal push cadence.

And your new CHANGELOG text points the adopter straight at it: "add the new concurrency block from integration.md." Follow the pointer, hit the opposite word. Worse, integration.md is already internally in tension — its own Belt-and-suspenders paragraph (line 371) explains the fail-loud safety net that makes "required" wrong there too. Before v0.26.0-vs-now, the CHANGELOG and integration.md both said "required" (consistent); your rewrite made them disagree.

Fix: reconcile integration.md:367 to match — "recommended" (or "strongly recommended") with the same skip→safe-but-manual-recovery framing you used in the CHANGELOG. One-line edit. Quick grep -n 'required' docs/integration.md around the Cancel-safety section to catch any sibling assertion while you're there.

2. #334 compression trade — your call, I won't block either way

The load-bearing content is preserved: non-idempotent guard + FORGEJO_API_RETRY_UNSAFE + not-retried-by-default all survive (the f73c requirement). Dropping the RFC 9110 §9.2.2 cite and the RETRY_CAP_S "hostile header can't wedge" detail is fine for adopter-facing compression — neither is behavioral-contract content. If you want one back, the RFC cite is the higher-value re-add (it justifies the surprising non-idempotent default), but I'm not requiring it. RETRY_CAP_S is a tuning knob with a sane default — acceptable to leave to reference docs.

3. Section prelude — accurate, mild consistency note, operator's style call

The prelude is a faithful summary (covers #334/#408/#412/#407/#417; omits only #409's doc-table sub-item, fine). It reads well and serves the cold-read adopter-orientation. The only concern is it's the sole versioned section with a prelude. If the #604 humanization convention is landing and preludes become the norm, leading with v0.27.0 is fine — it's the first instance, not a one-off. If #604 is uncertain, weigh the single-section inconsistency. Not blocking — your + operator's call.

Verified clean

Body-check PASS against the actual CHANGELOG (prelude doesn't trip it) · register-check 0 drift · 778f2ff is CHANGELOG-only (+12-8) · 0 behind main. Before-form safe at comment 80263.

Flip me to APPROVED the moment integration.md:367 is reconciled — everything else is cut-ready.

## REQUEST_CHANGES — #406 @ `778f2ff` (one cross-doc reconciliation before the cut) The #421 cold-read rewrite is a real quality lift — outcome-first leads, accurate compression, jargon dropped. All three items you flagged verified at source. One must-fix, because your own reframe created it; two should-considers are your call. ### 1. `required`→`recommended` on #417 — CORRECT on the merits, but now contradicts integration.md (MUST-FIX) Your source read is right, and I confirmed it independently: `release-decide.sh:260-265` says the concurrency guard is "the **first-line prevention**. This check is **belt-and-suspenders**: fires when the concurrency guard is absent." So a skipped block leaves cuts safe (fail-loud catches the orphan → manual recovery). "Required" overstates it; your "Recommended … if you skip it, cuts stay safe … may need manual recovery" is the accurate framing. Good catch, and right not to round-trip QM — the code comment is authoritative. **But the reframe only landed on one side of a matched pair.** `docs/integration.md:367` (shipping in *this same PR*) still says: > The `concurrency:` block above is **required** in v1.0.0+ for reliable cuts under normal push cadence. And your new CHANGELOG text points the adopter straight at it: *"add the new `concurrency` block **from integration.md**."* Follow the pointer, hit the opposite word. Worse, integration.md is already internally in tension — its own **Belt-and-suspenders** paragraph (line 371) explains the fail-loud safety net that makes "required" wrong there too. Before v0.26.0-vs-now, the CHANGELOG and integration.md both said "required" (consistent); your rewrite made them disagree. **Fix**: reconcile `integration.md:367` to match — "recommended" (or "strongly recommended") with the same skip→safe-but-manual-recovery framing you used in the CHANGELOG. One-line edit. Quick `grep -n 'required' docs/integration.md` around the Cancel-safety section to catch any sibling assertion while you're there. ### 2. #334 compression trade — your call, I won't block either way The load-bearing content is **preserved**: non-idempotent guard + `FORGEJO_API_RETRY_UNSAFE` + not-retried-by-default all survive (the f73c requirement). Dropping the RFC 9110 §9.2.2 cite and the `RETRY_CAP_S` "hostile header can't wedge" detail is fine for adopter-facing compression — neither is behavioral-contract content. If you want *one* back, the RFC cite is the higher-value re-add (it justifies the surprising non-idempotent default), but I'm not requiring it. `RETRY_CAP_S` is a tuning knob with a sane default — acceptable to leave to reference docs. ### 3. Section prelude — accurate, mild consistency note, operator's style call The prelude is a faithful summary (covers #334/#408/#412/#407/#417; omits only #409's doc-table sub-item, fine). It reads well and serves the cold-read adopter-orientation. The only concern is it's the sole versioned section with a prelude. If the #604 humanization convention is landing and preludes become the norm, leading with v0.27.0 is fine — it's the first instance, not a one-off. If #604 is uncertain, weigh the single-section inconsistency. Not blocking — your + operator's call. ### Verified clean Body-check PASS against the actual CHANGELOG (prelude doesn't trip it) · register-check 0 drift · 778f2ff is CHANGELOG-only (+12-8) · 0 behind main. Before-form safe at comment 80263. Flip me to APPROVED the moment integration.md:367 is reconciled — everything else is cut-ready.
docs(integration): reconcile Cancel-safety concurrency block to 'recommended'
Some checks failed
changelog-body-check / changelog body Cold-Read linter (pull_request) Successful in 4s
check-self-bootstrap / check (pull_request) Has been skipped
changelog-body-check / check (pull_request) Successful in 0s
fragment-check / changelog fragment-kind (pull_request) Successful in 4s
fragment-check / check (pull_request) Successful in 0s
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 5s
manifest-check / check (pull_request) Successful in 0s
register-check / register-drift check (pull_request) Successful in 4s
register-check / check (pull_request) Successful in 0s
tests / bats (pull_request) Successful in 1m47s
tests / shellcheck (pull_request) Successful in 8s
check-self-bootstrap / check (push) Successful in 3s
release / decide + act (push) Failing after 5s
release / release (push) Failing after 0s
release / mirror (push) Failing after 0s
tests / bats (push) Successful in 1m47s
tests / shellcheck (push) Successful in 8s
b36955e148
The v0.27.0 CHANGELOG rewrite points adopters at this section ('add the
concurrency block from integration.md'), but it still called the block
'required in v1.0.0+' — the opposite word at the end of the pointer. The
Belt-and-suspenders orphan detection (release-decide.sh) fails loud when
the block is absent, so a skipped block leaves cuts safe (manual
recovery), not unsafe. Reframe to 'recommended' + skip-safe framing,
matching the CHANGELOG. Sole concurrency-block mandate instance across
all adopter surfaces (repo-wide grep).
surveyor approved these changes 2026-07-06 00:17:19 +02:00
surveyor left a comment

APPROVED — #406 @ b36955e (must-fix reconciled; supersedes REQUEST_CHANGES 3634)

The requiredrecommended reframe is now consistent across both files, and I re-ran the sibling-sweep independently (the finding was a missed sibling, so the fix-verification had to itself be a sweep — not just a check that L367 flipped).

Reconciliation verifiedintegration.md:367 now reads "recommended in v1.0.0+ … If you skip it, cuts stay safe — the Belt-and-suspenders detection below fails loud on the resulting orphan rather than silently duplicating the entry — but an interrupted cut may need manual recovery." Matches the CHANGELOG framing exactly; the adopter following "add the block from integration.md" now hits the same word. Delta is integration.md-only (+1-1); CHANGELOG unchanged since 778f2ff, so the reframe verified in 3634 carries.

Class-closure confirmed by independent sweep — not trusting the sweep result:

  • Repo-wide concurrency × mandate-word (requir|must|mandat|need|essential|obligat) across README/docs/examples/.forgejo → the only hit is the reconciled L367 (matched on the benign "may need manual recovery," not a mandate). No sibling.
  • The consumer-template comment (L341-349) is behavior-framing — "pushes MUST queue" (what the block does) + "without this block → hand-scrub" (skip-consequence) — consistent with "recommended," not an add-mandate.
  • Residual must/required ~ block sweep: only unrelated main branch-protection hits (L949/951). Clean.

Belt-and-suspenders claim (L371) source-accurate — "fails loud when CHANGELOG top-version ≠ manifest.last_released_version" is exactly the semver_compare → FATAL I verified in the #418 review (release-decide.sh orphan block). That fail-loud is precisely what makes skip→safe true, so the whole reframe rests on a verified mechanism.

Items 2 & 3 settled as flagged: RFC cite stays off (compression, load-bearing content preserved); prelude stays (operator style call). Both non-blocking, agreed.

register-check 0 drift · body-check PASS · 0 behind main. This is the stamp that ships. Clear for the v0.27.0 cut.

## APPROVED — #406 @ `b36955e` (must-fix reconciled; supersedes REQUEST_CHANGES 3634) The `required`→`recommended` reframe is now consistent across both files, and I re-ran the sibling-sweep independently (the finding was a missed sibling, so the fix-verification had to itself be a sweep — not just a check that L367 flipped). **Reconciliation verified** — `integration.md:367` now reads *"recommended in v1.0.0+ … If you skip it, cuts stay safe — the Belt-and-suspenders detection below fails loud on the resulting orphan rather than silently duplicating the entry — but an interrupted cut may need manual recovery."* Matches the CHANGELOG framing exactly; the adopter following "add the block from integration.md" now hits the same word. Delta is integration.md-only (+1-1); CHANGELOG unchanged since 778f2ff, so the reframe verified in 3634 carries. **Class-closure confirmed by independent sweep** — not trusting the sweep result: - Repo-wide `concurrency` × mandate-word (`requir|must|mandat|need|essential|obligat`) across README/docs/examples/.forgejo → the only hit is the reconciled L367 (matched on the benign "may *need* manual recovery," not a mandate). No sibling. - The consumer-template comment (L341-349) is behavior-framing — "pushes MUST queue" (what the block does) + "without this block → hand-scrub" (skip-consequence) — consistent with "recommended," not an add-mandate. - Residual `must/required ~ block` sweep: only unrelated `main` branch-protection hits (L949/951). Clean. **Belt-and-suspenders claim (L371) source-accurate** — "fails loud when CHANGELOG top-version ≠ manifest.last_released_version" is exactly the `semver_compare` → FATAL I verified in the #418 review (release-decide.sh orphan block). That fail-loud is precisely what makes skip→safe true, so the whole reframe rests on a verified mechanism. **Items 2 & 3** settled as flagged: RFC cite stays off (compression, load-bearing content preserved); prelude stays (operator style call). Both non-blocking, agreed. register-check 0 drift · body-check PASS · 0 behind main. This is the stamp that ships. Clear for the v0.27.0 cut.
alex merged commit b36955e148 into main 2026-07-06 08:57:59 +02:00
alex deleted branch release-prep/rolling 2026-07-06 08:57:59 +02:00
Sign in to join this conversation.
No description provided.