Repository navigation
ci: gen-manifests.py becomes the platform's canonical checker, configured per repo (Plugins#1426) - #3560
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The new canonical gen-manifests.py prints remediation commands that point to a vendored script path that centralized repos intentionally won’t have, so CI failures would direct users to a non-existent fix command.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR centralizes gen-manifests.py as a platform-owned CI checker (mirroring the existing centralized compile-check.py pattern), and updates core documentation and the reusable node-repo-validate workflow to support per-repo configuration via scripts/gen-manifests.config.json.
Changes:
- Add canonical
.github/scripts/gen-manifests.py(with--self-test, trunk-baseline logic, and per-repo config contract). - Update
.github/workflows/node-repo-validate.ymlto fetch + self-test the canonical script and optionally take its verdict viacentralized-gen-manifests. - Update architecture docs to reflect the centralization approach and the new per-repo config file.
File summaries
| File | Description |
|---|---|
src/MeshWeaver.Documentation/Data/Architecture/ModuleVersioning.md |
Documents the per-repo gen-manifests.config.json contract and the “one checker, every repo” centralization model. |
src/MeshWeaver.Documentation/Data/Architecture/ModuleBuildArchitecture.md |
Updates the “scripts are centralized” adoption contract to include gen-manifests.py. |
src/MeshWeaver.Documentation/Data/Architecture/CiContentBake.md |
Updates the validate-lane description to reflect platform-fetched gen-manifests.py configured by the caller. |
.github/workflows/node-repo-validate.yml |
Fetches and self-tests the canonical manifest checker at platform-ref, and gates use of it via centralized-gen-manifests. |
.github/scripts/gen-manifests.py |
Introduces the platform’s canonical manifest generator/checker with config validation, trunk witness logic, and self-tests. |
Review details
Suppressed comments (1)
.github/scripts/gen-manifests.py:796
- This remediation message also points to
python3 scripts/gen-manifests.py, which won’t exist once a repo centralizes; update it to referencescripts/platform-script.py gen-manifests.pyso CI failures give an actually runnable command.
errors.append("fix: merge the trunk if it has moved, then python3 scripts/gen-manifests.py "
"— the tag set AND the trunk baseline were verified against the remote first, "
"so the expected numbers above are the ones CI computes")
- Files reviewed: 5/5 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| errors.append(f"{plugin.name}: manifest.lock has no valid `version` " | ||
| f"(got {existing.get('version')!r}, expected MAJOR.MINOR.PATCH)") | ||
| if errors: | ||
| errors.append("fix: python3 scripts/gen-manifests.py") |
…gins#1426) The version checker was vendored in six repos and had drifted to five vintages — Plugins 1157 lines, Crm 646, Education 646, Reinsurance 644, SocialMedia 643, Manufacturing 305 — so every fix landed in whichever copy hit the bug and the other five kept it: #434 (a release has two witnesses), #942/#1023 (--resolve), and Plugins#1426, where the trunk baseline is resolved against the remote's LIVE tip. On a push run of main for a commit that is no longer the tip — every intermediate commit of a merge-queue group — the tree under test is an ancestor of the baseline it is compared against, so the checker demands a version the tree never needed and reds. Manufacturing's copy never grew the trunk witness at all, so it also has no --resolve and no self-test. .github/scripts/gen-manifests.py is now the canonical copy, fetched by node-repo-validate.yml at the caller's platform-ref and run against the caller's tree — the same centralization as compile-check.py, with the same MW_REPO_ROOT contract and the same no-fallback fetch. What stays in the caller is one allow-file, scripts/gen-manifests.config.json: skip required. Per-repo by construction, and equal to validate-repos.py's SKIP. No default: a guessed list either demands a manifest.lock for scripts/ or silently stops versioning a real module. hashModuleSources default false. The #878 fix (hash a mixed package's src/ project and its riding siblings into moduleVersion) needs the caller's project-closure.py to expose graph_of/module_owned/ riding_siblings; asking for it without one is an error, never a quiet fall-back to the smaller hash. Default false because turning it on MOVES every mixed package's version. Both are declared, never inferred from whether a file exists — a capability that degrades silently on a missing input is the skip-trapdoor shape AGENTS.md forbids, and here it would change what a published version means without saying so. The dot-directory rule only Education's copy had is now everyone's. The lane fetches and self-tests the canonical on EVERY call; the new centralized-gen-manifests input only chooses which copy is BELIEVED, so neither setting skips a gate and a repo that has not adopted runs its vendored copy in a step that says so. Retiring a caller's vendored copy is a separate step — it stays its local command and, in most callers, validate-repos.py's library for check_manifests, until a platform-script.py-style resolver replaces it. Verified. `--self-test` grew the config contract to five cases (missing config, a typo'd key, a missing project-closure.py, one predating #1118, and the flip actually moving the hash) beside the #1426 case and its controls; with the #1426 branch removed from _trunk_commit it fails with "must derive against ITSELF", and passes with it. Equivalence measured on all six trees as committed: the canonical, with each repo's declared config, reproduces every lock and every derived version the vendored copy wrote — --check and --check-versions identical in all six, including SocialMedia (whose project-closure.py predates #1118 and whose own header warns against hashing src/ here) and Plugins (hashModuleSources true). Manufacturing gains the trunk witness: "verified against their tags" becomes "verified against the published tags and the trunk". The falsification control is that Plugins' copy run UNCONFIGURED against SocialMedia moves that module's content hash, so the sweep is not vacuous. check-workflow-timeouts.py: 0 violations. No What's New entry: CI-internal, nothing a portal user can notice — as Plugins#1427. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LzrcSMRzD2R5UcGDk5TKU
f20b2a5 to
3e3a8b5
Compare
Fleet half of Systemorph/MeshWeaver.Plugins#1426. The repo half is #1427 (merged); this is the decision that issue left open — port the hunk into five copies, or centralize the script — and it is centralize.
The measurement that decided it
scripts/gen-manifests.pyonmain_trunk_commit)--resolveOne script, five vintages. Education↔Reinsurance differ by 8 lines, Reinsurance↔SocialMedia by 3 — and every one of those lines is the
SKIPset. The rest is lag: each fix landed in whichever copy hit the bug and the other five kept it (#434 two witnesses, #942/#1023--resolve, #1426 the trunk baseline). That is behind, not different — the exact condition AGENTS.md's centralization rule names.The mechanism is not new. This very lane already fetches three of the platform's scripts at
platform-ref—check-workflow-timeouts.py,check-pr-secret-preflight.py, and (in the compile-check lane)compile-check.py, whose header records the same history: "three satellite copies had already drifted apart when this centralization landed".compile-check.pyalready takes its tree throughMW_REPO_ROOTand its policy through an allow-file; MeshWeaver.Reinsurance already shipsscripts/platform-script.pyto resolve the platform's copy locally. This change walksgen-manifests.pydown the same path.What stays in the caller
One allow-file,
scripts/gen-manifests.config.json:skipvalidate-repos.py'sSKIP, whichcheck-skip-sets.pyasserts. No default: a guessed list either demands amanifest.lockforscripts/or silently stops versioning a real module.hashModuleSourcessrc/project and its riding siblings intomoduleVersion) needs the caller'sproject-closure.pyto exposegraph_of/module_owned/riding_siblings. Asking for it without one is an error, never a quiet fall-back to the smaller hash — and default false because turning it on moves every mixed package's version.Both are declared, never inferred from whether a file happens to exist: a capability that degrades silently on a missing input is the skip-trapdoor shape AGENTS.md forbids, and here it would change what a published version means without saying so.
Why the divergence did not defeat centralization — it shaped it
Measured, and it is the reason
hashModuleSourcesexists rather than being assumed: Plugins' copy cannot run against MeshWeaver.SocialMedia (AttributeError: module 'project_closure' has no attribute 'module_owned', thenriding_siblings— SocialMedia'sproject-closure.pypredates Plugins#1118), and with those stubbed it moves theSocialMediamodule's content hash. SocialMedia's ownproject-closure.pywarns in bold against hashingsrc/there, answering #878 with a separate--closuregate instead. That is a genuine per-repo design choice, so it became a declared switch — not a lift-and-shift that would have republished a live module.No gate is skipped
The lane fetches and self-tests the canonical on every call, with no fallback — a ref it cannot be fetched at fails red naming it. The new
centralized-gen-manifestsinput only chooses which copy is believed; a repo that has not adopted yet runs its vendored copy in a step whose name says so. Both branches run a gate; neither setting makes a run silent.Verified
The regression test, before and after. With the
#1426ancestor-of-tip branch removed from_trunk_commit(the pre-#1427 behaviour), the self-test fails:and with it restored, exit 0. Its internal control still asserts the fixture does reproduce the red when derived against the tip, so it cannot pass vacuously.
--self-testalso grew five cases for the config contract: a missing config, a typo'd key (hashModulesSources), a missingproject-closure.py, one predating #1118, and the flip actually moving the hash — plus the proof thathashModuleSources: falsehashes exactly what a vendored copy hashed.Equivalence, all six trees as committed — the swap must republish nothing:
--check--check-versionsverified against their tags→verified against the published tags and the trunkThe falsification control is the SocialMedia failure above: an unconfigured canonical does move a hash, so a green sweep means something.
check-workflow-timeouts.py --root .: 71 jobs, 0 violations.What this does not do yet
Nothing changes for any satellite until it opts in —
centralized-gen-manifestsdefaults to false, so an automaticplatform-refbump onto this lane is inert. Adoption is one commit per repo (add the config, drop the vendored copy, pointvalidate-repos.pyandcheck-skip-sets.pyat the platform copy, flip the input), landing after this. Those PRs carry theClosesfor Plugins#1426.No What's New entry: CI-internal, nothing a portal user can notice — as Plugins#1427.
Pairs-with: none — no
publicsurface insrc/is touched; the diff is CI scripts and docs.🤖 Generated with Claude Code
https://claude.ai/code/session_018LzrcSMRzD2R5UcGDk5TKU