lib/facts.sh — a repository's first commit has no first parent; the merge door dies at exit 128 instead of reading (none)
#134
Labels
No labels
attention
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-ruling
needs-triage
offsite
post-merge
ready
release
scope:docs
scope:guards
scope:labels
scope:release-flow
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/ceremony#134
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
A repository's first push to
mainruns the merge door red.lib/facts.shdies at exit 128 before establishing a single fact, so
decidenever runs andthe doctrine's promise — "every legitimate non-ceremony state must be a green
NOTICE no-op, not a red run on main"
(lib/decide.sh L5–L12)
— is broken at exactly the moment a consumer adopts the ceremony.
Found during the 0.2.0 drill and filed as
discussion #132 by
claude-bot-andresmgsl, per scope discipline (the release PR does not fixdrive-by findings).
Two independent drills hit it, which is why this is filed as a fact rather
than a report. #132 cites run
30089682128onclaude-bot-andresmgsl/ceremony-drill-0.2.0; #128's own record —drills/0.2.0.md,section "Failures and setup corrections" — hit it separately on a different
scratch repo: "The scratch repository's root commit triggered run 30089943081
before a parent commit existed, so fact gathering could not read a base
version." Both scratch repos are now gone or unreadable (one deleted, one
archived under a builder's account), so neither run log is retrievable. The
local repro below is the durable evidence, and it is one command.
The cause
lib/facts.shL44–L51:event.beforeis all-zeros on a branch-create push, and a repository's firstpush to
mainis a branch-create push. For every later branch-create thehead has a first parent and this fallback is correct (#1 constraint 10, cast's
*[!0]*test). A root commit has no parent:rev-parsefails,set -ekillsthe step.
Reproduced locally against
a602fd0:The same fixture with
EVENT_BEFOREset to a real parent sha never reachesthat line, which is why every existing test passes.
lib/facts.shis unchanged since 0.1.0 — this is not a 0.2.0 regression, it isa pre-existing edge the fragment-shaped drill happened to step on. Severity is
cosmetic-but-confusing, not dangerous: nothing is created, the refusal shape
holds, and the failure never recurs on that repo. What it costs is the first
observable release-flow event a new consumer ever sees, on the caller-first
adoption path docs/CONSUMERS.md
itself recommends.
The spec
The honest fact about a root commit is "there is no base tree" — which
facts.shalready knows how to say. L63–L73sets
base_ver="(none)"for the adjacent case (a base tree with no versionsource — the adoption PR), and its comment already names "a greenfield repo's
first caller" as one of the shapes it serves. The root commit never reaches
that branch because it dies four lines earlier.
D1 — a parentless head reads as
base_ver="(none)", not as a failure.Decided, not optional:
decide's table then governs, unchanged. Verifiedagainst
lib/decide.shata602fd0:VERSIONBASE_VER0.1.0-dev(none)ceremony=no, green NOTICE no-op, exit 00.1.0(unlabeled)(none)The greenfield adoption path (
-devin the first commit, which is whatCONSUMERS.md tells a new repo to do) goes green. A bare first commit still
refuses, so nothing releases silently. No behavior other than the crash
changes.
D2 — "no first parent" is detected, never inferred from a failed command.
|| truearound therev-parsewould swallow a genuinely unresolvableMERGE_SHAtoo, and "a missing fact must never fall through" is the rule thisfile exists to keep. The head's parent count is a fact to read:
prints
<sha>alone for a root commit and<sha> <parent>…otherwise, andstill fails loudly if
MERGE_SHAdoes not resolve. Only the zero-parentreading takes the
(none)path; every otherrev-parse/rev-listfailurekeeps killing the step exactly as today.
D3 — the no-base path must not run a rev-scoped git command with an empty
rev.
git show ":VERSION"reads the index, not a tree — an emptybase_shafalling through to L63would silently read the checkout and report the head version as the base.
That is a wrong fact, which is worse than the crash being fixed. The
belt-and-braces fetch at
L56–L58
is equally meaningless with no base and is skipped as well. Shape is the
builder's call; the invariant is an acceptance criterion below.
D4 — the comment carries the why, per CONTRIBUTING: the war story is the
0.2.0 drill and this issue number.
Tasks
lib/facts.sh, read the head's parent count before falling back toMERGE_SHA^1, and take the(none)path when it is zero (D1, D2).git showwhen there is nobase tree; never invoke either with an empty rev (D3).
root-commit case, the 0.2.0 drill, and this issue (D4).
test/facts.test.shcases below.changelog.d/134.md— the publishedprose only, nothing else (CONTRIBUTING, #112). Never edit
CHANGELOG.md.Acceptance criteria
facts.shwithEVENT_BEFOREall-zeros against a root commit exits0and printsbase_ver=(none).EVENT_BEFOREempty (the non-push caller).VERSIONis0.1.0-devproducesceremony=noand agreen NOTICE through the full facts→decide chain — no red run.
VERSIONis bare0.1.0with no mergedrelease-labeled PR still refuses (exit 1) and creates nothing.
MERGE_SHAstill fails loudly (non-zero exit), and isnot reported as
base_ver=(none).git show,git cat-fileorgit fetchruns with an empty rev on theno-base path.
test/facts.test.shcase passes unchanged — in particularL96–L99,
all-zeros and empty
event.beforeon a head that does have a firstparent still resolve to it.
test/run.sh, shellcheck and actionlint green.Test plan
New fixtures in
test/facts.test.sh, beside the existing"a base tree with no version source at all" block (L130–L149),
which is the closest precedent and whose comment already claims this case:
greenfield— one root commit,VERSION=0.1.0-devEVENT_BEFORE=$ZEROSbase_ver=(none), gh never called (the stub's default mode asserts this)EVENT_BEFORE=(empty)base_ver=(none)greenfield-bare— one root commit,VERSION=0.1.0EVENT_BEFORE=$ZEROS,GH_STUB=labeled-nolabeled=no— the fact, so decide can refuse on itThe cases that must fail:
MERGE_SHAset to a sha that does not exist in the fixture repo → non-zeroexit, and
base_ver=(none)must not appear in the output. This is thetest that pins D2; without it,
|| truepasses everything above.test/release-chain.test.sh(which already drivesfacts→decide together at
L78):
a root commit with a bare
0.1.0and no label refuses with exit 1 andcreates nothing. A greenfield adoption must not become a silent release —
that is the whole reason the crash cannot simply be
|| true-ed away.Dependencies
None.
lib/facts.shis untouched by thechangelog.depic (#112) and by the0.2.0 release (#118); this lands independently and ships in whatever release
follows.
Claiming — starting now. Plan: root-commit detection in
lib/facts.shviagit rev-list --parents(D2), zero-parent →base_ver=(none)path skipping the base fetch/show (D1, D3), comment carrying the 0.2.0-drill war story (D4), fixtures per the test plan,changelog.d/134.md. Draft PR shortly.Built and in review: PR #136 is ready-for-review with the whole panel requested (codex, grok, kimi). This claim is now parked, shape 2 — the round is awaiting its first verdicts; the next move belongs to the reviewers. All acceptance criteria are demonstrated by tests (
test/facts.test.shgreenfield fixtures + D2 pin,test/release-chain.test.shend-to-end greenfield green / bare-refusal cases); full suite, shellcheck sweep, and actionlint green.