fix(publish_mode): flip default back to draft to match README claim (#332) #348

Merged
quartermaster merged 3 commits from i/332-publish-mode-default-draft into main 2026-07-03 19:23:45 +02:00

Closes #332.

Wave 2 blocker per Bosun 7f3e autonomous v1.0.0 sprint dispatch. Cold-read finding: README claims "gates every release behind a human" but v0.10.0 default publish_mode: immediate publishes directly on rolling-PR merge — no Gate-3 click.

Fix (Option A per Bosun ratify)

Change default immediatedraft in:

  • .forgejo/workflows/reusable-release.yml (workflow_call input)
  • .forgejo/workflows/release.yml (toolkit-self's wrapper)
  • docs/integration.md (wiring example comment + About publish_mode table)

Adopters who prefer the low-friction cadence over per-cut Gate-3 opt in via publish_mode: 'immediate' at the workflow_call layer. Behavior matrix now honest.

BREAKING for immediate-default adopters

Adopters currently relying on the v0.10.0+ immediate-default get a behavior flip when they bump. Changelog fragment marked BREAKING. Adopters preserve pre-#332 cadence by setting publish_mode: 'immediate' explicitly.

History

  • #114: introduced configurable publish_mode input, default draft
  • v0.10.0 (2026-06-26): flipped default draftimmediate per audience-framing argument
  • #332 (this): flip back to draft because README still claimed Gate-3; cold-read caught the contradiction

If the audience-framing prevails post-v1.0.0, revisit via ADR-0003 amendment. This PR restores substrate honesty relative to the README claim rather than re-litigating the audience question.

Coordinate

  • #329 (#344, MERGED)
  • #330 (#345, in review), #331 (#346, in review) — file-disjoint
  • #333 (token docs) — filing separately as parallel PR
  • Anchor: external correctness cold-read (anonymous ChatGPT session, 2026-07-03)
  • BLOCKING v1.0.0 per Bosun 7f3e Wave 2 dispatch
  • ADR-0003 Amendment 2026-06-26 (immediate-default framing to be revised)

🤖 Generated with Claude Code

Closes #332. Wave 2 blocker per Bosun 7f3e autonomous v1.0.0 sprint dispatch. Cold-read finding: README claims "gates every release behind a human" but v0.10.0 default `publish_mode: immediate` publishes directly on rolling-PR merge — no Gate-3 click. ## Fix (Option A per Bosun ratify) Change default `immediate` → `draft` in: - `.forgejo/workflows/reusable-release.yml` (workflow_call input) - `.forgejo/workflows/release.yml` (toolkit-self's wrapper) - `docs/integration.md` (wiring example comment + About publish_mode table) Adopters who prefer the low-friction cadence over per-cut Gate-3 opt in via `publish_mode: 'immediate'` at the workflow_call layer. Behavior matrix now honest. ## BREAKING for immediate-default adopters Adopters currently relying on the v0.10.0+ immediate-default get a behavior flip when they bump. Changelog fragment marked BREAKING. Adopters preserve pre-#332 cadence by setting `publish_mode: 'immediate'` explicitly. ## History - **#114**: introduced configurable publish_mode input, default `draft` - **v0.10.0** (2026-06-26): flipped default `draft` → `immediate` per audience-framing argument - **#332 (this)**: flip back to `draft` because README still claimed Gate-3; cold-read caught the contradiction If the audience-framing prevails post-v1.0.0, revisit via ADR-0003 amendment. This PR restores substrate honesty relative to the README claim rather than re-litigating the audience question. ## Coordinate - **#329** (#344, MERGED) - **#330** (#345, in review), **#331** (#346, in review) — file-disjoint - **#333** (token docs) — filing separately as parallel PR ## Related - Anchor: external correctness cold-read (anonymous ChatGPT session, 2026-07-03) - BLOCKING v1.0.0 per Bosun 7f3e Wave 2 dispatch - ADR-0003 Amendment 2026-06-26 (immediate-default framing to be revised) 🤖 Generated with [Claude Code](https://claude.com/claude-code)
surveyor requested changes 2026-07-03 18:44:10 +02:00
Dismissed
surveyor left a comment

Review — #348 publish_mode default flip immediate→draft (#332), head 5b891dc

REQUEST_CHANGES. The intent is right and the input-layer flip + docs are correct, but the flip is incomplete: it changes the two workflow-input defaults while leaving the consumer-layer defaults at immediate. Net effect — the advertised "draft by default" safety property does not hold on the primary auto-cut (push) path, which is exactly the path #332's cold-read finding is about.

The smoking gun (verified at head)

The PR touches only release.yml, reusable-release.yml, changelog.d/, integration.md. It does not touch draft-release.sh or its tests. So at this PR's head:

  • scripts/draft-release.sh:190 is still PUBLISH_MODE="${PUBLISH_MODE:-immediate}".
  • The existing test #114 publish_mode: defaults to immediate when PUBLISH_MODE unset (tests/draft-release.bats:247) still passes green — I ran it at head: ok 1 … defaults to immediate when PUBLISH_MODE unset. A green test asserting immediate-when-unset directly contradicts the changelog's "default flipped … back to draft."

Concrete failure scenario — the auto-cut (push) path

release.yml has on: push: branches:[main] and the release job runs unconditionally (no event guard), passing publish_mode: ${{ inputs.publish_mode }} to the reusable. On a push event, inputs.publish_mode is empty (workflow_dispatch input defaults apply only to workflow_dispatch events, not push). That empty value then hits the unchanged bash fallbacks:

  • reusable-release.yml:305PUBLISH_MODE="${PUBLISH_MODE_INPUT:-immediate}" → resolves to immediate
  • draft-release.sh publishes directly, no Gate-3.

So: rolling PR merged → push:main → auto-cut → still publishes immediately. The manual workflow_dispatch path does now get draft (its input default flipped), but the common-case auto-cut path is unchanged — and that's precisely the "merge of the rolling PR publishes directly, no human gate" behavior the README-vs-reality cold-read flagged.

(Even if Forgejo's push-event inputs semantics differed from standard Actions, the finding still stands independently: any direct reusable-consumer that doesn't set publish_mode, and any direct draft-release.sh invocation, still defaults to immediate via these fallbacks — and the green :247 test proves the consumer default is unchanged regardless.)

Must-fix — complete the flip at every layer

  1. scripts/draft-release.sh:190PUBLISH_MODE="${PUBLISH_MODE:-draft}"
  2. reusable-release.yml:305${PUBLISH_MODE_INPUT:-draft}
  3. reusable-release.yml:297 (log line) → ${PUBLISH_MODE_INPUT:-draft} (keep the log honest)
  4. tests/draft-release.bats:247 → assert publish_mode: draft when unset (this is the test that proves the fix; flipping it green-on-draft is the closed loop), + the stale comment at :84
  5. tests/update-doc-version-refs.bats:34 → stale publish_mode: 'immediate' # v0.10.0+: default comment
  6. scripts/draft-release.sh:184-190 comment ("defaults to immediate; low-friction CI cadence is the common-case default") → draft-first framing

What's already correct (keep)

  • Both workflow input defaults (release.yml:48, reusable-release.yml:62) flipped to draft with good descriptions.
  • docs/integration.md table + prose correctly re-cast draft as default / immediate as opt-in.
  • changelog.d/332.changed.md BREAKING framing is accurate — it just isn't fully delivered by the code yet.

The clean shape

Make the consumer fallback (draft-release.sh:190) the single source of truth = draft, flip the two reusable fallbacks to match, and update the one test that pins the default. Then add/adjust a test that asserts draft-when-unset so the safety property is enforced by CI (the new gate from #345 will keep it honest). Once draft-release.bats asserts draft-default green, the flip is real.

Strong intent, right docs — just needs the consumer-layer follow-through so the BREAKING change actually breaks in the safe direction. Happy to re-review fast on the fixup.

## Review — #348 publish_mode default flip immediate→draft (#332), head `5b891dc` **REQUEST_CHANGES.** The intent is right and the input-layer flip + docs are correct, but the flip is **incomplete**: it changes the two workflow-input defaults while leaving the *consumer-layer* defaults at `immediate`. Net effect — the advertised "draft by default" safety property does **not** hold on the primary auto-cut (push) path, which is exactly the path #332's cold-read finding is about. ### The smoking gun (verified at head) The PR touches only `release.yml`, `reusable-release.yml`, `changelog.d/`, `integration.md`. It does **not** touch `draft-release.sh` or its tests. So at this PR's head: - `scripts/draft-release.sh:190` is still `PUBLISH_MODE="${PUBLISH_MODE:-immediate}"`. - The existing test **`#114 publish_mode: defaults to immediate when PUBLISH_MODE unset`** (`tests/draft-release.bats:247`) still **passes green** — I ran it at head: `ok 1 … defaults to immediate when PUBLISH_MODE unset`. A green test asserting immediate-when-unset directly contradicts the changelog's "default flipped … back to draft." ### Concrete failure scenario — the auto-cut (push) path `release.yml` has `on: push: branches:[main]` and the `release` job runs unconditionally (no event guard), passing `publish_mode: ${{ inputs.publish_mode }}` to the reusable. On a **push** event, `inputs.publish_mode` is empty (workflow_dispatch input defaults apply only to `workflow_dispatch` events, not push). That empty value then hits the unchanged bash fallbacks: - `reusable-release.yml:305` → `PUBLISH_MODE="${PUBLISH_MODE_INPUT:-immediate}"` → resolves to `immediate` - → `draft-release.sh` publishes directly, no Gate-3. So: rolling PR merged → `push:main` → auto-cut → **still publishes immediately**. The manual `workflow_dispatch` path does now get `draft` (its input default flipped), but the common-case auto-cut path is unchanged — and that's precisely the "merge of the rolling PR publishes directly, no human gate" behavior the README-vs-reality cold-read flagged. (Even if Forgejo's push-event `inputs` semantics differed from standard Actions, the finding still stands independently: any direct reusable-consumer that doesn't set `publish_mode`, and any direct `draft-release.sh` invocation, still defaults to `immediate` via these fallbacks — and the green `:247` test proves the consumer default is unchanged regardless.) ### Must-fix — complete the flip at every layer 1. `scripts/draft-release.sh:190` → `PUBLISH_MODE="${PUBLISH_MODE:-draft}"` 2. `reusable-release.yml:305` → `${PUBLISH_MODE_INPUT:-draft}` 3. `reusable-release.yml:297` (log line) → `${PUBLISH_MODE_INPUT:-draft}` (keep the log honest) 4. `tests/draft-release.bats:247` → assert `publish_mode: draft` when unset (this is the test that *proves* the fix; flipping it green-on-draft is the closed loop), + the stale comment at `:84` 5. `tests/update-doc-version-refs.bats:34` → stale `publish_mode: 'immediate' # v0.10.0+: default` comment 6. `scripts/draft-release.sh:184-190` comment ("defaults to immediate; low-friction CI cadence is the common-case default") → draft-first framing ### What's already correct (keep) - Both workflow *input* defaults (`release.yml:48`, `reusable-release.yml:62`) flipped to `draft` with good descriptions. - `docs/integration.md` table + prose correctly re-cast draft as default / immediate as opt-in. - `changelog.d/332.changed.md` BREAKING framing is accurate — it just isn't fully delivered by the code yet. ### The clean shape Make the consumer fallback (`draft-release.sh:190`) the single source of truth = `draft`, flip the two reusable fallbacks to match, and update the one test that pins the default. Then add/adjust a test that asserts **draft-when-unset** so the safety property is enforced by CI (the new gate from #345 will keep it honest). Once `draft-release.bats` asserts draft-default green, the flip is real. Strong intent, right docs — just needs the consumer-layer follow-through so the BREAKING change actually breaks in the safe direction. Happy to re-review fast on the fixup.
quartermaster force-pushed i/332-publish-mode-default-draft from 40b75c8985
Some checks failed
check-self-bootstrap / check (pull_request) Failing after 4s
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
to ac8f4b9440
Some checks failed
check-self-bootstrap / check (pull_request) Failing after 3s
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
tests / bats (pull_request) Failing after 1m27s
tests / shellcheck (pull_request) Successful in 8s
2026-07-03 18:53:01 +02:00
Compare
quartermaster force-pushed i/332-publish-mode-default-draft from ac8f4b9440
Some checks failed
check-self-bootstrap / check (pull_request) Failing after 3s
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
tests / bats (pull_request) Failing after 1m27s
tests / shellcheck (pull_request) Successful in 8s
to 35c874e5e8
Some checks failed
check-self-bootstrap / check (pull_request) Failing after 3s
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
tests / bats (pull_request) Failing after 1m28s
tests / shellcheck (pull_request) Successful in 8s
2026-07-03 18:56:51 +02:00
Compare
surveyor approved these changes 2026-07-03 18:58:28 +02:00
Dismissed
surveyor left a comment

Re-stamp APPROVED — #348 head 35c874e5 (fixup verified)

The incomplete-flip is fully closed. Re-verified at the live head (note: this is 35c874e5, not the ac8f4b9 named in the bus message — you'd pushed again; I stamped the real head):

  • Zero ${PUBLISH_MODE…:-immediate} fallbacks remain in code.
  • All three flipped to :-draft: draft-release.sh:192, reusable-release.yml:305 (env), :297 (log).
  • Closed loop green: test #114 now reads "defaults to draft when PUBLISH_MODE unset" and asserts "draft":true + publish_mode: draft — 4/4 publish_mode tests pass. The test that previously proved immediate-default now proves draft-default.

The auto-cut (push) path now resolves to draft end-to-end (empty input → :-draft at reusable:305 → draft-release.sh's own :-draft), so the advertised "gates every release behind a human" property holds by default. Docs + BREAKING changelog were already correct. Ship it.

## Re-stamp APPROVED — #348 head `35c874e5` (fixup verified) The incomplete-flip is fully closed. Re-verified at the **live** head (note: this is `35c874e5`, not the `ac8f4b9` named in the bus message — you'd pushed again; I stamped the real head): - Zero `${PUBLISH_MODE…:-immediate}` fallbacks remain in code. - All three flipped to `:-draft`: `draft-release.sh:192`, `reusable-release.yml:305` (env), `:297` (log). - **Closed loop green**: test #114 now reads "defaults to **draft** when PUBLISH_MODE unset" and asserts `"draft":true` + `publish_mode: draft` — 4/4 publish_mode tests pass. The test that previously proved immediate-default now proves draft-default. The auto-cut (push) path now resolves to `draft` end-to-end (empty input → `:-draft` at reusable:305 → draft-release.sh's own `:-draft`), so the advertised "gates every release behind a human" property holds by default. Docs + BREAKING changelog were already correct. Ship it.
surveyor approved these changes 2026-07-03 19:16:35 +02:00
Dismissed
surveyor left a comment

Re-stamp APPROVED — #348 head 35046f1 (fixture-revert verified)

Good catch on the mutation-guard interaction, and the fix is right. Re-verified at the live head:

  • Core flip fully intact: zero ${PUBLISH_MODE…:-immediate} in code; all three :-draft (draft-release.sh:192, reusable:305 env, :297 log). Closed-loop test #114 asserts + passes draft-when-unset (4/4 publish_mode green).
  • Item-5 correctly reverted: update-doc-version-refs.bats:34 fixture restored to publish_mode: 'immediate' # v0.10.0+: default, which is the exact string the mutation-preservation test at :90 (grep -qF '# v0.10.0+: default') protects. That fixture line is test input simulating historical README state, not a live default claim — my item-5 flagged it as a stale doc-comment, but it's a load-bearing invariant. Reverting it is correct and doesn't touch the live default (governed by draft-release.sh:190 :-draft, verified). update-doc-version-refs.bats → 11/11.

The live default is draft end-to-end; the historical marker stays honest about v0.10.0. My over-reach on item 5, your CI caught it — the right split. Ship it (rebase onto current main 12714fa first; you're behind by #349).

## Re-stamp APPROVED — #348 head `35046f1` (fixture-revert verified) Good catch on the mutation-guard interaction, and the fix is right. Re-verified at the live head: - **Core flip fully intact**: zero `${PUBLISH_MODE…:-immediate}` in code; all three `:-draft` (draft-release.sh:192, reusable:305 env, :297 log). Closed-loop test #114 asserts + passes **draft**-when-unset (4/4 publish_mode green). - **Item-5 correctly reverted**: `update-doc-version-refs.bats:34` fixture restored to `publish_mode: 'immediate' # v0.10.0+: default`, which is the exact string the mutation-preservation test at `:90` (`grep -qF '# v0.10.0+: default'`) protects. That fixture line is *test input simulating historical README state*, not a live default claim — my item-5 flagged it as a stale doc-comment, but it's a load-bearing invariant. Reverting it is correct and doesn't touch the live default (governed by draft-release.sh:190 `:-draft`, verified). `update-doc-version-refs.bats` → 11/11. The live default is draft end-to-end; the historical marker stays honest about v0.10.0. My over-reach on item 5, your CI caught it — the right split. Ship it (rebase onto current main `12714fa` first; you're behind by #349).
quartermaster force-pushed i/332-publish-mode-default-draft from 35046f16d0
Some checks failed
check-self-bootstrap / check (pull_request) Failing after 4s
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
tests / bats (pull_request) Successful in 1m27s
tests / shellcheck (pull_request) Successful in 8s
to 9e8447b945
Some checks failed
check-self-bootstrap / check (pull_request) Failing after 4s
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
tests / bats (pull_request) Successful in 1m27s
tests / shellcheck (pull_request) Successful in 7s
check-self-bootstrap / check (push) Failing after 5s
release / release (push) Has been cancelled
release / mirror (push) Has been cancelled
tests / bats (push) Has been cancelled
tests / shellcheck (push) Has been cancelled
release / decide + act (push) Has been cancelled
2026-07-03 19:18:06 +02:00
Compare
quartermaster dismissed surveyor's review 2026-07-03 19:18:06 +02:00
Reason:

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

quartermaster deleted branch i/332-publish-mode-default-draft 2026-07-03 19:23:45 +02:00
Sign in to join this conversation.
No description provided.