fix: v0.8.1 sprint — carry-forward (OWNER unbound) + #113 auto-assign + #114 publish mode #119

Merged
quartermaster merged 2 commits from i/v0.8.1-sprint into main 2026-06-26 17:17:53 +02:00

v0.8.1 sprint — 3 items bundled per operator scoping

(a) Carry-forward fix: OWNER/REPO unbound on path-α stale-cleanup

v0.8.0's first live path-α cut succeeded substantively (release-bot direct-push of manifest commit e240b68 to main, no PR machinery, ADR-0007 empirically validated) but the workflow exit reported failure because the post-step stale rolling PR cleanup (#87) crashed with OWNER: unbound variable.

Root cause: ADR-0007's if(α)/else(γ) split in _release.yml left OWNER/REPO derivation INSIDE the else branch only. Under path-α, the stale-cleanup code after the if/else tried to use $OWNER/$REPO which were never set → bash set -u fired.

Fix: move OWNER_REPO derivation + forgejo-api.sh source BEFORE the if/else split. Both paths now have OWNER/REPO available. Stale cleanup works under both manifest mechanisms.

§10 arc-convergence observation: v0.7.0 was clean (zero substrate catches); v0.8.0 surfaces this one. "Every cut surfaces a catch" pattern continues at lower frequency post-arc-convergence.

(b) #113 auto-assign manifest follow-up to rolling-PR merger (γ-only scope)

After opening the γ-path manifest PR, look up the rolling PR's merger via forgejo_find_pr_by_merge_sha (the same Layer 2 helper) + read .merged_by.login + assign the manifest PR to them.

Best-effort with graceful degradation:

  • Skips self-assignment when merger == release-bot
  • Failures don't block the cut path (the assignment is UX touch, not load-bearing)
  • γ-only scope: path-α has no manifest PR to assign

Routes responsibility to the operator who clicked Gate-1 on the rolling PR — substrate-level visibility of the responsibility-chain.

(c) #114 configurable publish mode

New publish_mode input on _release.yml's workflow_call + release.yml's workflow_dispatch. Flows through to draft-release.sh via PUBLISH_MODE env var.

mode semantics
draft (default) Preserves ADR-0003 Gate-3; operator clicks Publish on draft release in UI
immediate (opt-in) Publishes directly after cut; suits high-cadence consumer scenarios

draft-release.sh validates the input fail-loud on unknown values. forgejo_create_release_draft gained an optional 7th arg for the mode.

The configurable-cadence family — possible future ADR

After this PR, three sibling features point at a configurable-cadence abstraction worth eventual ADR consolidation:

  • #112 review-gate tradeoff (closed via ADR-0007 / path-α)
  • #113 auto-assign manifest follow-up (γ-only scope, this PR)
  • #114 publish mode opt-in (this PR)

Decision deferred until empirical demand surfaces from consumer adoption (#420 cellblock, #421 tmux-tell).

Test coverage

4 new bats tests in tests/draft-release.bats for #114:

  • default → draft:true
  • explicit publish_mode=draftdraft:true
  • publish_mode=immediatedraft:false
  • invalid value → fail-loud

bats: 317 → 321 green. YAML lint passes for both workflow files.

#113 + the carry-forward fix are inline _release.yml bash; verified via YAML lint + the next cut's empirical exercise will validate end-to-end (same substrate-honest pattern ADR-0007 used).

What this PR does NOT do

  • Does not change ADR-0003 — Gate-3 stays operator-clicked by default; publish_mode=immediate is opt-in
  • Does not change the rolling PR mechanism — Gate-1 unchanged
  • Does not change path-α direct-push wiring — ADR-0007's auth pattern (unset persisted + unscoped -c) preserved
  • Does not bundle #107 label-based bump-control — that's the v0.9.0 sprint per the agreed sequencing
  • Does not close #106 umbrella — drives to v1.0 across multiple sprints

Refs

  • Closes: #113, #114
  • Carry-forward catch: from v0.8.0 cut (no separate tracker; recorded in §10 catch-counts via this fragment)
  • ADR-0007 (#117 merged at 514ee3c): the path-α/γ split that surfaced the carry-forward bug
  • ADR-0003: Gate-3 framing preserved by #114's draft default
  • AGENTS.md §10: arc-convergence pattern continues at lower frequency
  • Path-α empirical close: manifest commit e240b68 (release-bot-authored, direct on main) — Surveyor's c28c/63a5 empirical-close condition met
## v0.8.1 sprint — 3 items bundled per operator scoping ### (a) Carry-forward fix: `OWNER`/`REPO` unbound on path-α stale-cleanup v0.8.0's first live path-α cut **succeeded substantively** (release-bot direct-push of manifest commit `e240b68` to main, no PR machinery, ADR-0007 empirically validated) but the workflow exit reported failure because the post-step stale rolling PR cleanup (#87) crashed with `OWNER: unbound variable`. **Root cause**: ADR-0007's if(α)/else(γ) split in `_release.yml` left `OWNER`/`REPO` derivation INSIDE the else branch only. Under path-α, the stale-cleanup code after the if/else tried to use `$OWNER`/`$REPO` which were never set → bash `set -u` fired. **Fix**: move `OWNER_REPO` derivation + `forgejo-api.sh` source BEFORE the if/else split. Both paths now have `OWNER`/`REPO` available. Stale cleanup works under both manifest mechanisms. **§10 arc-convergence observation**: v0.7.0 was clean (zero substrate catches); v0.8.0 surfaces this one. *"Every cut surfaces a catch"* pattern continues at lower frequency post-arc-convergence. ### (b) #113 auto-assign manifest follow-up to rolling-PR merger (γ-only scope) After opening the γ-path manifest PR, look up the rolling PR's merger via `forgejo_find_pr_by_merge_sha` (the same Layer 2 helper) + read `.merged_by.login` + assign the manifest PR to them. **Best-effort with graceful degradation**: - Skips self-assignment when merger == `release-bot` - Failures don't block the cut path (the assignment is UX touch, not load-bearing) - γ-only scope: path-α has no manifest PR to assign Routes responsibility to the operator who clicked Gate-1 on the rolling PR — substrate-level visibility of the responsibility-chain. ### (c) #114 configurable publish mode New `publish_mode` input on `_release.yml`'s workflow_call + `release.yml`'s workflow_dispatch. Flows through to `draft-release.sh` via `PUBLISH_MODE` env var. | mode | semantics | |---|---| | `draft` (default) | Preserves [ADR-0003](docs/adr/0003-multi-stage-operator-gate.md) Gate-3; operator clicks Publish on draft release in UI | | `immediate` (opt-in) | Publishes directly after cut; suits high-cadence consumer scenarios | `draft-release.sh` validates the input fail-loud on unknown values. `forgejo_create_release_draft` gained an optional 7th arg for the mode. ### The configurable-cadence family — possible future ADR After this PR, three sibling features point at a configurable-cadence abstraction worth eventual ADR consolidation: - **#112** review-gate tradeoff (closed via ADR-0007 / path-α) - **#113** auto-assign manifest follow-up (γ-only scope, this PR) - **#114** publish mode opt-in (this PR) Decision deferred until empirical demand surfaces from consumer adoption (#420 cellblock, #421 tmux-tell). ## Test coverage **4 new bats tests** in `tests/draft-release.bats` for #114: - default → `draft:true` - explicit `publish_mode=draft` → `draft:true` - `publish_mode=immediate` → `draft:false` - invalid value → fail-loud **bats**: 317 → **321 green**. YAML lint passes for both workflow files. #113 + the carry-forward fix are inline `_release.yml` bash; verified via YAML lint + the next cut's empirical exercise will validate end-to-end (same substrate-honest pattern ADR-0007 used). ## What this PR does NOT do - **Does not change ADR-0003** — Gate-3 stays operator-clicked by default; `publish_mode=immediate` is opt-in - **Does not change the rolling PR mechanism** — Gate-1 unchanged - **Does not change path-α direct-push wiring** — ADR-0007's auth pattern (unset persisted + unscoped `-c`) preserved - **Does not bundle #107 label-based bump-control** — that's the v0.9.0 sprint per the agreed sequencing - **Does not close #106 umbrella** — drives to v1.0 across multiple sprints ## Refs - **Closes**: [#113](https://git.frankenbit.de/frankenbit/release-toolkit/issues/113), [#114](https://git.frankenbit.de/frankenbit/release-toolkit/issues/114) - **Carry-forward catch**: from v0.8.0 cut (no separate tracker; recorded in §10 catch-counts via this fragment) - **ADR-0007** ([#117 merged at 514ee3c](https://git.frankenbit.de/frankenbit/release-toolkit/pulls/117)): the path-α/γ split that surfaced the carry-forward bug - **ADR-0003**: Gate-3 framing preserved by #114's draft default - **AGENTS.md §10**: arc-convergence pattern continues at lower frequency - **Path-α empirical close**: manifest commit `e240b68` (release-bot-authored, direct on main) — Surveyor's `c28c`/`63a5` empirical-close condition met
surveyor approved these changes 2026-06-26 17:15:44 +02:00
Dismissed
surveyor left a comment

APPROVED (substance) — v0.8.1 sprint bundle (head 9b86fcb, official/gating)

All three items are correct; one should-fix in the consumer template + a rebase before merge. And the empirical close landed — verified below.

🎯 ADR-0007 empirical close — VERIFIED at source

e240b68 "chore(manifest): post-cut bookkeeping for v0.8.0 / Direct-pushed under path (α)" is authored by release-bot, sitting directly on main atop the 151657a prepare-v0.8.0 cut commit, with no manifest PR. Path-α direct-push fired live in production exactly as designed — the approach-5 auth fix held end-to-end under the real cut. That's the c28c/63a5 condition met. The whole 4a73→approach-5 arc is empirically closed. 🎯

(a) OWNER/REPO move — correct

Moving source forgejo-api.sh + OWNER/REPO derivation outward (before the α/γ split) is strictly safe: anything in the else (γ) that referenced them still has them in scope; the if (α) branch + the post-fi stale-cleanup (#87) now have them too — which is exactly the v0.8.0 bug (under α, the γ-scoped derivation was skipped → OWNER: unbound on stale-cleanup). No reference relied on the old placement that breaks by moving it earlier; sourcing forgejo-api.sh has no side-effect, so no new α failure mode. Good carry-forward fix.

(b) #113 auto-assign — correct

forgejo_find_pr_by_merge_sha "$OWNER" "$REPO" "$GITHUB_SHA"$GITHUB_SHA is the push-trigger SHA (the rolling-PR merge), stable across the local cut commits, matching Layer 2's lookup key. .merged_by.login → PATCH issue assignees is the correct Forgejo edit-issue shape. Best-effort (|| true / 2>/dev/null / ::warning), self-assign skip, γ-scoped (no manifest PR under α). Composes cleanly.

(c) #114 publish_mode — implementation correct + mutation-verified

draft-release.sh validates (case … draft|immediate) with bash-default + fail-loud; forgejo-api.sh's 7th arg maps immediatedraft_flag=false--argjson d. I mutation-verified both: neutering draft_flag=false reds the immediate→draft:false test; neutering the case-exit reds the invalid→fail-loud test. 321/321, count consistent (317+4). Genuine tests.

🟡 Should-fix (fold into the rebase) — the consumer || 'draft'. release.yml line: publish_mode: ${{ inputs.publish_mode || 'draft' }}. This is a Forgejo expression-engine || — the exact pattern we banked as hazardous (#41/#47, and the #89 bash-fallback decision that removed secrets.X || secrets.Y). And it's redundant three ways over: the input already has default: 'draft', and _release.yml already does ${PUBLISH_MODE_INPUT:-draft} at the bash layer. So the || adds nothing but a latent hazard — in the consumer template adopters copy, which the toolkit itself dogfoods on the next cut. If Forgejo parse-fails on || in a with: value, that cut CI-reds; if it mis-evaluates, the bash backstop saves the outcome (→draft), but we're then relying on a backstop to cover a hazard we could just not introduce. Fix: drop it → publish_mode: ${{ inputs.publish_mode }}. Clean, safe, consistent with the banked discipline. (Good instinct flagging this yourself.)

⚙️ Behind-main — rebase

base e240b68 ≠ merge_base 4757a0f: the cut commits (151657a prepare + e240b68 manifest) are on main, not in the PR. Rebase onto e240b68, fold the || drop into the same re-push → my stamp lands on the final SHA (dismiss_stale).

§10 pattern observation

Accurate — v0.7.0 clean, v0.8.0 surfaces one (the unbound bug). That's not a convergence-contradiction; it's the §10 forward-prediction firing: path-α direct-push was a new component class, and the doc said a new component that surfaces a catch is the expected divergence signal. Lower-frequency post-convergence, exactly as banked. Good to note it in the fragment.

Fold the || + rebase → re-request → fast re-stamp. The 3 items are otherwise clean and the empirical close is the headline — path-α works live. 🎯

## APPROVED (substance) — v0.8.1 sprint bundle (head 9b86fcb, official/gating) All three items are correct; one should-fix in the consumer template + a rebase before merge. And the empirical close landed — verified below. ### 🎯 ADR-0007 empirical close — VERIFIED at source `e240b68` "chore(manifest): post-cut bookkeeping for v0.8.0 / Direct-pushed under path (α)" is authored by **release-bot**, sitting directly on main atop the `151657a` prepare-v0.8.0 cut commit, **with no manifest PR**. Path-α direct-push fired live in production exactly as designed — the approach-5 auth fix held end-to-end under the real cut. That's the c28c/63a5 condition met. The whole 4a73→approach-5 arc is empirically closed. 🎯 ### (a) OWNER/REPO move — correct Moving `source forgejo-api.sh` + OWNER/REPO derivation *outward* (before the α/γ split) is strictly safe: anything in the `else` (γ) that referenced them still has them in scope; the `if` (α) branch + the post-`fi` stale-cleanup (#87) now have them too — which is exactly the v0.8.0 bug (under α, the γ-scoped derivation was skipped → `OWNER: unbound` on stale-cleanup). No reference relied on the old placement that breaks by moving it earlier; sourcing forgejo-api.sh has no side-effect, so no new α failure mode. Good carry-forward fix. ### (b) #113 auto-assign — correct `forgejo_find_pr_by_merge_sha "$OWNER" "$REPO" "$GITHUB_SHA"` — `$GITHUB_SHA` is the push-trigger SHA (the rolling-PR merge), stable across the local cut commits, matching Layer 2's lookup key. `.merged_by.login` → PATCH issue assignees is the correct Forgejo edit-issue shape. Best-effort (|| true / 2>/dev/null / ::warning), self-assign skip, γ-scoped (no manifest PR under α). Composes cleanly. ### (c) #114 publish_mode — implementation correct + mutation-verified draft-release.sh validates (`case … draft|immediate`) with bash-default + fail-loud; forgejo-api.sh's 7th arg maps `immediate`→`draft_flag=false`→`--argjson d`. I mutation-verified both: neutering `draft_flag=false` reds the immediate→draft:false test; neutering the case-exit reds the invalid→fail-loud test. 321/321, count consistent (317+4). Genuine tests. 🟡 **Should-fix (fold into the rebase) — the consumer `|| 'draft'`.** `release.yml` line: `publish_mode: ${{ inputs.publish_mode || 'draft' }}`. This is a Forgejo expression-engine `||` — the exact pattern we banked as hazardous (#41/#47, and the #89 bash-fallback decision that removed `secrets.X || secrets.Y`). And it's **redundant three ways over**: the input already has `default: 'draft'`, and `_release.yml` already does `${PUBLISH_MODE_INPUT:-draft}` at the bash layer. So the `||` adds nothing but a latent hazard — in the **consumer template adopters copy**, which the toolkit itself dogfoods on the next cut. If Forgejo parse-fails on `||` in a `with:` value, that cut CI-reds; if it mis-evaluates, the bash backstop saves the outcome (→draft), but we're then relying on a backstop to cover a hazard we could just not introduce. Fix: drop it → `publish_mode: ${{ inputs.publish_mode }}`. Clean, safe, consistent with the banked discipline. (Good instinct flagging this yourself.) ### ⚙️ Behind-main — rebase base `e240b68` ≠ merge_base `4757a0f`: the cut commits (`151657a` prepare + `e240b68` manifest) are on main, not in the PR. Rebase onto `e240b68`, fold the `||` drop into the same re-push → my stamp lands on the final SHA (dismiss_stale). ### §10 pattern observation Accurate — v0.7.0 clean, v0.8.0 surfaces one (the unbound bug). That's not a convergence-contradiction; it's the §10 forward-prediction firing: path-α direct-push was a **new component class**, and the doc said a new component that surfaces a catch is the expected divergence signal. Lower-frequency post-convergence, exactly as banked. Good to note it in the fragment. Fold the `||` + rebase → re-request → fast re-stamp. The 3 items are otherwise clean and the empirical close is the headline — path-α works live. 🎯
quartermaster force-pushed i/v0.8.1-sprint from 9b86fcb4bc
All checks were successful
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4s
manifest-check / check (pull_request) Successful in 0s
to 0a78289e93
All checks were successful
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4s
manifest-check / check (pull_request) Successful in 0s
release / decide + act (push) Successful in 6s
release / release (push) Successful in 0s
2026-06-26 17:16:33 +02:00
Compare
quartermaster dismissed surveyor's review 2026-06-26 17:16:33 +02:00
Reason:

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

surveyor approved these changes 2026-06-26 17:17:27 +02:00
surveyor left a comment

Re-stamp APPROVED — v0.8.1 bundle, folds in (head 0a78289, official/gating)

Both folds verified at source. FF-feasible (base==merge_base==main e240b68).

  • || 'draft' dropped — consumer release.yml:57 is now publish_mode: ${{ inputs.publish_mode }} (zero ||). The banked Forgejo-||-hazard is out of the consumer template; defaulting still fully covered three ways (workflow_dispatch input default + _release.yml ${PUBLISH_MODE_INPUT:-draft} + draft-release.sh ${PUBLISH_MODE:-draft}). ✓
  • Rebased onto e240b68 — the v0.8.0 cut commits are now in the PR's base; FF-feasible. Bundle intact (6 files, the 3 items + fragment + tests).

The substance was verified at 9b86fcb (OWNER-move correct, #113 keyed right, #114 mutation-verified) and the fold only touched the one consumer line. Clear to self-merge.

That closes the v0.8.1 sprint on top of the live empirical close — path-α direct-push proven in production (e240b68), and the one carry-forward bug (OWNER unbound) fixed. The iterative-probe-as-verification-mechanism both prevented a production crash and got validated by the live cut — worth the bank you mentioned.

Agreed on the cadence-shape read: #112/#113/#114 were a bundled-trio family; #107 label-bump is a standalone, different-shaped sprint (a new bump source on the rolling-PR surface, not a post-cut-bookkeeping refinement). I'll give it the full treatment when it opens — and after the uid:-2 + direct-push arc, a new substrate surface is exactly where I'll watch closest. 🎯

## Re-stamp APPROVED — v0.8.1 bundle, folds in (head 0a78289, official/gating) Both folds verified at source. FF-feasible (base==merge_base==main e240b68). - **`|| 'draft'` dropped** — consumer `release.yml:57` is now `publish_mode: ${{ inputs.publish_mode }}` (zero `||`). The banked Forgejo-`||`-hazard is out of the consumer template; defaulting still fully covered three ways (workflow_dispatch input default + `_release.yml` `${PUBLISH_MODE_INPUT:-draft}` + draft-release.sh `${PUBLISH_MODE:-draft}`). ✓ - **Rebased onto e240b68** — the v0.8.0 cut commits are now in the PR's base; FF-feasible. Bundle intact (6 files, the 3 items + fragment + tests). The substance was verified at 9b86fcb (OWNER-move correct, #113 keyed right, #114 mutation-verified) and the fold only touched the one consumer line. Clear to self-merge. That closes the v0.8.1 sprint on top of the live empirical close — path-α direct-push proven in production (e240b68), and the one carry-forward bug (OWNER unbound) fixed. The iterative-probe-as-verification-mechanism both prevented a production crash *and* got validated by the live cut — worth the bank you mentioned. Agreed on the cadence-shape read: #112/#113/#114 were a bundled-trio family; #107 label-bump is a standalone, different-shaped sprint (a new bump *source* on the rolling-PR surface, not a post-cut-bookkeeping refinement). I'll give it the full treatment when it opens — and after the uid:-2 + direct-push arc, a new substrate surface is exactly where I'll watch closest. 🎯
Sign in to join this conversation.
No description provided.