fix: facts.sh reads a parentless head as base_ver=(none), not exit 128 #136

Merged
claude-bot-andresmgsl merged 3 commits from build/134-facts-root-commit into main 2026-07-24 12:47:34 +00:00
claude-bot-andresmgsl commented 2026-07-24 12:14:30 +00:00 (Migrated from github.com)

Closes #134.

A repository's first push to main is a branch-create push (event.before all-zeros) whose head is a root commit — MERGE_SHA^1 does not exist, rev-parse fails, and set -e killed lib/facts.sh at exit 128 before a single fact was established. Both 0.2.0 drills hit it independently. Per the issue's decided spec: the head's parent count is read as a fact (git rev-list --parents -n 1, D2), a zero-parent head takes the existing base_ver="(none)" path (D1) with the belt-and-braces fetch and the base git show skipped so no rev-scoped command ever runs with an empty rev (D3), and the comments carry the 0.2.0-drill war story (D4). An unresolvable MERGE_SHA still dies loudly — the parents= assignment propagates rev-list's 128 under set -e, never masquerading as (none).

Acceptance criteria (from #134)

  • facts.sh with EVENT_BEFORE all-zeros against a root commit exits 0 and prints base_ver=(none)test/facts.test.sh "root commit, all-zeros event.before"
  • Same, with EVENT_BEFORE empty (the non-push caller) — "root commit, empty event.before"
  • A root commit whose VERSION is 0.1.0-dev produces ceremony=no and a green NOTICE through the full facts→decide chain — test/release-chain.test.sh greenfield cases
  • A root commit whose VERSION is bare 0.1.0 with no merged release-labeled PR still refuses (exit 1) and creates nothing — release-chain "a bare unlabeled root commit still refuses"
  • An unresolvable MERGE_SHA still fails loudly (exit 128) and is not reported as base_ver=(none) — the D2 pin, both the positive (dies with "bad object") and the negative (no (none) in output) assertions
  • No git show, git cat-file or git fetch runs with an empty rev on the no-base path — both are behind [ -n "$base_sha" ]; the greenfield fixtures run with the refuse-by-default gh stub and no origin, so a stray fetch/show would surface
  • Every existing test/facts.test.sh case passes unchanged — including L96–L99, all-zeros/empty event.before on a parented head still resolve to the first parent
  • test/run.sh (17 files), CI shellcheck sweep (shellcheck -x, 37 scripts), actionlint — all green locally

Worklog

  • lib/facts.sh: parent count read via rev-list --parents; zero parents → (none) path; fetch + base show skipped on no-base; comments name the 0.2.0 drills and #134 (D1–D4)
  • test/facts.test.sh: greenfield fixtures (zeros + empty event.before, bare root with labeled-no), D2 pin (bad MERGE_SHA → 128, no (none) in output)
  • test/release-chain.test.sh: end-to-end — greenfield -dev root goes green ceremony=no with a NOTICE; bare unlabeled root refuses exit 1
  • changelog.d/134.md
  • Full suite + shellcheck + actionlint green locally
  • Mark ready, request the panel — codex, grok, kimi requested
Closes #134. A repository's first push to `main` is a branch-create push (`event.before` all-zeros) whose head is a root commit — `MERGE_SHA^1` does not exist, `rev-parse` fails, and `set -e` killed `lib/facts.sh` at exit 128 before a single fact was established. Both 0.2.0 drills hit it independently. Per the issue's decided spec: the head's parent count is read as a fact (`git rev-list --parents -n 1`, D2), a zero-parent head takes the existing `base_ver="(none)"` path (D1) with the belt-and-braces fetch and the base `git show` skipped so no rev-scoped command ever runs with an empty rev (D3), and the comments carry the 0.2.0-drill war story (D4). An unresolvable `MERGE_SHA` still dies loudly — the `parents=` assignment propagates `rev-list`'s 128 under `set -e`, never masquerading as `(none)`. ## Acceptance criteria (from #134) - [x] `facts.sh` with `EVENT_BEFORE` all-zeros against a root commit exits `0` and prints `base_ver=(none)` — `test/facts.test.sh` "root commit, all-zeros event.before" - [x] Same, with `EVENT_BEFORE` empty (the non-push caller) — "root commit, empty event.before" - [x] A root commit whose `VERSION` is `0.1.0-dev` produces `ceremony=no` and a green NOTICE through the full facts→decide chain — `test/release-chain.test.sh` greenfield cases - [x] A root commit whose `VERSION` is bare `0.1.0` with no merged release-labeled PR still refuses (exit 1) and creates nothing — release-chain "a bare unlabeled root commit still refuses" - [x] An unresolvable `MERGE_SHA` still fails loudly (exit 128) and is not reported as `base_ver=(none)` — the D2 pin, both the positive (dies with "bad object") and the negative (no `(none)` in output) assertions - [x] No `git show`, `git cat-file` or `git fetch` runs with an empty rev on the no-base path — both are behind `[ -n "$base_sha" ]`; the greenfield fixtures run with the refuse-by-default gh stub and no `origin`, so a stray fetch/show would surface - [x] Every existing `test/facts.test.sh` case passes unchanged — including L96–L99, all-zeros/empty `event.before` on a parented head still resolve to the first parent - [x] `test/run.sh` (17 files), CI shellcheck sweep (`shellcheck -x`, 37 scripts), actionlint — all green locally ## Worklog - [x] `lib/facts.sh`: parent count read via `rev-list --parents`; zero parents → `(none)` path; fetch + base show skipped on no-base; comments name the 0.2.0 drills and #134 (D1–D4) - [x] `test/facts.test.sh`: greenfield fixtures (zeros + empty `event.before`, bare root with `labeled-no`), D2 pin (bad `MERGE_SHA` → 128, no `(none)` in output) - [x] `test/release-chain.test.sh`: end-to-end — greenfield `-dev` root goes green `ceremony=no` with a NOTICE; bare unlabeled root refuses exit 1 - [x] `changelog.d/134.md` - [x] Full suite + shellcheck + actionlint green locally - [x] Mark ready, request the panel — codex, grok, kimi requested
danmt (Migrated from github.com) reviewed 2026-07-24 12:14:30 +00:00
codex-bot-andresmgsl commented 2026-07-24 12:20:41 +00:00 (Migrated from github.com)

🔎 reviewing head fa768510bf

🔎 reviewing head fa768510bff3624f40716b53c115968271015ae1
grok-bot-andresmgsl commented 2026-07-24 12:20:59 +00:00 (Migrated from github.com)

🔎 reviewing head fa768510bf

🔎 reviewing head fa768510bff3624f40716b53c115968271015ae1
codex-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-24 12:22:14 +00:00
codex-bot-andresmgsl (Migrated from github.com) left a comment

All acceptance criteria from #134 are satisfied on fa768510bff3624f40716b53c115968271015ae1.

The implementation detects a root commit from a successful git rev-list --parents result, preserves a loud exit 128 for an unresolvable MERGE_SHA, and guards every base-scoped cat-file, fetch, and git show behind a non-empty base_sha. The tests cover all-zeros and empty EVENT_BEFORE, the parented-head fallback, the green -dev facts→decide path, the bare unlabeled refusal, and the invalid-head negative case. The changelog fragment and incident-linked comments are present.

Verified locally:

  • test/run.sh: 17 test files passed
  • .github/scripts/shellcheck-all.sh: 37 tracked scripts clean
  • .github/scripts/actionlint-all.sh: 6 workflow files clean
  • git diff --check: clean

Current GitHub checks are also green; the two release-on-event jobs are expectedly skipped for this PR.

All acceptance criteria from #134 are satisfied on `fa768510bff3624f40716b53c115968271015ae1`. The implementation detects a root commit from a successful `git rev-list --parents` result, preserves a loud exit 128 for an unresolvable `MERGE_SHA`, and guards every base-scoped `cat-file`, fetch, and `git show` behind a non-empty `base_sha`. The tests cover all-zeros and empty `EVENT_BEFORE`, the parented-head fallback, the green `-dev` facts→decide path, the bare unlabeled refusal, and the invalid-head negative case. The changelog fragment and incident-linked comments are present. Verified locally: - `test/run.sh`: 17 test files passed - `.github/scripts/shellcheck-all.sh`: 37 tracked scripts clean - `.github/scripts/actionlint-all.sh`: 6 workflow files clean - `git diff --check`: clean Current GitHub checks are also green; the two release-on-event jobs are expectedly skipped for this PR.
grok-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-24 12:22:58 +00:00
grok-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: approve

Reviewed head fa768510bff3624f40716b53c115968271015ae1 against #134.

Acceptance criteria

Criterion Result
Root commit + all-zeros event.before → exit 0, base_ver=(none) pass — unit + manual repro
Root commit + empty event.before → same pass
Root -dev → facts→decide green NOTICE, ceremony=no passrelease-chain.test.sh
Root bare unlabeled → refuse exit 1, creates nothing pass
Unresolvable MERGE_SHA dies loudly (128), never (none) pass — D2 pin positive and negative
No git show / fetch with empty rev on no-base path pass — both behind [ -n "$base_sha" ]; greenfield fixtures use refuse-by-default gh stub
Existing parented all-zeros / empty fallbacks still resolve to first parent pass — prior cases unchanged; manual parented repro ok
Suite + shellcheck pass — 17/17 test files; shellcheck -x lib/facts.sh clean; CI green

Spec fidelity (D1–D4)

  • D1: zero-parent head takes the existing (none) path; decide's table unchanged.
  • D2: parent count via git rev-list --parents -n 1 — fact, not || true on a failed rev-parse. Bad SHA still propagates exit 128 under set -e.
  • D3: belt-and-braces fetch and base git show gated on non-empty base_sha — avoids the index-read wrong-fact trap the issue names.
  • D4: comments carry the 0.2.0-drill / #134 war story.

Diff

Minimal and correct: lib/facts.sh, the two test files with greenfield + D2 fixtures, and changelog.d/134.md. No behavior outside the crash path changes.

Approve.

## Verdict: approve Reviewed head `fa768510bff3624f40716b53c115968271015ae1` against #134. ### Acceptance criteria | Criterion | Result | |---|---| | Root commit + all-zeros `event.before` → exit 0, `base_ver=(none)` | **pass** — unit + manual repro | | Root commit + empty `event.before` → same | **pass** | | Root `-dev` → facts→decide green NOTICE, `ceremony=no` | **pass** — `release-chain.test.sh` | | Root bare unlabeled → refuse exit 1, creates nothing | **pass** | | Unresolvable `MERGE_SHA` dies loudly (128), never `(none)` | **pass** — D2 pin positive and negative | | No `git show` / fetch with empty rev on no-base path | **pass** — both behind `[ -n "$base_sha" ]`; greenfield fixtures use refuse-by-default gh stub | | Existing parented all-zeros / empty fallbacks still resolve to first parent | **pass** — prior cases unchanged; manual parented repro ok | | Suite + shellcheck | **pass** — 17/17 test files; `shellcheck -x lib/facts.sh` clean; CI green | ### Spec fidelity (D1–D4) - **D1**: zero-parent head takes the existing `(none)` path; decide's table unchanged. - **D2**: parent count via `git rev-list --parents -n 1` — fact, not `|| true` on a failed rev-parse. Bad SHA still propagates exit 128 under `set -e`. - **D3**: belt-and-braces fetch and base `git show` gated on non-empty `base_sha` — avoids the index-read wrong-fact trap the issue names. - **D4**: comments carry the 0.2.0-drill / #134 war story. ### Diff Minimal and correct: `lib/facts.sh`, the two test files with greenfield + D2 fixtures, and `changelog.d/134.md`. No behavior outside the crash path changes. Approve.
kimi-bot-andresmgsl commented 2026-07-24 12:35:35 +00:00 (Migrated from github.com)

🔎 reviewing head fa768510bf

🔎 reviewing head fa768510bff3624f40716b53c115968271015ae1
kimi-bot-andresmgsl (Migrated from github.com) approved these changes 2026-07-24 12:36:13 +00:00
kimi-bot-andresmgsl (Migrated from github.com) left a comment

Verdict: approve

Reviewed head fa768510bff3624f40716b53c115968271015ae1 against #134, in a detached worktree.

Acceptance criteria

Criterion Result
Root commit + all-zeros event.before → exit 0, base_ver=(none) pass — unit test plus my own manual repro on a fresh root-commit fixture
Root commit + empty event.before → same pass
Root -devceremony=no green NOTICE through facts→decide passrelease-chain.test.sh greenfield cases, 8/8
Root bare 0.1.0 unlabeled → refuse exit 1, creates nothing pass — e2e with refuse-by-default gh stub
Unresolvable MERGE_SHA dies loudly, never reported as (none) pass — D2 pin asserts both exit 128 "bad object" and the absence of base_ver=(none); reproduced manually (grep count 0)
No git show / cat-file / fetch with an empty rev on the no-base path pass — belt-and-braces fetch and base git show both behind [ -n "$base_sha" ]; the index-read hazard from D3 is commented
Existing facts tests pass unchanged pass — 24 pass; the only 2 failures are package-json: cases needing node, absent in this environment and green in CI
Suite + shellcheck + actionlint pass — same node-only local exceptions; CI fully green at this head (test, self-guards, release-exercise, action-exercise, docs-sync-exercise)

Spec fidelity (D1–D4)

  • D1: a parentless head takes the existing base_ver=(none) path; decide's table is untouched — a -dev root is the green bootstrap, a bare root still refuses.
  • D2: parent count read as a fact via git rev-list --parents -n 1; under set -e an unresolvable head still propagates the 128 — detection, not inference from a failed command. The *" "* split is the right discriminator.
  • D3: the no-base path runs no rev-scoped command with an empty rev; the comment names the index-read trap (git show ":VERSION"), which is the reason this invariant matters.
  • D4: comments carry the 0.2.0-drill war story and #134, per CONTRIBUTING.

Conventions

  • One fragment changelog.d/134.md, published prose only; no CHANGELOG.md edit.
  • The head's retrigger commit is empty; the change itself is two commits (fix + tests).

Notes (non-blocking)

  • None blocking. The diff is exactly the scoped fix plus its tests — no drive-by.

Meets every #134 criterion. Approve.

## Verdict: approve Reviewed head `fa768510bff3624f40716b53c115968271015ae1` against #134, in a detached worktree. ### Acceptance criteria | Criterion | Result | |---|---| | Root commit + all-zeros `event.before` → exit 0, `base_ver=(none)` | **pass** — unit test plus my own manual repro on a fresh root-commit fixture | | Root commit + empty `event.before` → same | **pass** | | Root `-dev` → `ceremony=no` green NOTICE through facts→decide | **pass** — `release-chain.test.sh` greenfield cases, 8/8 | | Root bare `0.1.0` unlabeled → refuse exit 1, creates nothing | **pass** — e2e with refuse-by-default gh stub | | Unresolvable `MERGE_SHA` dies loudly, never reported as `(none)` | **pass** — D2 pin asserts both exit 128 "bad object" and the absence of `base_ver=(none)`; reproduced manually (grep count 0) | | No `git show` / `cat-file` / `fetch` with an empty rev on the no-base path | **pass** — belt-and-braces fetch and base `git show` both behind `[ -n "$base_sha" ]`; the index-read hazard from D3 is commented | | Existing facts tests pass unchanged | **pass** — 24 pass; the only 2 failures are `package-json:` cases needing `node`, absent in this environment and green in CI | | Suite + shellcheck + actionlint | **pass** — same node-only local exceptions; CI fully green at this head (test, self-guards, release-exercise, action-exercise, docs-sync-exercise) | ### Spec fidelity (D1–D4) - **D1**: a parentless head takes the existing `base_ver=(none)` path; `decide`'s table is untouched — a `-dev` root is the green bootstrap, a bare root still refuses. - **D2**: parent count read as a fact via `git rev-list --parents -n 1`; under `set -e` an unresolvable head still propagates the 128 — detection, not inference from a failed command. The `*" "*` split is the right discriminator. - **D3**: the no-base path runs no rev-scoped command with an empty rev; the comment names the index-read trap (`git show ":VERSION"`), which is the reason this invariant matters. - **D4**: comments carry the 0.2.0-drill war story and #134, per CONTRIBUTING. ### Conventions - One fragment `changelog.d/134.md`, published prose only; no `CHANGELOG.md` edit. - The head's retrigger commit is empty; the change itself is two commits (fix + tests). ### Notes (non-blocking) - None blocking. The diff is exactly the scoped fix plus its tests — no drive-by. Meets every #134 criterion. Approve.
claude-bot-andresmgsl commented 2026-07-24 12:37:49 +00:00 (Migrated from github.com)

Handoff — round converged, over to @danmt

What shipped. lib/facts.sh no longer dies at exit 128 when the merge head is a root commit — the shape both 0.2.0 drills hit on a repository's first push to main (event.before all-zeros, MERGE_SHA^1 nonexistent). Per #134's decided spec: the head's parent count is read as a fact via git rev-list --parents -n 1 (D2); a zero-parent head takes the existing base_ver="(none)" path (D1) with the belt-and-braces fetch and the base git show skipped, so no rev-scoped command ever runs with an empty rev (D3); the comments carry the 0.2.0-drill war story (D4). An unresolvable MERGE_SHA still fails loudly at 128 — it never masquerades as (none).

Rounds. One. The panel (codex, grok, kimi) was requested at 12:16Z on head fa76851; all three approved that head with no changes requested, so the round converged whole on the first pass — no post-review fix commits, and the reviewed head is the merge head.

What was verified.

  • All 8 acceptance criteria from #134, checked in the PR body and independently confirmed by each reviewer; grok and kimi additionally reproduced the greenfield fixtures by hand (kimi in a detached worktree).
  • New coverage: test/facts.test.sh greenfield fixtures (all-zeros and empty event.before, bare root with labeled-no) plus the D2 loud-death pin (bad MERGE_SHA → 128, no (none) in output); test/release-chain.test.sh end-to-end greenfield cases (root -dev goes green ceremony=no with a NOTICE; bare unlabeled root still refuses exit 1). All pre-existing facts.test.sh cases pass unchanged.
  • CI fully green on fa76851: test suite (17 files), shellcheck sweep (37 scripts), actionlint, labels/scope, labels/reconcile, release-exercise (fixture-chain + both step-replays), self-guards, action-exercise, docs-sync-exercise.

Residue / notes.

  • The brief blocker:ci-red at 12:17Z was false — a queued duplicate labels/reconcile run was cancelled and read as red (same shape as #138); the empty-commit retrigger fa76851 cleared it and became the reviewed head. No underlying red ever existed.
  • No post-merge steps owed. The change is self-contained; changelog.d/134.md publishes with the next release PR's assembly.
  • The reconciler validated the convergence at 12:36Z and applied state:needs-human + the review request itself, so this summary completes the handoff.

This PR could be merged right now. The merge is yours.

## Handoff — round converged, over to @danmt **What shipped.** `lib/facts.sh` no longer dies at exit 128 when the merge head is a root commit — the shape both 0.2.0 drills hit on a repository's first push to `main` (`event.before` all-zeros, `MERGE_SHA^1` nonexistent). Per #134's decided spec: the head's parent count is read as a fact via `git rev-list --parents -n 1` (D2); a zero-parent head takes the existing `base_ver="(none)"` path (D1) with the belt-and-braces fetch and the base `git show` skipped, so no rev-scoped command ever runs with an empty rev (D3); the comments carry the 0.2.0-drill war story (D4). An unresolvable `MERGE_SHA` still fails loudly at 128 — it never masquerades as `(none)`. **Rounds.** One. The panel (codex, grok, kimi) was requested at 12:16Z on head `fa76851`; all three approved that head with no changes requested, so the round converged whole on the first pass — no post-review fix commits, and the reviewed head is the merge head. **What was verified.** - All 8 acceptance criteria from #134, checked in the PR body and independently confirmed by each reviewer; grok and kimi additionally reproduced the greenfield fixtures by hand (kimi in a detached worktree). - New coverage: `test/facts.test.sh` greenfield fixtures (all-zeros and empty `event.before`, bare root with `labeled-no`) plus the D2 loud-death pin (bad `MERGE_SHA` → 128, no `(none)` in output); `test/release-chain.test.sh` end-to-end greenfield cases (root `-dev` goes green `ceremony=no` with a NOTICE; bare unlabeled root still refuses exit 1). All pre-existing `facts.test.sh` cases pass unchanged. - CI fully green on `fa76851`: test suite (17 files), shellcheck sweep (37 scripts), actionlint, labels/scope, labels/reconcile, release-exercise (fixture-chain + both step-replays), self-guards, action-exercise, docs-sync-exercise. **Residue / notes.** - The brief `blocker:ci-red` at 12:17Z was false — a queued duplicate `labels/reconcile` run was cancelled and read as red (same shape as #138); the empty-commit retrigger `fa76851` cleared it and became the reviewed head. No underlying red ever existed. - No post-merge steps owed. The change is self-contained; `changelog.d/134.md` publishes with the next release PR's assembly. - The reconciler validated the convergence at 12:36Z and applied `state:needs-human` + the review request itself, so this summary completes the handoff. This PR could be merged right now. The merge is yours.
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: heavy-duty/ceremony#136
No description provided.