Skip to content

feat: the release gate scaffold wires a managed repo's own CI and binds the receipts to the release they gate - #716

Merged
REPPL merged 31 commits into
mainfrom
feat/release-gate-scaffold-wiring
Sep 26, 2026
Merged

REPPL merged 31 commits into
mainfrom
feat/release-gate-scaffold-wiring

Conversation

@REPPL

@REPPL REPPL commented Sep 26, 2026

Copy link
Copy Markdown
Collaborator

With this change, abcd launch scaffold wires a managed repository's release gate to its own CI checks and writes its reviews charter. A scaffolded repository can now take its first release through the merge path to a published release. The release job's receipt gate is bound to the release it gates. With this, itd-93 ships.

  • The scaffold reads the repository's own CI checks and wires them in. It writes the reviews charter, and it reports "no CI checks" as []. The scaffold's workflow audit is described as what it is: not a full zizmor run.
  • launch runs the release job's receipt gate locally. Its emit step ends with the receipts protocol, which says the release publishes from the tagged merge and asks for a full sha.
  • Release-gate fix (major, security; iss-2609251755386183): the receipt gate used to accept a release that had no receipts of its own. The derivation took the nearest receipts directory, which could be the previous release's. The derived content commit must now carry the released tree's newest dated CHANGELOG version. A tree with no readable version refuses. So does an unreadable newest heading. The version filter runs before "nearest", and receipt directories require a full sha.
  • The pushed tag is bound to the released CHANGELOG version. record-lint --released-version prints the version, and it fails closed. In scaffolded profiles, a verify step compares the tag against that version before the receipts step runs. A mismatched tag, -rc, +build, a four-part version, an Unreleased-only tree or a missing CHANGELOG are all refused, and nothing publishes. abcd's own release.yml carries the step through template parity, after its archive step.
  • The runbooks list the tag-binding gate, and a lockstep test holds the gate lists in step.

Open and deferred out loud: iss-2609252024442310, a minor finding. Receipts that name a post-roll CHANGELOG revision are still admitted, and sharpening that waits on a protocol ruling (S). The record carries deferred_after: v0.10.0 and its reason.
Pre-existing residuals noted by the review: verify does not check that the tagged sha is on the default branch, and there is no tag ruleset, so the release environment reviewer is the stop.

Review: Fable 5.1, three rounds. Rounds 1 and 2 asked for fixes (eight findings, then the tag binding). Round 3 verified: SHIP. Reading-corpus change. It lands after the launch gate suite (itd-65) and the payload diff (itd-66).

Resolves: iss-2609251751298408
Resolves: iss-2609251755386183
Resolves: iss-2609251939460232
Resolves: iss-2609251939461459
Resolves: iss-2609251939466588
Resolves: iss-2609251939468296
Resolves: iss-2609251939476304
Resolves: iss-2609251945586202
Resolves: iss-2609252029191920
Refs: iss-2609252024442310
Delivers: itd-93
Assisted-by: Claude:claude-opus-5-5

The derived content commit is not bound to the release it gates, so an
earlier release's receipts admit a later release that recorded none; and
commands/launch.md describes the receipt gate at a position it no longer
holds (after the tag, deriving <merge>^2^).

Refs: iss-2609251755386183, iss-2609251751298408

Assisted-by: Claude:claude-opus-5-5
…gates

record-lint --derive-content-sha armed the semantic receipt gate against
the nearest commit on the released lineage that a receipts directory
names, and never checked that the commit belonged to this release. A roll
that recorded no receipts of its own derived the previous release's cut,
whose PROMOTE receipts are valid, and the gate admitted a release nobody
reviewed (reproduced on a scratch clone at the v0.10.0 tip).

The derivation now requires the derived commit to carry the released
tree's own newest dated CHANGELOG version, read from each commit's blob,
and fails closed naming both versions otherwise. The rehearsal and the
batched-queue shape are unaffected: both compare equal versions.

Refs: iss-2609251755386183

Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2609251755386183

Assisted-by: Claude:claude-opus-5-5
…emit step with its protocol

itd-93 AC7 and AC8 (spc-2609230613193436).

`abcd launch receipts` is the release job's semantic-receipt gate, run on
the release branch before the merge. It shares the job's reader rather
than modelling it: lint.CheckReleaseReceipts reads the required gates from
the committed release.yml (the list the job enforces), derives the content
commit with DeriveReleaseContentSha, arms with ArmReceiptGate and judges
with checkReceiptGate — the three functions record-lint runs. It names
each missing or non-PROMOTE receipt and the commit it must name, refuses
on an uncommitted receipt change (the job reads the committed tree), and
exits 0/1/2. A test runs the workflow's own step, lifted from the
committed release.yml against a record-lint built from this tree, beside
the verb over five repository states and holds them to one verdict and
one set of reasons.

The emit step of `abcd launch ship` ends with the receipts protocol as a
numbered checklist, composed in core (release.ReceiptsProtocolFor) from
the same workflow list and carried in --json as receipts_protocol; a
workflow that arms no gate gets a checklist that requires no receipt.

commands/launch.md documents the verb and the checklist, and its account
of where the gate runs is brought to the workflow as it is (in verify,
before the tag on the auto-release path; content commit from the
receipts directory).

Refs: iss-2609251751298408

Assisted-by: Claude:claude-opus-5-5
…te where it runs

Resolves: iss-2609251751298408

Assisted-by: Claude:claude-opus-5-5
… the reviews charter

itd-93 AC1 (the missing half) and AC5 (the missing half), spc-2609230613193436.

`launch scaffold` reads the managed repo's pull-request and merge-queue
workflows and derives the check names they report (DeriveCIChecks): a
job's own name, else its id; a matrix job, an expression-named job and a
reusable-workflow call are omitted, never guessed; every name passes an
allowlist that excludes every YAML and shell metacharacter before it is
written. The names reach the runbook's new merge-gate section (the
contexts to require on the default branch), the bare release.yml verify
header, and the report (ci_checks). abcd's own rendering is unchanged, so
self-scaffold parity holds.

The scaffold also writes .abcd/development/release-gate/check-reviews.sh,
the reviews charter (RD001): dated review directories keep their shape,
and sha-keyed receipt directories (full SHA-1 or SHA-256) are exempt. The
bare verify job runs it as deterministic gate 6, and the runbook numbers
it from the same source. The runbook's semantic-gate account and
procedure now teach the receipts-directory derivation and the two-commit
protocol with `launch receipts`.

Audit: zizmor is not installed where the tests run and is not a
dependency, so TestScaffoldedWorkflowsPassTheWorkflowAudit asserts its two
named classes with the repository's own tools over every profile — no
duplicate mapping key, no ${{ }} inside a run script, and every
expression resolving under the strict actionsexpr evaluator against a
context set no pull-request author writes.

Assisted-by: Claude:claude-opus-5-5
…ge path to a published release

itd-93 AC2 (spc-2609230613193436).

The test runs the rendered workflows themselves: a small runner reads the
jobs and steps out of the rendered YAML, evaluates every if/env/with/output
expression with the strict actionsexpr evaluator, and runs every run
script with bash in a fresh clone. Only the edge is faked — a local bare
repository is the forge's remote, a fake gh keeps releases and
attestations in a directory, checkout is a clone, setup-go is the local
toolchain, attest records subject digests, and `go run ./cmd/record-lint`
runs as a record-lint built from this tree. No real forge is contacted.

Two profiles, each: scaffold, commit, run the workflow_dispatch rehearsal
green (verify and rehearsal succeed, tag and release skipped, nothing
tagged or published), roll the CHANGELOG on a release branch (plus the
receipts commit for the semantic profile), merge --no-ff, run
auto-release. Asserted: v0.1.0 is published from the merged commit under
a tag at that commit, and with a semantic gate the receipt gate armed
against the roll — the reviewed content commit — not the tagged merge
commit, and the receipts it admitted were attested.

Watched red on a scratch copy with the verify step armed against
`git rev-parse HEAD` (the self-reference): auto-release fails and nothing
publishes.

Assisted-by: Claude:claude-opus-5-5
The remainder spec's five criteria are delivered: the scaffold wired to
the managed repo's own CI check names with an audit-clean render (AC1),
the merge path driven to a published release against a fake forge (AC2),
the scaffolded reviews charter exempting sha-keyed receipt directories
(AC5), `launch receipts` sharing the release job's reader and proven
equal to it (AC7), and the emit step ending with the receipts protocol
(AC8). spc-14 delivered the rest. itd-93 declares no impact; closed as
additive.

Delivers: itd-93

Assisted-by: Claude:claude-opus-5-5
…I checks as []

The protocol named the receipt key a 40-character sha, which a SHA-256
repository's commits are not; the gate and the charter accept both. The
scaffold's --json reported an absent CI-check list as null; it is [].

Assisted-by: Claude:claude-opus-5-5
The review of the release-gate wiring (fix round 1 of lane gatewire)
named two security holes in the content-commit derivation's version
binding, two false refusals, a wording slip and a follow-up audit.

Refs: iss-2609251939461459, iss-2609251939468296, iss-2609251939460232, iss-2609251939466588, iss-2609251939476304, iss-2609251939472371

Assisted-by: Claude:claude-opus-5-5
The content-commit derivation bound the nearest receipts directory to the
released tree's CHANGELOG version with a lenient reader, and judged the
nearest candidate only. Four ways past or into it:

- A released tree with no dated heading, or no CHANGELOG.md, read as ""
  and an earlier commit's receipts matched on "" == "". The released tree
  must now name a version, or the derivation refuses.
- A pre-release, build-metadata or undated head was skipped, so the
  binding compared against the previous release, whose PROMOTE receipts
  admitted this one. A new strict reader, changelog.ReleasedVersionIn,
  takes the newest "## [" heading other than [Unreleased] and refuses one
  the dated-heading reader does not parse.
- Candidates are filtered by version first and the nearest taken from
  those, so a co-batched pull request's own receipts directory can no
  longer tie with or shadow the roll's and wedge a correct cut.
- receiptShaRe requires a full 40- or 64-hex sha, as the charter,
  protocol and runbook say, so an abbreviated twin is a stray entry, not a
  tying candidate. This also narrows --release-gate to full shas; the
  release job passes the derived full sha.

Item 2's shape: refuse an unreadable newest heading, rather than pass
the tag in and require tag == CHANGELOG == receipts. Refusal lives in the
one reader both front doors share, so `abcd launch receipts` (which has
no tag) and the release job keep one verdict (itd-93 AC7). Passing the
tag would split them and change the pinned workflow's verify step.

The rehearsal simulated its roll as "## [0.0.0-rehearsal]", which the
gate read past, landing on the previous version. The strict reader
refuses it, so the rehearsal rolls "## [0.0.0]" in release.yml and in both
branches of the scaffold template.
TestScaffoldedGateCutsAFirstReleaseThatPublishes/semantic-gate was RED on
it and is green again.

Refs: iss-2609251939461459, iss-2609251939468296, iss-2609251939460232, iss-2609251939466588

Assisted-by: Claude:claude-opus-5-5
… refuses

Resolves: iss-2609251939461459

Assisted-by: Claude:claude-opus-5-5
…fuses

Resolves: iss-2609251939468296

Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2609251939460232

Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2609251939466588

Assisted-by: Claude:claude-opus-5-5
…he tagged merge

The roll step called the content commit 'the commit the release publishes
from'. The release publishes from the tagged merge, as the runbook's last
step says; the content commit is the one the reviewers read and every
receipt names.

Refs: iss-2609251939476304

Assisted-by: Claude:claude-opus-5-5
…erge

Resolves: iss-2609251939476304

Assisted-by: Claude:claude-opus-5-5
abcd's tests audit every rendered workflow profile for duplicate keys and
template injection, and only abcd's own workflows run under zizmor in CI.
The launch page and the scaffolded runbook now say that action pinning, job
permissions and credential handling go unchecked for the bare profile, and
tell the operator to run their own workflow auditor. Running zizmor over a
rendered bare profile in CI stays open as a follow-up.

abcd's own release-gate runbook also describes the stricter content-commit
derivation: version filter before nearest, full-sha entries, a strictly read
released heading, and the rehearsal's plain 0.0.0 roll.

Refs: iss-2609251939472371, iss-2609251939460232, iss-2609251939466588, iss-2609251939468296, iss-2609251939461459

Assisted-by: Claude:claude-opus-5-5
Found while choosing the shape of the pre-release-head fix. The shape
chosen refuses an unreadable newest heading in the reader both front doors
share, so the tag is not consulted. A hand-pushed tag naming another
version therefore still passes under the CHANGELOG version's receipts.
Closing that needs a ruling on the verify step and on the local check's
parity, so it is filed rather than fixed in this round.

Refs: iss-2609251945586202

Assisted-by: Claude:claude-opus-5-5
A hand-pushed tag naming a version other than the released tree's newest
dated CHANGELOG heading (v0.1.1, v0.2.0 or v0.1.0-rc.1 on a 0.1.0 tree)
passed a scaffolded semantic profile's verify job under the receipts of
the CHANGELOG's version, and published under a version nobody reviewed.

record-lint gains --released-version, a standalone mode that prints the
version the released tree (HEAD) names, read by the strict reader the
receipt derivation binds with (lint.ReleasedVersion over
changelog.ReleasedVersionIn), and fails closed with no output where the
derivation does. The template's verify job gains a step, in the semantic
block, before the receipts gate, guarded like the archive step
(github.event_name != 'workflow_dispatch'): it refuses unless TAG equals
"v" plus that version. The comparison is string equality in the workflow,
which holds the tag; no heading is read in bash, so no second reader
exists to drift (anchor.go). The derivation and the tag-less launch
receipts are untouched (AC7).

Chosen over --derive-content-sha --expect-tag: the assertion then rides
the receipts step, which is also dormant on a private repository, and
couples the tag check to a derivation it does not need.

abcd's own profile was already closed by launch archive --tag
(cli/archive.go), which refuses a tag that is not the newest dated
version before the receipts step. The step rides into abcd's own
release.yml for free through template parity.

TestScaffoldedGateRefusesATagNamingAnotherVersion drives the rendered
workflow on a tag push through the fake forge: each mismatched tag fails
verify at the binding with the receipts gate skipped and nothing
published, and v0.1.0 on the same commit publishes.

Refs: iss-2609251945586202
Assisted-by: Claude:claude-opus-5-5
commands/launch.md's scaffold section said the scaffolded release.yml
arms its gate against HEAD^2^ on the merge path and HEAD^ on a direct
tag. It derives the content commit from the receipts directory of the
released tree, as the same page says in its Ship and Semantic receipts
sections; the bullet says so, applies it to the semantic profile the
receipt gate exists in, and names the tag binding verify makes first.

The same stale claim sat in the bare profile's rehearsal step comment
("mirrors release.yml's resolve step exactly"): that profile arms no
receipt gate, so release.yml has no resolve step to mirror. The comment
says what the step proves. The runbook template and the release-gate
README carry no such phrase.

Assisted-by: Claude:claude-opus-5-5
…eased version

The record carries its correction: the hole was confined to scaffolded
profiles, since abcd's own verify job refuses a mismatched tag at
launch archive --tag before the receipts step.

Resolves: iss-2609251945586202
Assisted-by: Claude:claude-opus-5-5
The reviewer's sharpening (only the roll, whose first parent carries a
different version, is the content commit) would refuse a release branch
whose receipts name a later CHANGELOG revision, so it waits on a ruling
rather than landing in this fix round.

Refs: iss-2609252024442310
Assisted-by: Claude:claude-opus-5-5
Refs: iss-2609252029191920
Assisted-by: Claude:claude-opus-5-5
…ding renders

The tag-binding step added to verify is a deterministic gate, and
gate_lockstep refused abcd's own release-gate README for not listing it.
The README lists it as gate 11 and says what it guards; the runbook
template lists it after the archive gate in every semantic profile.

TestRunbookGateListMatchesVerifySteps holds the rendered runbook's
numbered list equal to the verify job's gate steps in every profile. It
also found that the abcd profile rendered no list at all: the trim
markers around the non-abcd merge-gate and audit sections swallowed both
newlines around the skipped block, gluing "## Deterministic gates" onto
the end of the rehearsal paragraph. The opening marker now trims after
itself instead of before, so the paragraph keeps its blank line in
every profile.

Refs: iss-2609252029191920
Assisted-by: Claude:claude-opus-5-5
Resolves: iss-2609252029191920
Assisted-by: Claude:claude-opus-5-5
…tocol ruling

Refs: iss-2609252024442310
Assisted-by: Claude:claude-opus-5-5
…ld-wiring

The payload diff's landing head carries main's sentences, --route and
the consolidated verbs. The ship emit step keeps both halves: it attaches
the composer's request block (nil on a refused cut) and ends with the
receipts protocol, now read from the checkout root the cut reads. The
scaffold drops its Short for the sentence manifest; the scaffold test
names `abcd lint docs` and this branch's scripts/ path. `launch receipts`
gains its sentence in the same shape as its siblings.

Assisted-by: Claude:claude-opus-5-5
@REPPL
REPPL enabled auto-merge September 26, 2026 01:33
@REPPL
REPPL added this pull request to the merge queue Sep 26, 2026
Merged via the queue into main with commit 93a44c7 Sep 26, 2026
13 checks passed
@REPPL
REPPL deleted the feat/release-gate-scaffold-wiring branch September 26, 2026 02:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant