Repository navigation
feat(ci): derive the merge contract from the host, and gate the copy against it - #267
Conversation
…against it Refs: CLOUD-54
CLOUD-54 Read the merge contract from the host ruleset: the derived `[ci]` table and its drift gate
Why Every consumer of "which checks must pass, and how does a branch land" — the The authority is the host: the GitHub rules API ( Two thirds of the original scope are landed and are regression surface here, not work:
What remains — and what this issue is — is the contract itself: the Rejected alternatives
Mechanism
Definition of done
Acceptance
Refinement — Ready (host ruleset payload in; committed Refinement gate: Definition of Ready & Done. This body carries only specializations.
Stated assumptions
|
|
|
/fast-forward |



Implements CLOUD-54 — the
[ci]schema, the derivation from the host ruleset,and the drift gate that keeps the committed projection honest.
The host is the authority;
[ci]is a projection"Which checks must pass, and how may a branch land" is a fact every gate either
reads from somewhere or hardcodes. This lands the reading. The GitHub rules API
is the authority and
[ci]is a derived copy — never the reverse, which is whatCLOUD-35's
[ci]-first-with-host-fallback design got wrong: one fact answerablefrom two places.
Committed rather than fetched per run, deliberately: agents fetch, gates
decide. Deriving live would put a credentialed network call inside a gate, and
a gate that can fail because a token expired is not a gate. The committed copy is
offline, deterministic and
[epoch]-observable; the drift check is what stops itquietly becoming a second authority.
Union for checks, intersection for methods
Required checks are the union over
required_status_checksrules — each addsan obligation, and a branch must satisfy all of them.
Allowed merge methods are the intersection over
pull_requestrules thatcarry the key — each narrows what may be used, so a method has to be permitted
by every rule that speaks to it. Union there would widen the contract past what
one of the rules allows, which is the dangerous direction.
No
pull_requestrule at all yieldsNone— "the host constrains no method" —which is a different claim from an empty set, and only agrees with
None.Drift is symmetric, and signed
Both directions are findings, with
+for a token the host has and-for onethe config claims. A stale name in the projection is not harmless: it is exactly
what a downstream reader would wait on forever.
A host that constrains methods while the config omits the key is the dangerous
case — the projection silently claims freedom the host does not grant — and it
reports.
Deliberate deviations, both flagged
1. Unknown rule types are ignored, but a non-array payload is exit 1. The
host adds rule types over time and failing on one would break the gate on a
change nobody made. But a payload that is not a rules array must not derive an
empty contract from it: empty would read as agreement against an absent
[ci]and as drift against a real one — two wrong answers from one wrong document.
2. The CI wiring is scheduled, not on the landing path. The issue says "a CI
step pipes
gh api ...into the gate so drift fails the build". I put it in.github/workflows/ci-drift.ymlon a weekly cron instead, followinglock-currency.yml's recorded lesson — which is the same mistake, avoidedtwice:
Someone editing the ruleset in the GitHub UI is exactly that shape: the drift is
real, but it belongs to no PR and no PR can fix it. Putting it on the landing
path would also have forced the fetch into
verify(ci-local-parityrefuses atask CI runs that
verifydoes not), making local verify require a token.The split stays intact either way:
mise run ci-driftdoes the fetching where acredential is expected;
batten config lint --host-rules -is a pure offlinecomparison any developer can run against a saved payload with no token. A fetch
failure is exit 1, not 0 — a check that could not look has not found agreement.
Consumer #1 adoption
[ci] required_checks = ["final"], derived from the live ruleset, with noallowed_merge_methodsbecause the ruleset carries nopull_requestrule. Thecomment records why landing-by-
/fast-forwardis deliberately not encodedthere: it is repository discipline, not a host constraint, and recording a
convention as a host rule would be the second authority this issue exists to
prevent.
min_batten_versionis raised to0.0.59— the version that introduces the key,not the next tag: release-plz bumps on landing, so naming the next tag would make
this file refuse the very build that has to lint it.
Bonus finding: the required check is
final— which is exactly the checkthat read
skippedin the landing that produced CLOUD-327. A required-set-awareci-waitwould have refused it. This table is that issue's intended source.Tests
Unit (
ci.rs): union over checks, intersection over twopull_requestrules, alegacy payload yielding checks and no method constraint, unknown rule types
ignored, every non-array payload a usage error while
[]stays valid, agreementsilent, drift in both directions, and validation refusing an empty/duplicated
check list or an unknown method token.
E2E (
config_lint.rs), one per Acceptance bullet: agreement is clean; a missingcheck, a stale check, an omitted method constraint and an over-claimed method are
each exit 2 naming the key and the signed tokens; a legacy payload is clean on
the method half and still compares checks; a non-rules payload and
--host-ruleswithout a committed
[ci]are both exit 1; every malformation is refused atparse;
-Jis byte-identical across runs and reads stdin; and lint without theflag is unchanged.
Refs: CLOUD-54