feat(manifest): dispatch repo-manifest-validate on schema_version, and lock the vendored schemas - #145
feat(manifest): dispatch repo-manifest-validate on schema_version, and lock the vendored schemas#145Jess Sullivan (Jesssullivan) wants to merge 2 commits into
Conversation
…d lock the vendored schemas
`repo-manifest-validate` hardcoded the v1 schema, and that was worse than
having no gate: it LOOKED like coverage. A v2 manifest could not pass it --
MassageIthaca's real one fails with `additional property 'runtime_network' is
not allowed` and `/schema_version: must equal const 1` -- so v2 adopters
forked around the action with hand-rolled checks and ended up with LESS
validation than before.
WHAT CHANGED
1. The action now reads the manifest's OWN `schema_version` and selects v1 or
v2. Unknown versions FAIL CLOSED: a manifest declaring 3 must not fall
through to v2's rules. The error names the remedy (vendor the schema and
extend the dispatch) rather than inviting someone to widen an existing one.
2. v2 is vendored from tinyland-inc/site.scaffold, whose `$id` both schemas
already name as the authority.
3. `schemas/VENDORED.json` records the source revision and per-file digests,
and a new `vendored-schema-provenance` gate asserts the copies still match.
Before this, ci-templates carried a v1 copy with NO lock and NO gate, and it
had silently diverged from its source in BOTH directions (ci-templates
gained `authorities.artifact_registry`; site.scaffold gained a
`gitops_receiver` prohibition). Nothing compared them.
The provenance gate is deliberately HERMETIC -- it compares recorded digests
and does not reach site.scaffold. A network call would make every consumer's
CI depend on another repository being reachable. It catches what a lock can
catch offline: a hand-edited copy. Mutation-proven.
v1 IS RECORDED AS `drifted`, NOT RECONCILED. Adopting upstream's v1 bytes
would change what every existing consumer is validated against, which is a
separate decision with its own review. This records the divergence so it is
reported instead of unnoticed.
BLAST RADIUS: ZERO, verified rather than assumed. Every repo that actually
invokes this action is on v1 (account-controller, ci-templates,
ci-templates-tin-2783-immutable-release) and is unaffected. None of the three
v2 adopters call it: bulkload and medical-massage-specialists-infra do not
reference it at all, and MassageIthaca's only mention is a COMMENT saying its
hand-rolled gate "mirrors" this action.
A FINDING THIS SURFACED, AND IT IS BIGGER THAN THE HARDCODING. All three v2
adopters fail the v2 schema too, for three DIFFERENT reasons:
- MassageIthaca: `$schema` is the raw.githubusercontent URL, but v2 permits
only relative paths.
- bulkload: declares v2 while missing v2's required `$schema`,
`runtime_network`, and `authorities`.
- medical-massage-specialists-infra: hits `const` values that encode a spoke
shape (`tofu_module_authority` must be site.scaffold, boundary flags must
be false) while it is an owner overlay -- site.scaffold ships a separate
`application-owner-overlay.schema.json`, which suggests dispatch may
eventually need `primary_role` and not just `schema_version`.
That is a real reconciliation with three owners, not something to resolve by
widening v2 until everything passes. This PR does not attempt it. Landing the
dispatch is still correct and strictly additive: it takes the gate from
"cannot admit v2 at all" to "admits v2 when v2 is valid".
DOES NOT SELF-MERGE. Per the 2026-08-26 ratification, a change touching the
shared schema surface ships with its consumer census attached and waits for
review.
Related to TIN-4094
Correcting this PR's own claims — an adversarial Wave B review found three things that narrow itPosting against my own PR because the body overclaims, and "remove false artifacts as you go" applies to the author first. 1. The shipping validator does not implement
|
| enrollment | repos | pin |
|---|---|---|
| direct | account-controller |
@v2.12.2 |
via spoke-ci.yml |
site.scaffold |
@v3.1.0 |
gftb-site, greatfallstoolbus.org, software.tinyland.dev |
@v2.10.0–@v2.12.2 |
|
via js-bazel-package.yml |
acuity-middleware, scheduling-kit |
@v2.5.1 |
| no gate at all | 21 repos | — |
On merge the dispatch reaches only site.scaffold, which is schema_version: 1 anyway. A pin-advance wave is a prerequisite for this PR to do anything observable, not a follow-on.
4. And v2 is currently unsatisfiable for overlays
My body said this takes the gate from "cannot admit v2 at all" to "admits v2 when v2 is valid." The second half needs qualifying: under the shipping validator, no repo can be an application-owner-overlay or organization-execution-overlay.
allOf[5] (if primary_role not in [...overlays...] → owns_gitops_apply: false) fires vacuously alongside allOf[11] (const application-owner-overlay → owns_gitops_apply: true) — because the if guard uses not, which the validator ignores. MMSI emits 12 boundaries/*: must equal const False errors plus the tofu const for exactly this reason.
So my three-adopters-fail-v2 finding was right, but I attributed MMSI's failure to schema over-reach. It is really validator incompleteness making two branches fire at once.
What I am NOT changing here
The dispatch itself is still correct and still strictly additive, and the hermetic VENDORED.json drift gate stands. This PR does not become wrong — it becomes narrower than advertised, and the gap it exposes (the validator's missing not) is a bigger finding than the fork it was written to fix.
Recommend this stays unarmed pending review, and that the validator gap gets its own issue before anyone counts on the dispatch.
…k (TIN-4132)
The stdlib fallback implemented a subset of JSON Schema and ignored the rest
as "annotation-safe". That made it a gate-that-looks-like-coverage one layer
below the schema fork this branch already repairs:
- `not` never evaluated, so every prohibition passed unconditionally. Three
static spokes carrying the evicted `authorities.gitops_receiver` validated
at rc=0.
- v2 expresses role discipline negatively (`not` x17, `contains` x13,
`anyOf` x4). Under the fallback, allOf[5]'s `if primary_role not in
[...overlays...]` guard fired VACUOUSLY alongside allOf[11], making both
overlay roles literally unsatisfiable -- misread as schema over-reach for
weeks (medical-massage-specialists-infra's 12 boundary const errors were
this, not the schema).
ITS REASON FOR EXISTING IS DEAD. The TIN-2109 cold-`nix develop` store-lock
failure belonged to the shared-store host-runner generation; current ARC pods
mount per-pod ephemeral PVC nix stores, so cross-job store-lock contention is
structurally gone (GF substrate confirmation, 2026-08-27, peer review
requested by the operator before this change). GF's words: "delete the
fallback with confidence; it guards against a dead generation while silently
passing real violations."
WHAT REPLACES IT: nothing weaker. The script imports jsonschema or exits 2
naming the dependency. The composite action, when host python lacks it, runs
the validator inside the pinned devshell (`nix develop --command`), which
substitutes cache-hot from the in-cluster Attic -- machinery this substrate
already operates; no pip surface added (GF's recommendation over a pinned pip
step, and the estate's Bazel/nix-ingestion doctrine). Only a host with
neither jsonschema nor nix refuses, visibly.
ACCEPTANCE, run against real estate bytes:
- darkmap.tinyland.dev vs the AUTHORITY v1 (site.scaffold): FAILS with
"should not be valid under {'required': ['gitops_receiver']}" -- the exact
violation the fallback passed at rc=0.
- darkmap vs the VENDORED v1 still passes rc=0 -- because the vendored copy
lacks the `not` (0 occurrences vs the authority's 1). That is the recorded
drift itself made visible, exactly what VENDORED.json's "drifted" row says;
the de-fork remains a separate decision and is now WORTH something, which
it was not under the old engine.
- medical-massage-specialists-infra vs v2: real errors only (the
tofu_module_authority const ruling question), not 12 vacuous boundary
cascades. The overlay branches are satisfiable again.
- A valid v1 manifest still passes; a jsonschema-less python exits 2 with the
reason.
The vendored-schema-provenance gate is untouched and still green.
Related to TIN-4132
…e vendored schemas (TIN-4192) Re-scopes ci-templates #145 onto main, per TIN-4192. #146 already landed the `schema_version` dispatch, so what #145 still uniquely carried is here and nothing else: the engine reduction, the vendoring lock, and the same hardcoded-v1 defect one directory over. WHY NOT STACK ON #145 #145 branches from 4a31f34, which predates #146. Merging it would delete `scripts/manifest-schema-validate-selftest.sh` and `tests/fixtures/repo-manifest-v2.json` (both added by #146) and replace #146's single-source routing with a second copy of the version -> schema mapping written in shell inside the action -- the exact drift #146's commit message names as the reason to keep the mapping in `SCHEMA_BY_VERSION` only. #145's action also fails this repo's own gate. It calls `nix develop --command python3`, and `validate-ci-templates.py cache-backed-optin-contract` rejects that string outright (TIN-2109: the action must not depend on `nix develop`). Measured: swapping #145's action.yml into this tree makes that check exit 1 naming the rule. So the engine reduction is carried across by hand, and this action does NOT shell out to nix -- providing the interpreter is the calling workflow's job, via the `nix-setup` composite. WHAT CHANGED 1. The stdlib fallback validator is deleted. It implemented a subset of JSON Schema and, for months, never evaluated `not` -- so every prohibition passed unconditionally, and static spokes carrying the evicted `authorities.gitops_receiver` validated at exit 0 against the authority v1 schema. v2 states its role discipline negatively (`not` x17, `contains` x13, `anyOf` x4), so under the subset the overlay branches were unsatisfiable, which was misread as schema over-reach for weeks. Widening it and adding `assert_fallback_covers()` made it honest about its gaps; it was still a second engine kept in step by a hand-written harness. Its reason for existing is gone too: the TIN-2109 cold-`nix develop` store-lock failure belonged to the shared-store host-runner generation, and current ARC pods mount per-pod ephemeral nix stores (GF substrate confirmation, 2026-08-27). 2. `schemas/VENDORED.json` + `just vendored-schema-provenance-check`. Both vendored manifest schemas name site.scaffold as their authority via `$id`, and until now nothing compared the copies -- which is how v1 drifted in both directions unnoticed. The gate is hermetic (recorded digests, no network) and also fails on a vendored schema no entry records and on an emptied record. v1 is recorded as `drifted` and REPORTED, not failed: adopting upstream's v1 bytes changes what every consumer is validated against and is its own change. 3. `validate-ci-templates.py manifest` stops hardcoding v1. It now imports `resolve_schema_name` from the validator so the mapping has exactly one copy. ACCEPTANCE, against real estate bytes on their default branches - darkmap.phasi.space vs the AUTHORITY v1: exit 1, "should not be valid under {'required': ['gitops_receiver']}" -- the violation the fallback passed at 0. - darkmap vs the VENDORED v1: still exit 0. That is the recorded drift made visible, exactly what VENDORED.json's `drifted` row says. - software.tinyland.dev: same pair of verdicts. - MassageIthaca (schema_version 2) routes to the v2 schema and fails on one real error -- its `$schema` names the schema's own `$id`, which v2 does not admit. The gate can now admit v2; that manifest is a separate repair. - A python without `jsonschema` exits 2 naming the dependency, on every entry point, for valid and invalid manifests alike. MUTATION PROOFS - provenance gate: one flipped byte in the v2 copy -> 1; an unrecorded `tinyland-repo-manifest.*.json` on disk -> 1; `files: []` -> 1; restored -> 0. - refusal contract: reinstating a fallback that answers "valid" on ImportError fails 5 of 20 selftest cases and nothing else. - C7: with `schema_version` set to 2, the previous `validate-ci-templates.py manifest` answers `/schema_version: 1 was expected`; this one routes to v2 and reports the real errors. Positive control: both answer 0 on the real v1 manifest. `just check` passes end to end. Related to TIN-4192, TIN-4132
|
Superseded in content by #152 (draft), which is the TIN-4192 re-scope cut onto 1. This branch predates #146. It is based on Both were added by #146. Merging as-cut would also replace #146's single-source routing with a second copy of the version→schema mapping written in shell inside the action — which #146's own commit message names as the thing to avoid: "Keeping the mapping in ONE place is the point: a second copy here in shell is how it would drift." 2. This branch's action fails this repo's own gate. So #152 carries what this branch still uniquely had — the engine reduction, That |
repo-manifest-validatehardcoded the v1 schema, and that was worse than having no gate: it looked like coverage. A v2 manifest could not pass it, so v2 adopters forked around the action with hand-rolled checks and ended up with less validation than before.What changed
schema_versionand selects v1 or v2. Unknown versions fail closed — a manifest declaring3must not fall through to v2's rules. The error names the remedy (vendor the schema, extend the dispatch) rather than inviting someone to widen an existing one.tinyland-inc/site.scaffold, whose$idboth schemas already name as the authority.schemas/VENDORED.jsonrecords the source revision and per-file digests, and a newvendored-schema-provenancegate asserts the copies still match.Before this, ci-templates carried a v1 copy with no lock and no gate, and it had silently diverged from its source in both directions — ci-templates gained
authorities.artifact_registry, site.scaffold gained agitops_receiverprohibition. Nothing compared them.The provenance gate is deliberately hermetic: it compares recorded digests and does not reach site.scaffold. A network call would make every consumer's CI depend on another repository being reachable. It catches what a lock can catch offline — a hand-edited copy. Mutation-proven.
v1 is recorded as
drifted, not reconciled. Adopting upstream's v1 bytes would change what every existing consumer is validated against — a separate decision with its own review. This records the divergence so it is reported instead of unnoticed.Blast radius: zero, verified rather than assumed
Every actual caller is v1 and unaffected.
A finding this surfaced, bigger than the hardcoding
All three v2 adopters fail the v2 schema too, for three different reasons:
$schemais theraw.githubusercontentURL, but v2 permits only relative paths.$schema,runtime_network, andauthorities.constvalues encoding a spoke shape (tofu_module_authoritymust be site.scaffold; boundary flags must be false) while it is an owner overlay. site.scaffold ships a separateapplication-owner-overlay.schema.json, which suggests dispatch may eventually needprimary_role, not justschema_version.That is a real reconciliation with three owners — not something to resolve by widening v2 until everything passes. This PR does not attempt it. Landing the dispatch is still correct and strictly additive: it takes the gate from "cannot admit v2 at all" to "admits v2 when v2 is valid".
Does not self-merge
Per the 2026-08-26 ratification, a change touching the shared schema surface ships with its consumer census attached and waits for review. Auto-merge is deliberately not armed.
Related to TIN-4094