v0.4.0 architectural arc: release-please-shaped drifting-PR + push-trigger mechanic #52

Closed
opened 2026-06-25 01:10:00 +02:00 by quartermaster · 0 comments

Goal

Reinstate the auto-fire / single-button release flow without exposing the Forgejo expression-engine bug (#41/#47). Adopt the operator's preferred shape (drifting/rolling release PR + single release action that triggers on merge), modeled on Google's release-please.

Why now

#41 closed in v0.3.4 (PR #51) by removing the broken pull_request.closed auto-fire entirely — release-draft.yml is now operator-dispatch-only via workflow_dispatch. That's the substrate-honest stopgap; the architectural redesign is the real fix.

Substrate constraint that shapes the design

Forgejo's Actions expression engine cannot reliably resolve github.* / forgejo.* accesses pulling from the pull_request event payload on .closed events:

  • Engine fail-opens on if: guards (job fires when it should skip)
  • Head-ref-shaped expressions evaluate to empty strings
  • Documented forgejo.* namespace is parity-by-definition with github.* — namespace-swap doesn't help
  • Falsified at n=3: PRs #48, #49, #50 all empirically reproduced the same error

Therefore: the redesign must not depend on pull_request event payload expressions for any load-bearing decision.

Proposed shape (release-please-style)

Trigger: on: push: branches: [main] only.

State: .release-toolkit-manifest.json (or extend existing config) tracking:

  • Last-released SHA
  • Last-released version
  • (optionally) which next-bump is computed from accumulated commits/fragments

Logic in a single workflow:

  1. Walk git log <last-released-sha>..HEAD (shell, not expressions)
  2. If HEAD commit is the merge of the rolling release PR → cut the release (call _release-draft.yml + tag) → update manifest
  3. Otherwise → update or open the rolling release PR (call release-prep.sh against the accumulated bump) → push to release-prep/vX.Y.Z branch → open/refresh PR

Single workflow run handles both paths with internal branching, same as release-please.

Acceptance criteria

  • .release-toolkit-manifest.json schema designed + documented — schema shipped (fields: schema, last_released_sha, last_released_version, last_released_tag, last_released_at; verifiable at .release-toolkit-manifest.json on forgejo/main; ADR-0004 governs the design). Machine-managed; operator-not-to-hand-edit convention documented in .forgejo/workflows/release.yml header.
  • Single release.yml workflow on push:main triggers both the rolling-PR update AND the post-merge release cut — landed as .forgejo/workflows/release.yml; header comment names the three modes (mode=noop / mode=update / mode=cut) and the load-bearing decision hook (release-decide.sh). Currently live on release-toolkit + tmux-tell + cellblock + ember + jam-site consumers.
  • Decision logic (rolling-PR-update vs release-cut) is shell-based (git log + manifest read), no pull_request expressions — release-decide.sh walks git log <last_released_sha>..HEAD; the entire release pipeline avoids pull_request expressions per Forgejo-engine-bug substrate constraint. Recently hardened via #259 (buried-prepare walk) + #331 (squash-merge regex) + #366-#370 (multi-layer safeguards).
  • Dogfooded on release-toolkit itself: a v0.4.0 cut goes through the new mechanic end-to-end — dogfooded from v0.4.0 through v0.33.0 (~30 cuts on release-toolkit itself); last-released state currently at 0.33.0 per manifest.
  • docs/integration.md updated with the new consumer-side template (likely 15-20 lines, similar to release-please's) — updated; sections include ## What you get, ## Prerequisites, ## Bootstrapping a new consumer repo, ## Adoption, ## Runner requirements (#357), ## Manifest vs tag anchor precedence (#380), ## Per-project configuration overrides, ## Default branch (#370). Consumer template lives at ## Bootstrapping a new consumer repo.
  • Migration path documented for existing v0.3.4 consumers (cellblock, tmux-tell pending adoption) — both consumers migrated + running current release-toolkit substrate (evidence: tmux-tell released v0.35.0 2026-07-23 via the toolkit; cellblock is a live consumer per docs/case-study-cellblock reference on tmux-tell). Migration path documented under docs/integration.md § "Adoption".
  • Existing scripts/release-prep.sh + scripts/draft-release.sh reused (not rewritten) — orchestrated by the new entry-point step — both scripts survive as the orchestrated worker steps invoked by release-decide.sh + .forgejo/workflows/reusable-release.yml; not rewritten. Header comment: mode=update -> ... invoke release-prep.sh --rolling-mode / mode=cut -> ... invoke draft-release.sh.
  • bump_override UX preserved (operator can still force a major/minor cut when needed) — .forgejo/workflows/release.yml:36-59 preserves workflow_dispatch with bump_override + dry_run inputs. Header comment: "workflow_dispatch retained as a fast-path for emergency cuts (bump_override + dry_run inputs preserve v0.3.x dispatch ergonomics)."

Open design questions

  • Does the rolling PR auto-merge on operator approval, or stay manual-merge? (release-please leaves it manual)
  • Does the cut auto-publish (= no Forgejo draft state) or stay manual-publish-click? (preserving the draft + click matches current UX)
  • How does bump_override work in the rolling-PR mechanic? (release-please uses PR labels for this; we could too)
  • What's the migration story for existing consumers — flag-day or coexistence period?

Refs

  • #41 (substrate-evidence + stopgap close)
  • #47 (Forgejo expression-engine substrate finding — likely promotes to a memory pin during this redesign work)
  • release-please mechanic research from v0.3.4 sprint (in QM session transcript)

Filed: 2026-06-25 from the v0.3.4 sprint convergence with operator + Surveyor.


AC-hygiene sweep 2026-07-24 (Quartermaster; per Bosun 3f6b dispatch): all 8 ACs substrate-verified across .release-toolkit-manifest.json + .forgejo/workflows/release.yml + docs/integration.md + ~30 successful dogfood cuts (v0.4.0 → v0.33.0). Done-not-ticked → ticked with evidence pointers. The v0.4.0 architectural arc is the load-bearing substrate the current release-toolkit runs on; every subsequent cut-mechanism improvement (#259, #331, #366+) sits on top of it.

## Goal Reinstate the auto-fire / single-button release flow without exposing the Forgejo expression-engine bug (#41/#47). Adopt the operator's preferred shape (drifting/rolling release PR + single release action that triggers on merge), modeled on Google's release-please. ## Why now #41 closed in v0.3.4 (PR #51) by removing the broken `pull_request.closed` auto-fire entirely — `release-draft.yml` is now operator-dispatch-only via `workflow_dispatch`. That's the substrate-honest stopgap; the architectural redesign is the real fix. ## Substrate constraint that shapes the design Forgejo's Actions expression engine cannot reliably resolve `github.*` / `forgejo.*` accesses pulling from the `pull_request` event payload on `.closed` events: - Engine fail-opens on `if:` guards (job fires when it should skip) - Head-ref-shaped expressions evaluate to empty strings - Documented `forgejo.*` namespace is parity-by-definition with `github.*` — namespace-swap doesn't help - Falsified at n=3: PRs #48, #49, #50 all empirically reproduced the same error Therefore: **the redesign must not depend on `pull_request` event payload expressions for any load-bearing decision.** ## Proposed shape (release-please-style) **Trigger**: `on: push: branches: [main]` only. **State**: `.release-toolkit-manifest.json` (or extend existing config) tracking: - Last-released SHA - Last-released version - (optionally) which next-bump is computed from accumulated commits/fragments **Logic** in a single workflow: 1. Walk `git log <last-released-sha>..HEAD` (shell, not expressions) 2. If HEAD commit is the merge of the rolling release PR → cut the release (call `_release-draft.yml` + tag) → update manifest 3. Otherwise → update or open the rolling release PR (call `release-prep.sh` against the accumulated bump) → push to `release-prep/vX.Y.Z` branch → open/refresh PR **Single workflow run handles both paths** with internal branching, same as release-please. ## Acceptance criteria - [x] `.release-toolkit-manifest.json` schema designed + documented — schema shipped (fields: `schema`, `last_released_sha`, `last_released_version`, `last_released_tag`, `last_released_at`; verifiable at `.release-toolkit-manifest.json` on `forgejo/main`; ADR-0004 governs the design). Machine-managed; operator-not-to-hand-edit convention documented in `.forgejo/workflows/release.yml` header. - [x] Single `release.yml` workflow on `push:main` triggers both the rolling-PR update AND the post-merge release cut — landed as `.forgejo/workflows/release.yml`; header comment names the three modes (`mode=noop` / `mode=update` / `mode=cut`) and the load-bearing decision hook (`release-decide.sh`). Currently live on release-toolkit + tmux-tell + cellblock + ember + jam-site consumers. - [x] Decision logic (rolling-PR-update vs release-cut) is shell-based (`git log` + manifest read), no `pull_request` expressions — `release-decide.sh` walks `git log <last_released_sha>..HEAD`; the entire release pipeline avoids `pull_request` expressions per Forgejo-engine-bug substrate constraint. Recently hardened via #259 (buried-prepare walk) + #331 (squash-merge regex) + #366-#370 (multi-layer safeguards). - [x] Dogfooded on release-toolkit itself: a v0.4.0 cut goes through the new mechanic end-to-end — dogfooded from v0.4.0 through v0.33.0 (~30 cuts on release-toolkit itself); last-released state currently at `0.33.0` per manifest. - [x] `docs/integration.md` updated with the new consumer-side template (likely 15-20 lines, similar to release-please's) — updated; sections include `## What you get`, `## Prerequisites`, `## Bootstrapping a new consumer repo`, `## Adoption`, `## Runner requirements (#357)`, `## Manifest vs tag anchor precedence (#380)`, `## Per-project configuration overrides`, `## Default branch (#370)`. Consumer template lives at `## Bootstrapping a new consumer repo`. - [x] Migration path documented for existing v0.3.4 consumers (cellblock, tmux-tell pending adoption) — both consumers migrated + running current release-toolkit substrate (evidence: tmux-tell released `v0.35.0` 2026-07-23 via the toolkit; cellblock is a live consumer per `docs/case-study-cellblock` reference on tmux-tell). Migration path documented under `docs/integration.md` § "Adoption". - [x] Existing `scripts/release-prep.sh` + `scripts/draft-release.sh` reused (not rewritten) — orchestrated by the new entry-point step — both scripts survive as the orchestrated worker steps invoked by `release-decide.sh` + `.forgejo/workflows/reusable-release.yml`; not rewritten. Header comment: `mode=update -> ... invoke release-prep.sh --rolling-mode` / `mode=cut -> ... invoke draft-release.sh`. - [x] `bump_override` UX preserved (operator can still force a major/minor cut when needed) — `.forgejo/workflows/release.yml:36-59` preserves `workflow_dispatch` with `bump_override` + `dry_run` inputs. Header comment: *"workflow_dispatch retained as a fast-path for emergency cuts (bump_override + dry_run inputs preserve v0.3.x dispatch ergonomics)."* ## Open design questions - Does the rolling PR auto-merge on operator approval, or stay manual-merge? (release-please leaves it manual) - Does the cut auto-publish (= no Forgejo draft state) or stay manual-publish-click? (preserving the draft + click matches current UX) - How does `bump_override` work in the rolling-PR mechanic? (release-please uses PR labels for this; we could too) - What's the migration story for existing consumers — flag-day or coexistence period? ## Refs - #41 (substrate-evidence + stopgap close) - #47 (Forgejo expression-engine substrate finding — likely promotes to a memory pin during this redesign work) - release-please mechanic research from v0.3.4 sprint (in QM session transcript) Filed: 2026-06-25 from the v0.3.4 sprint convergence with operator + Surveyor. --- _AC-hygiene sweep 2026-07-24 (Quartermaster; per Bosun 3f6b dispatch): all 8 ACs substrate-verified across `.release-toolkit-manifest.json` + `.forgejo/workflows/release.yml` + `docs/integration.md` + ~30 successful dogfood cuts (v0.4.0 → v0.33.0). Done-not-ticked → ticked with evidence pointers. The v0.4.0 architectural arc is the load-bearing substrate the current release-toolkit runs on; every subsequent cut-mechanism improvement (#259, #331, #366+) sits on top of it._
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#52
No description provided.