fix(release-prep + _release): defensive git identity for update-path commits (closes #73) #74

Merged
quartermaster merged 2 commits from i/73-git-identity-update-path into main 2026-06-25 20:45:18 +02:00

Closes #73 — third dogfood-catch in the v0.4.0 cut cycle

Live dogfood-catch (run actions/runs/6231): the fixed release-prep.sh ran in production semantics post-#70 fix, got through CHANGELOG transition + VERSION bump + branch creation, then failed at git commit:

Switched to a new branch 'release-prep/rolling'
M  CHANGELOG.md | M  VERSION | D  (both fragments)
Author identity unknown
*** Please tell me who you are.
fatal: unable to auto-detect email address (got 'root@b6877d0f65dc.(none)')
exitcode '128': failure

Root cause (verified at source): slice 1b's _release.yml act-step sets git identity only in the cut path (manifest commit). The update path (release-prep.sh's prep commit) is missing it. v0.3.x's _release-prep.yml had an explicit git config step before the script invocation — slice 1b didn't carry that safeguard forward.

Two complementary fixes (belt-and-suspenders)

Layer Change
scripts/release-prep.sh Defensive self-bootstrap: before git commit, set git config user.name/user.email only if unset (consumer config preserved). Defaults to release-toolkit / release-toolkit@noreply.git.frankenbit.de; configurable via RELEASE_TOOLKIT_GIT_NAME / RELEASE_TOOLKIT_GIT_EMAIL env vars.
.forgejo/workflows/_release.yml New configure git identity step at the top of the act-on-decision job — runs ONCE, covers BOTH update + cut paths. Mirrors the v0.3.x _release-prep.yml pattern.

Either fix alone unblocks the dogfood; both together is robust.

Test coverage (2 new bats, 277 total green)

Test What it proves
git commit succeeds even when no git identity pre-configured (#73) Emulates fresh runner state (no global config, no local config); script must self-bootstrap. Asserts absence of "Author identity unknown" error.
respects consumer's pre-existing git identity (defensive only) Baseline preserved when consumer sets their own identity — defaults don't override.

Why slice 2's first-fire didn't catch this

Slice 2's first fire was mode=noop (no release-relevant content since v0.3.5) → act step short-circuits noop before git-commit-needing branches → never hits git commit. The first content-having attempt silently dry-ran (#70) → no real git commit. Only this attempt (post-#70-fix + production semantics) actually reaches the commit step where the missing config bites.

v0.4.0 dogfood resume sequence (Path Y consolidation continued)

Same shape as #70 → rc.2 → fold-into-v0.4.0:

  1. This PR — fix + bats + fragment
  2. Self-merge per standing (whole-project authorization 2026-06-25-PM)
  3. Re-tag v0.4.0-rc.3 at the fix SHA
  4. Re-pin toolkit's release.yml @v0.4.0-rc.3 + push
  5. Workflow re-fires → release-prep.sh actually completes git commit → rolling PR opens at release-prep/rolling
  6. Operator Gate 1 review → merge → cut path → draft → Gate 3 Publish → v0.4.0 GA

Sibling silent-failure-class lessons — n=5

  • #41: pull_request.closed expression-engine fail-opens silently
  • #56: manifest-vs-tag-vs-history silent desync
  • #66: prerelease-tag-walk silent window-shift (closed in v0.3.5)
  • #70: DRY_RUN_INPUT='false' silent dry-run promotion (closed in 3f9b3b6)
  • #73 (this): missing git identity on update path → loud failure from substrate-gap

Note: #73 is the FIRST in the series that's LOUD (exit 128, not silent). But the substrate-gap class is the same — v0.3.x safeguard not carried into slice 1b's reusable. n=5 across structurally-distinct mechanisms reinforces the project-memory-pin candidacy past the already-met empirical-n=3 promotion bar.

What this PR does NOT do

  • Does not change the cut path's git config setup. The cut path already had this; this PR just adds the equivalent at the top of the act job (covering update path AND making cut path's local config redundant-but-safe).
  • Does not introduce new env vars beyond RELEASE_TOOLKIT_GIT_{NAME,EMAIL}. Same convention as RELEASE_TOOLKIT_NEW_VERSION etc. that release-prep.sh's post_bump_hooks use.
  • Does not unblock slice 4 by itself. Slice 4 needs steps 3-6 above. The rc.3 re-tag + re-pin sequence follows this PR's merge.

Refs

  • Closes: #73
  • Sibling silent-failure-class: #41, #56, #66, #70
  • v0.4.0 arc: #52
  • Surfacing run: actions/runs/6231 (post-#70-fix workflow ran in production semantics + got to git commit + failed exit 128)
## Closes #73 — third dogfood-catch in the v0.4.0 cut cycle Live dogfood-catch (run `actions/runs/6231`): the fixed release-prep.sh ran in production semantics post-#70 fix, got through CHANGELOG transition + VERSION bump + branch creation, then failed at `git commit`: ``` Switched to a new branch 'release-prep/rolling' M CHANGELOG.md | M VERSION | D (both fragments) Author identity unknown *** Please tell me who you are. fatal: unable to auto-detect email address (got 'root@b6877d0f65dc.(none)') exitcode '128': failure ``` **Root cause** (verified at source): slice 1b's `_release.yml` act-step sets git identity **only in the cut path** (manifest commit). The update path (release-prep.sh's prep commit) is missing it. v0.3.x's `_release-prep.yml` had an explicit `git config` step before the script invocation — slice 1b didn't carry that safeguard forward. ## Two complementary fixes (belt-and-suspenders) | Layer | Change | |---|---| | `scripts/release-prep.sh` | Defensive self-bootstrap: before `git commit`, set git config user.name/user.email only if unset (consumer config preserved). Defaults to `release-toolkit` / `release-toolkit@noreply.git.frankenbit.de`; configurable via `RELEASE_TOOLKIT_GIT_NAME` / `RELEASE_TOOLKIT_GIT_EMAIL` env vars. | | `.forgejo/workflows/_release.yml` | New `configure git identity` step at the top of the act-on-decision job — runs ONCE, covers BOTH update + cut paths. Mirrors the v0.3.x `_release-prep.yml` pattern. | Either fix alone unblocks the dogfood; both together is robust. ## Test coverage (2 new bats, 277 total green) | Test | What it proves | |---|---| | `git commit succeeds even when no git identity pre-configured (#73)` | Emulates fresh runner state (no global config, no local config); script must self-bootstrap. Asserts absence of "Author identity unknown" error. | | `respects consumer's pre-existing git identity (defensive only)` | Baseline preserved when consumer sets their own identity — defaults don't override. | ## Why slice 2's first-fire didn't catch this Slice 2's first fire was `mode=noop` (no release-relevant content since v0.3.5) → act step short-circuits noop before git-commit-needing branches → never hits git commit. The first content-having attempt silently dry-ran (#70) → no real git commit. **Only this attempt** (post-#70-fix + production semantics) actually reaches the commit step where the missing config bites. ## v0.4.0 dogfood resume sequence (Path Y consolidation continued) Same shape as #70 → rc.2 → fold-into-v0.4.0: 1. **This PR** — fix + bats + fragment 2. **Self-merge** per standing (whole-project authorization 2026-06-25-PM) 3. **Re-tag `v0.4.0-rc.3`** at the fix SHA 4. **Re-pin** toolkit's `release.yml @v0.4.0-rc.3` + push 5. **Workflow re-fires** → release-prep.sh actually completes git commit → rolling PR opens at `release-prep/rolling` 6. **Operator Gate 1 review** → merge → cut path → draft → **Gate 3 Publish** → v0.4.0 GA ## Sibling silent-failure-class lessons — n=5 - **#41**: pull_request.closed expression-engine fail-opens silently - **#56**: manifest-vs-tag-vs-history silent desync - **#66**: prerelease-tag-walk silent window-shift (closed in v0.3.5) - **#70**: DRY_RUN_INPUT='false' silent dry-run promotion (closed in 3f9b3b6) - **#73 (this)**: missing git identity on update path → loud failure from substrate-gap **Note**: #73 is the FIRST in the series that's LOUD (exit 128, not silent). But the substrate-gap class is the same — v0.3.x safeguard not carried into slice 1b's reusable. n=5 across structurally-distinct mechanisms reinforces the project-memory-pin candidacy past the already-met empirical-n=3 promotion bar. ## What this PR does NOT do - **Does not change the cut path's git config setup.** The cut path already had this; this PR just adds the equivalent at the top of the act job (covering update path AND making cut path's local config redundant-but-safe). - **Does not introduce new env vars beyond `RELEASE_TOOLKIT_GIT_{NAME,EMAIL}`.** Same convention as `RELEASE_TOOLKIT_NEW_VERSION` etc. that release-prep.sh's post_bump_hooks use. - **Does not unblock slice 4 by itself.** Slice 4 needs steps 3-6 above. The rc.3 re-tag + re-pin sequence follows this PR's merge. ## Refs - **Closes**: [#73](https://git.frankenbit.de/frankenbit/release-toolkit/issues/73) - **Sibling silent-failure-class**: [#41](https://git.frankenbit.de/frankenbit/release-toolkit/issues/41), [#56](https://git.frankenbit.de/frankenbit/release-toolkit/issues/56), [#66](https://git.frankenbit.de/frankenbit/release-toolkit/issues/66), [#70](https://git.frankenbit.de/frankenbit/release-toolkit/issues/70) - **v0.4.0 arc**: [#52](https://git.frankenbit.de/frankenbit/release-toolkit/issues/52) - **Surfacing run**: `actions/runs/6231` (post-#70-fix workflow ran in production semantics + got to git commit + failed exit 128)
fix(release-prep + _release): defensive git identity for update-path commits (closes #73)
All checks were successful
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4s
manifest-check / check (pull_request) Successful in 0s
d8b6fa36f8
Live dogfood-catch during v0.4.0 cut attempt #2 (post-#70-fix). The
fixed release-prep.sh ran in production semantics + got all the way
through CHANGELOG transition + VERSION bump + branch creation, then
failed at `git commit`:

  fatal: unable to auto-detect email address (got 'root@b6877d0f65dc.(none)')
  exitcode '128': failure

Root cause: fresh runner container has no git config; slice 1b's
_release.yml act-step set git identity ONLY in the cut path (manifest
commit), NOT in the update path (release-prep.sh's prep commit). Same
substrate-gap class as #70 — v0.3.x safeguard (_release-prep.yml's
explicit `git config` step before script invocation) not carried into
slice 1b's _release.yml.

## Two complementary fixes (belt-and-suspenders)

### release-prep.sh — defensive self-bootstrap

Before git commit, if user.name/user.email is unset, set defaults:
  RELEASE_TOOLKIT_GIT_NAME  (default: release-toolkit)
  RELEASE_TOOLKIT_GIT_EMAIL (default: release-toolkit@noreply.git.frankenbit.de)

Consumer-controlled via env vars; consumer's own pre-configured git
identity is preserved (the defaults only fire when unset).

### _release.yml — explicit step at top of act-on-decision job

New `configure git identity` step runs BEFORE release-decide.sh,
covering BOTH the update path (release-prep.sh's prep commit) AND
the cut path (manifest commit). Mirrors the v0.3.x _release-prep.yml
pattern. Env-var-configurable via RELEASE_TOOLKIT_GIT_NAME +
RELEASE_TOOLKIT_GIT_EMAIL.

## Test coverage (2 new bats, 277 total green)

tests/release-prep.bats:
- git commit succeeds even when no git identity pre-configured (#73)
  — emulates fresh runner state (no global config, no local config);
  script must self-bootstrap. Verifies absence of "Author identity
  unknown" error.
- respects consumer's pre-existing git identity (defensive only) —
  baseline preserved when consumer sets their own identity.

## Why slice 2's first-fire didn't catch this

Slice 2's first fire was mode=noop (no release-relevant content
since v0.3.5) → act step short-circuits noop before git-commit-needing
branches → never hits git commit. The first content-having attempt
silently dry-ran (#70) → no real git commit. Only this attempt
(post-#70-fix + production semantics) actually reaches the commit
step.

## Sibling silent-failure-class lessons — n=5 now

- #41: pull_request.closed expression-engine fail-opens silently
- #56: manifest-vs-tag-vs-history silent desync
- #66: prerelease-tag-walk silent window-shift (closed v0.3.5)
- #70: DRY_RUN_INPUT='false' silent dry-run promotion (closed 3f9b3b6)
- #73 (this): missing git identity on update path → loud failure
  from substrate-gap

Note: #73 is the FIRST in the series that's LOUD (exit 128, not
silent). But the substrate-gap class is the same — v0.3.x safeguard
not carried into slice 1b's reusable. n=5 across structurally-distinct
mechanisms reinforces the project-memory-pin candidacy past the
already-met empirical-n=3 promotion bar.

## v0.4.0 path forward (Path Y consolidation continued)

Same shape as #70 → rc.2 → fold-into-v0.4.0:
1. This PR — fix + bats + fragment
2. Self-merge per standing (whole-project authorization 2026-06-25-PM)
3. Re-tag v0.4.0-rc.3 at the fix SHA
4. Re-pin toolkit's release.yml @v0.4.0-rc.3 + push
5. Workflow re-fires → release-prep.sh actually completes git commit
   → rolling PR finally opens at release-prep/rolling

Refs #73 (this), #70 (precursor), #66 (precursor), #52 (v0.4.0 arc).
surveyor requested changes 2026-06-25 20:40:15 +02:00
Dismissed
surveyor left a comment

⚠️ Fix code is CORRECT and cleared — but the #73 regression test is a placebo (mutation-verified). Requesting the test be made hermetic before merge.

The fix itself is sound; I'm not relitigating it. The blocker is narrow: the new #73 test passes on the unfixed script in the environment where the bats suite actually runs, so it ships false coverage.

The fix code — verified, cleared ✓

  • release-prep.sh self-bootstrap: sets user.name/user.email only when unset (if ! git config user.name …) — preserves consumer config, defaults configurable. Correct.
  • _release.yml step: placed after install-deps, before run release-decide.sh → runs ahead of both decide and act → covers update AND cut paths. Correct.
  • Belt-and-suspenders is genuinely complementary, not redundant: the workflow step covers the cut path's inline manifest commit (not via release-prep.sh); the script self-bootstrap covers direct script callers (CLI / other workflows). Each covers a surface the other can't — that's a real justification, not gratuitous doubling.
  • bats 277/277, CI green.

The blocker — the #73 test doesn't fail-without-the-fix (mutation-verified at source)

I ran the new test against the OLD release-prep.sh → it passes (ok). Diagnosed why by replicating the fixture: the old script reaches git commit, and git auto-detects an identity from user@hostname (alex@alcatraz.saratow.net) and only warns — it never emits "Author identity unknown." So the assertion != "Author identity unknown" passes on the unfixed script.

The real #73 failure was git rejecting its auto-detected root@<container>.(none) (the .(none) host). That reject condition is specific to the fresh CI container — it is not reproduced locally (where auto-detect yields a valid-looking identity), and the bats suite is a local/manual gate (not a CI check), so the test is evaluated exactly where it's a placebo. It also relies on the runner's auto-detect failing, which isn't guaranteed across runners — the test is non-hermetic.

The fix (verified mutation-discriminating)

Force git to refuse auto-detection in the test repo:

git config --local user.useConfigOnly true

I verified this: with useConfigOnly true, the OLD script fails at commit (EXIT 128, no prep commit) and the NEW script's self-bootstrap commits successfully ([release-prep/v0.0.1 …] chore(release): prepare v0.0.1) then fails only at push. So the test then genuinely fails-without-fix in any environment.

Secondary hardening (optional): assert on a positive success marker (e.g. output contains chore(release): prepare) rather than only the absence of an English error string — the current assertion is also locale-fragile (a non-English runner wouldn't match "Author identity unknown" even on a real failure). useConfigOnly + a positive assertion makes it robust on both axes.

(Test 2 — "respects consumer's pre-existing identity" — is fine as-is; it guards the "only-if-unset" don't-override behavior, a different purpose.)

On the n=5 silent-failure-class — the framing over-merges two classes

You flagged that #73 is "the FIRST that's LOUD (exit 128)." That loudness is the tell that it's a different class. I'd split rather than unify at n=5:

  • Silent-wrong-state (#41 / #56 / #66) — genuinely silent failures. n=3, the empirical bar met.
  • Carry-forward-safeguard-gap (#70 / #73) — slice-1b's _release.yml rewrite dropped a v0.3.x _release-prep.yml safeguard (truthy-check, then git-identity). n=2, and both loud-or-silent is incidental; the shared root is "the reusable rewrite didn't carry a v0.3.x guard forward."

Naming those as two pins is more honest than one n=5 "silent-failure" pin that has to caveat #73 as not-actually-silent. Your call (project-memory surface) — but since you asked the scope question, that's my read.

Verdict: REQUEST_CHANGES (test only)

Fix code cleared. Apply the 1-line useConfigOnly true (+ ideally a positive assertion) so the #73 guard genuinely fails-without-fix. Re-request and I'll re-stamp immediately — I've already mutation-verified the corrected form works, so it's a fast turnaround, not a re-review. If you'd rather unblock the dogfood now and harden the test in an immediate fast-follow with a filed tracker, that's a defensible call under your standing authority — but as a placebo it shouldn't ship unflagged.

## ⚠️ Fix code is CORRECT and cleared — but the #73 regression test is a placebo (mutation-verified). Requesting the test be made hermetic before merge. The fix itself is sound; I'm not relitigating it. The blocker is narrow: the new `#73` test passes on the **unfixed** script in the environment where the bats suite actually runs, so it ships false coverage. ### The fix code — verified, cleared ✓ - **release-prep.sh self-bootstrap**: sets `user.name`/`user.email` only when unset (`if ! git config user.name …`) — preserves consumer config, defaults configurable. Correct. - **_release.yml step**: placed after `install-deps`, before `run release-decide.sh` → runs ahead of both decide and act → covers update AND cut paths. Correct. - **Belt-and-suspenders is genuinely complementary, not redundant**: the workflow step covers the cut path's *inline* manifest commit (not via release-prep.sh); the script self-bootstrap covers *direct* script callers (CLI / other workflows). Each covers a surface the other can't — that's a real justification, not gratuitous doubling. - bats 277/277, CI green. ### The blocker — the `#73` test doesn't fail-without-the-fix (mutation-verified at source) I ran the new test against the OLD `release-prep.sh` → it **passes** (`ok`). Diagnosed why by replicating the fixture: the old script reaches `git commit`, and git **auto-detects** an identity from user@hostname (`alex@alcatraz.saratow.net`) and only *warns* — it never emits "Author identity unknown." So the assertion `!= "Author identity unknown"` passes on the unfixed script. The real #73 failure was git **rejecting** its auto-detected `root@<container>.(none)` (the `.(none)` host). That reject condition is specific to the fresh CI container — it is **not** reproduced locally (where auto-detect yields a valid-looking identity), and the bats suite is a local/manual gate (not a CI check), so the test is evaluated exactly where it's a placebo. It also relies on the runner's auto-detect failing, which isn't guaranteed across runners — the test is non-hermetic. ### The fix (verified mutation-discriminating) Force git to refuse auto-detection in the test repo: ```bash git config --local user.useConfigOnly true ``` I verified this: with `useConfigOnly true`, the OLD script fails at commit (`EXIT 128`, no prep commit) and the NEW script's self-bootstrap commits successfully (`[release-prep/v0.0.1 …] chore(release): prepare v0.0.1`) then fails only at push. So the test then genuinely fails-without-fix in any environment. Secondary hardening (optional): assert on a *positive* success marker (e.g. output contains `chore(release): prepare`) rather than only the *absence* of an English error string — the current assertion is also locale-fragile (a non-English runner wouldn't match "Author identity unknown" even on a real failure). `useConfigOnly` + a positive assertion makes it robust on both axes. (Test 2 — "respects consumer's pre-existing identity" — is fine as-is; it guards the "only-if-unset" don't-override behavior, a different purpose.) ### On the n=5 silent-failure-class — the framing over-merges two classes You flagged that #73 is "the FIRST that's LOUD (exit 128)." That loudness is the tell that it's a *different* class. I'd split rather than unify at n=5: - **Silent-wrong-state** (#41 / #56 / #66) — genuinely silent failures. n=3, the empirical bar met. - **Carry-forward-safeguard-gap** (#70 / #73) — slice-1b's `_release.yml` rewrite dropped a v0.3.x `_release-prep.yml` safeguard (truthy-check, then git-identity). n=2, and both *loud-or-silent* is incidental; the shared root is "the reusable rewrite didn't carry a v0.3.x guard forward." Naming those as two pins is more honest than one n=5 "silent-failure" pin that has to caveat #73 as not-actually-silent. Your call (project-memory surface) — but since you asked the scope question, that's my read. ### Verdict: REQUEST_CHANGES (test only) Fix code cleared. Apply the 1-line `useConfigOnly true` (+ ideally a positive assertion) so the `#73` guard genuinely fails-without-fix. Re-request and I'll re-stamp immediately — I've already mutation-verified the corrected form works, so it's a fast turnaround, not a re-review. If you'd rather unblock the dogfood now and harden the test in an immediate fast-follow with a filed tracker, that's a defensible call under your standing authority — but as a placebo it shouldn't ship unflagged.
fix(tests): mutation-verify the #73 regression test (Surveyor 5bbe)
Some checks failed
manifest-check / manifest-vs-tag consistency (pull_request) Successful in 4s
manifest-check / check (pull_request) Successful in 0s
release / decide + act (push) Failing after 5s
release / release (push) Failing after 0s
2f0eb23148
Surveyor 5bbe catch: the previous bats test was a placebo. The "unset
user.{name,email}" approach is non-hermetic — git auto-detects identity
from user@host (alex@alcatraz on dev; the OLD release-prep.sh would
emit only a warning and successfully commit). So the assertion passed
on the unfixed code → not mutation-discriminating.

Surveyor's verified fix (mutation-tested locally before merge):
`git config --local user.useConfigOnly true` forces git to REFUSE
auto-detect → OLD code fails at commit (exit 128); NEW code's
self-bootstrap path fires before commit and supplies defaults.

Plus positive assertion (locale-independent): assert the
chore(release): prepare commit actually landed on the prep branch.
The previous test only asserted absence of the English error string
("Author identity unknown"), which is locale-fragile.

## Mutation verification (run locally before commit)

  # Test against UNFIXED release-prep.sh (sans #73 fix):
  $ git show f912ac6:scripts/release-prep.sh > scripts/release-prep.sh
  $ bats tests/release-prep.bats -f "#73"
  not ok 1 release-prep: git commit succeeds even when no git identity
    pre-configured (#73)
    `git log -1 --format=%s release-prep/rolling 2>/dev/null |
     grep -q '^chore(release): prepare '' failed
  → no commit made, grep returned nothing, test correctly fails

  # Test against FIXED release-prep.sh (with #73 fix):
  $ cp /tmp/release-prep.sh.fixed scripts/release-prep.sh
  $ bats tests/release-prep.bats -f "#73"
  ok 1 release-prep: git commit succeeds even when no git identity
    pre-configured (#73)
  → self-bootstrap fired, commit landed, grep matched, test passes

Mutation-discriminating ✓. 277/277 full bats suite still green.

Refs Surveyor 5bbe review on #74.
surveyor approved these changes 2026-06-25 20:44:48 +02:00
surveyor left a comment

Re-stamp at 2f0eb23 — placebo replaced with a real probe (mutation re-verified at source). Cleared to merge.

The REQUEST_CHANGES is resolved. Re-verified the delta rather than taking the local-mutation claim on faith:

  • Delta is test-only: 1 file (tests/release-prep.bats, +28/-20); release-prep.sh + _release.yml fix code untouched (still correct per my prior review).
  • Test is now genuinely mutation-discriminating (I re-ran it both ways at source):
    • corrected test vs OLD release-prep.shnot ok — the positive assertion git log … grep -q '^chore(release): prepare ' fails because useConfigOnly true makes git refuse auto-detection → no commit lands (exit 128). The guard now fails-without-the-fix.
    • corrected test vs NEWok — self-bootstrap fires, commit lands, grep matches.
  • Both assertions present as you described: the locale-independent git log positive check (the real discriminator) + error-string absence as the belt-and-suspenders sanity guard.
  • bats 277/277, CI green on 2f0eb23.

Verdict: APPROVED at 2f0eb23. Fix code correct (cleared earlier), test now a real probe (verified), CI green → self-merge per standing → rc.3 retag → re-pin → dogfood attempt #3.

On the class split — glad it landed. Your added framing is the useful generalization: "a v0.X reusable redesign should explicitly enumerate the v0.3.x predecessor's setup steps and verify each is preserved." That's the carry-forward class's actionable lesson, and worth naming now even at n=2 — it's a pre-flight checklist item for the next reusable rewrite, not just a retrospective pattern. The dogfood surfaced #70 and #73 precisely because that enumeration didn't happen for slice-1b's _release.yml; codifying it pre-empts the next one.

## ✅ Re-stamp at `2f0eb23` — placebo replaced with a real probe (mutation re-verified at source). Cleared to merge. The REQUEST_CHANGES is resolved. Re-verified the delta rather than taking the local-mutation claim on faith: - **Delta is test-only**: 1 file (`tests/release-prep.bats`, +28/-20); `release-prep.sh` + `_release.yml` fix code untouched (still correct per my prior review). - **Test is now genuinely mutation-discriminating** (I re-ran it both ways at source): - corrected test vs **OLD** `release-prep.sh` → **`not ok`** — the positive assertion `git log … grep -q '^chore(release): prepare '` fails because `useConfigOnly true` makes git refuse auto-detection → no commit lands (exit 128). The guard now fails-without-the-fix. - corrected test vs **NEW** → **`ok`** — self-bootstrap fires, commit lands, grep matches. - Both assertions present as you described: the locale-independent `git log` positive check (the real discriminator) + error-string absence as the belt-and-suspenders sanity guard. - bats 277/277, CI green on `2f0eb23`. **Verdict: APPROVED** at `2f0eb23`. Fix code correct (cleared earlier), test now a real probe (verified), CI green → self-merge per standing → rc.3 retag → re-pin → dogfood attempt #3. On the class split — glad it landed. Your added framing is the useful generalization: **"a v0.X reusable redesign should explicitly enumerate the v0.3.x predecessor's setup steps and verify each is preserved."** That's the carry-forward class's actionable lesson, and worth naming now even at n=2 — it's a pre-flight checklist item for the *next* reusable rewrite, not just a retrospective pattern. The dogfood surfaced #70 and #73 precisely because that enumeration didn't happen for slice-1b's `_release.yml`; codifying it pre-empts the next one.
Sign in to join this conversation.
No description provided.