fix: gate packaging on a green build, narrow the cosign identity (#11) - #15
Merged
Conversation
Two supply-chain fixes with the same shape: a guarantee that had stopped being checked, and a window that had stopped being temporary. ## The packaging gate (#11) `npm run package` staged whatever `dist/` and `ui/` happened to contain. During the 0.3.1 acceptance run that produced a 142,081-byte ZIP against a healthy 537,065, from a `tsc` that had FAILED: `dist/` had been deleted, the stale `*.tsbuildinfo` beside it told the compiler there was nothing to do, and the archive came out with no UI bundle and otherwise indistinguishable from a valid artifact. It uploaded, installed, activated and 404'd. `build-zip.mjs` now deletes the build outputs AND their `*.tsbuildinfo` (plugin-api, runner-shim, plugin, and the `ui/` that packages/ui writes into this package), runs `npm run build` from the repository root, and refuses to package on a non-zero exit. Checking for the presence of `dist/plugin.js` would not have caught the original bug: the file was there, from the previous version. The payload assertions move to `scripts/package-payload.mjs`, where they can be exercised against fixtures. They collect EVERY problem and report them together — one broken build should cost one edit-run cycle, not one per missing thing — and cover `dist/plugin.js`, `migrations/*.js` counted against the source directory with a floor of nine, `ui/index.html` plus a hashed `ui/assets/index-<hash>.js`, `handoff-plan.json`, `manifest.yaml`, `package.json`, the in-ZIP `README.md` (promoted from optional), and the absence of `.map`, `.css`, `node_modules/` and `.tsbuildinfo`. A 400 KB floor and 2 MB ceiling sit behind that as a backstop. `test/packagePayload.test.ts` drives the gate over synthetic stages, one defect at a time; `test/packagedArtifact.test.ts` runs the same gate over the archive the checkout actually staged, so the fixtures cannot drift into describing a payload nobody ships. ## The cosign narrowing (epic #470 P4, D5) docs/SUPPLY_CHAIN.md said to narrow one release after the first image published from this repository. That was 0.3.2. `CORE_SIGNER_IDENTITY`, `TRANSITION_SIGNERS` and the `widened` branch of `resolveCertificateIdentity` are deleted, and `DEFAULT_TRANSITION_IDENTITY_REGEXP` is renamed `DEFAULT_IDENTITY_REGEXP` because it is not a transition any more: ^https://github\.com/byte5ai/omadia-dev-platform/\.github/workflows/release-runner-image\.yml@refs/(?:heads/main|tags/v[0-9]+\.[0-9]+\.[0-9]+)$ The ref arm narrowed too, and that is the part with a consequence: it was `refs/(heads|tags)/<any ref>`, so a workflow_dispatch from any branch — including one any contributor can push — minted an identity the daemon accepted. A dispatch from a feature branch still builds and pushes, and the image it signs is now refused by every daemon. Release from `main` or from a version tag. The `verify` job in release-runner-image.yml carries the identical string; the daemon suite fails if the two drift. `certificateIdentity.test.mjs` is the counter-proof: the cases that asserted the old signer is accepted now assert it is rejected, and the widening cases assert nothing is widened. An operator still pinned to core's exact identity now gets a refusal naming the image rather than an automatic widening. That is the intended end state; the migration is one environment variable, documented in SUPPLY_CHAIN.md, OPERATOR-GUIDE.md and the CHANGELOG. Version 0.3.3 -> 0.3.4 in package.json and manifest.yaml (drift guard).
This was referenced Aug 21, 2026
Weegy
added a commit
that referenced
this pull request
Aug 24, 2026
…16) (#17) package-lock.json recorded packages/plugin at 0.3.1 while its package.json moved to 0.3.4 across #8, #13 and #15. Nothing caught it: npm ci reconstructs the dependency tree, and a workspace member's own `version` field is not part of it, so the public lockfile claimed a version the plugin had not been for three releases. The one-command repair is the one that must not run. `npm install --package-lock-only` fixes the member entry and, in the same pass, rewrites the `../odoo-bot/middleware/packages/plugin-api` file: external from whatever core checkout sits next to this repo on the developer's disk (0.1.0 committed, 1.10.0 here) — machine state that must never land in a public lockfile. So: - Surgically bump ONLY the packages/plugin lockfile entry 0.3.1 -> 0.3.4. The external stays exactly as committed; no other member had drifted. - Add scripts/check-lock-sync.mjs: a read-only checker that asserts, for every workspace member (root included), lockfile version == package.json version, and prints the exact by-hand fix while forbidding the regeneration that leaks the local core checkout. It refuses any workspaces pattern it cannot safely expand -- outside-repo, a missing named member, or an unsupported glob -- rather than silently checking fewer members than the workspace has. - Wire it into `npm run package` (build-zip.mjs) and into CI before `npm ci`, where npm ci genuinely cannot see the drift. Add an `npm run check:lock` script. - Test packages/plugin/test/lockSync.test.ts covers discovery, drift, the external-is-never-a-member invariant, the error message, and that both wiring points are actually in place. Version stays 0.3.4 -- lockfile and tooling only, no release, no hub publish.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #11.
Two supply-chain fixes with the same shape: a guarantee that had stopped being checked, and a window that had stopped being temporary.
1.
npm run packagegates on a green build (#11)It used to stage whatever
dist/andui/happened to contain. During the 0.3.1 acceptance/publish run that producedomadia-dev-platform-0.3.1.zipat 142,081 bytes instead of 537,065, from atscthat had failed:dist/had been deleted, the stale*.tsbuildinfobeside it told the compiler there was nothing to do, and the archive came out with no UI bundle and otherwise indistinguishable from a valid artifact. It uploaded. It installed. It activated. It 404'd when the operator clicked the nav entry.scripts/build-zip.mjsnow:Deletes the build outputs and their
*.tsbuildinfo—packages/plugin-api/dist,packages/runner-shim/dist,packages/plugin/dist, and theui/thatpackages/uiwrites into this package — then runsnpm run buildfrom the repository root and refuses to package on a non-zero exit.Checking for the presence of
dist/plugin.jswould not have caught the original bug: the file was there, from the previous version. Deletingdist/alone would not have caught it either — that is exactly what produced it.Asserts the staged payload via the new
packages/plugin/scripts/package-payload.mjs, which collects every problem and reports them together. One broken build should cost one edit-run cycle, not one per missing thing.dist/plugin.js*.mapmigrations/*.js— counted against the source directory, floor of 9*.cssui/index.htmland ≥1 hashedui/assets/index-<hash>.jsnode_modules/pathhandoff-plan.json,manifest.yaml,package.json*.tsbuildinfoREADME.md(promoted from optional).sqlundermigrations/The migration count is compared against the source directory rather than a constant, because a migration added and never codegen'd is drift a hard-coded nine cannot see.
README.mdis now required because the hub renders a storefront page from the manifest and links no repository: an operator inspecting an unzipped plugin has that file or has nothing.Tests
packages/plugin/test/packagePayload.test.ts— 21 cases over synthetic stages: a good stage (the inverse guard), then one defect at a time. Both cases the issue names are there:ui/absent and a.mapthat sneaks past the prune. Alsoui/index.htmlwithout its bundle, a short migration set, source/stage migration drift, a.sqlmigration, a stagednode_modules, a missingREADME.md/manifest.yaml/handoff-plan.json, and one case asserting that several defects are all reported at once. Every case asserts a named problem, never merelylength > 0.packages/plugin/test/packagedArtifact.test.ts— runs the same gate over the archive this checkout actually staged, so the fixtures cannot drift into describing a payload nobody ships.Mutation-checked, not assumed
src/plugin.ts, thennpm run package`npm run build` exited 2 — refusing to packageconst maps = entries.filter(…)→const maps = []problems.push(\missing: ${rel}`)` → no-op2. The cosign certificate identity is narrowed (epic #470 P4, D5)
docs/SUPPLY_CHAIN.mdwrote the schedule down: narrow one release after the first image published from this repository. That was 0.3.2. As designed, this is a deletion rather than a rewrite —CORE_SIGNER_IDENTITY,TRANSITION_SIGNERSand thewidenedbranch ofresolveCertificateIdentityare gone, andDEFAULT_TRANSITION_IDENTITY_REGEXPis renamedDEFAULT_IDENTITY_REGEXPbecause it is not a transition any more.Before
After
Two things narrowed, not one:
publish-images.ymlis no longer accepted.refs/(heads|tags)/<any ref>, so aworkflow_dispatchfrom any branch — including one any contributor can push — minted an identity the daemon accepted. A dispatch from a feature branch still builds and still pushes, and the image it signs is now refused by every daemon. Release frommainor from avX.Y.Ztag. This is stated in the workflow header, inSUPPLY_CHAIN.md, in theOPERATOR-GUIDEand in the CHANGELOG, and asserted by a test.The
verifyjob inrelease-runner-image.ymlcarries the identical string;certificateIdentity.test.mjsfails if publisher and consumer drift.The counter-proof
certificateIdentity.test.mjsis rewritten around the reversal:accepts the OLD core signer at a release tag→REJECTS the retired core signer(tags andheads/main).does NOT widen a stale core pin any more,never reports awidenedsource for any input,says nothing about widening, for any pin.@refs/heads/feature/evil,@refs/heads/mainly,@refs/tags/latest,@refs/tags/v1.2,@refs/tags/edge.names exactly one repo+workflow — no alternation left to hide in, so re-adding an alternative signer cannot pass while every acceptance case stays green.the workflow no longer accepts the retired signer as an IDENTITY— scoped to the identity URL, not to the substringpublish-images, because the workflow legitimately cites core's publish workflow as the precedent for its docker tag shapes. That is a naming convention, not a trust decision.Everything else — anchoring, foreign-identity rejection, precedence, refusal on a bad signature, issuer requirement — survives unchanged.
Between 0.3.2 and 0.3.3 a pin on either signer was widened for you, with a warning at every boot. From 0.3.4 it is not: a daemon whose
DEV_IMAGE_COSIGN_IDENTITYstill namesbyte5ai/omadia/.github/workflows/publish-images.ymlwill refuse to start on a newly published image, with anImageVerificationErrornaming the image. The fix is one variable —DEV_IMAGE_COSIGN_IDENTITY_REGEXPset to the pattern above, which takes precedence over the exact pin.A refusal rather than a silent downgrade is the intended shape: the alternative was granting a retired publisher standing authority nobody re-confirmed.
CI note — the
release-runner-imageverifyjobThat workflow triggers only on
pushtomain, onv*tags, and on dispatch — not on pull requests, so it will not run against this branch. It will run on the merge tomain, becausesidecars/dev-runner-daemon/**is in thedecidejob's runner-path set andimageVerify.mjschanged. That build signs fromrefs/heads/main, which the new regexp accepts — theheads/mainarm exists precisely so tag-less builds stay verifiable. I am watching that run after merge.Version
0.3.3 → 0.3.4in bothpackages/plugin/package.jsonandpackages/plugin/manifest.yaml— the hub reads the manifest, andbuild-zip.mjs's drift guard aborts if they disagree.Verification
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.