Skip to content

fix: gate packaging on a green build, narrow the cosign identity (#11) - #15

Merged
Weegy merged 1 commit into
mainfrom
fix/package-gate-cosign-narrowing
Aug 21, 2026
Merged

fix: gate packaging on a green build, narrow the cosign identity (#11)#15
Weegy merged 1 commit into
mainfrom
fix/package-gate-cosign-narrowing

Conversation

@Weegy

@Weegy Weegy commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

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 package gates on a green build (#11)

It used to stage whatever dist/ and ui/ happened to contain. During the 0.3.1 acceptance/publish run that produced omadia-dev-platform-0.3.1.zip at 142,081 bytes instead of 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. It installed. It activated. It 404'd when the operator clicked the nav entry.

scripts/build-zip.mjs now:

  1. Deletes the build outputs and their *.tsbuildinfopackages/plugin-api/dist, packages/runner-shim/dist, packages/plugin/dist, and the ui/ that packages/ui writes into this package — then 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. Deleting dist/ alone would not have caught it either — that is exactly what produced it.

  2. 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.

Must be present Must be absent
dist/plugin.js any *.map
every migrations/*.js — counted against the source directory, floor of 9 any *.css
ui/index.html and ≥1 hashed ui/assets/index-<hash>.js any node_modules/ path
handoff-plan.json, manifest.yaml, package.json any *.tsbuildinfo
in-ZIP README.md (promoted from optional) .sql under migrations/
  1. Bounds the archive size — floor 400 KB, ceiling 2 MB — as a backstop for whatever the named assertions did not think of. The broken artifact was 142,081 bytes; a healthy one is 538,458.

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.md is 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 .map that sneaks past the prune. Also ui/index.html without its bundle, a short migration set, source/stage migration drift, a .sql migration, a staged node_modules, a missing README.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 merely length > 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

Mutation Result
Type error appended to src/plugin.ts, then npm run package exit 1, no ZIP written, message: `npm run build` exited 2 — refusing to package
const maps = entries.filter(…)const maps = [] 2 tests fail
problems.push(\missing: ${rel}`)` → no-op 5 tests fail

2. The cosign certificate identity is narrowed (epic #470 P4, D5)

docs/SUPPLY_CHAIN.md wrote 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_SIGNERS and the widened branch of resolveCertificateIdentity are gone, and DEFAULT_TRANSITION_IDENTITY_REGEXP is renamed DEFAULT_IDENTITY_REGEXP because it is not a transition any more.

Before

^(?:https://github\.com/byte5ai/omadia/\.github/workflows/publish-images\.yml|https://github\.com/byte5ai/omadia-dev-platform/\.github/workflows/release-runner-image\.yml)@refs/(?:heads|tags)/[A-Za-z0-9._/-]+$

After

^https://github\.com/byte5ai/omadia-dev-platform/\.github/workflows/release-runner-image\.yml@refs/(?:heads/main|tags/v[0-9]+\.[0-9]+\.[0-9]+)$

Two things narrowed, not one:

  • One signer. Core's publish-images.yml is no longer accepted.
  • Two refs, not every ref. The old arm 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 still pushes, and the image it signs is now refused by every daemon. Release from main or from a vX.Y.Z tag. This is stated in the workflow header, in SUPPLY_CHAIN.md, in the OPERATOR-GUIDE and in the CHANGELOG, and asserted by a test.

The verify job in release-runner-image.yml carries the identical string; certificateIdentity.test.mjs fails if publisher and consumer drift.

The counter-proof

certificateIdentity.test.mjs is rewritten around the reversal:

  • accepts the OLD core signer at a release tagREJECTS the retired core signer (tags and heads/main).
  • The widening cases → does NOT widen a stale core pin any more, never reports a widened source for any input, says nothing about widening, for any pin.
  • New rejections: @refs/heads/feature/evil, @refs/heads/mainly, @refs/tags/latest, @refs/tags/v1.2, @refs/tags/edge.
  • New mutation guard: 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.
  • New drift guard: the workflow no longer accepts the retired signer as an IDENTITY — scoped to the identity URL, not to the substring publish-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.

⚠️ Operator action, only if you pinned core's identity

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_IDENTITY still names byte5ai/omadia/.github/workflows/publish-images.yml will refuse to start on a newly published image, with an ImageVerificationError naming the image. The fix is one variable — DEV_IMAGE_COSIGN_IDENTITY_REGEXP set 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-image verify job

That workflow triggers only on push to main, on v* tags, and on dispatch — not on pull requests, so it will not run against this branch. It will run on the merge to main, because sidecars/dev-runner-daemon/** is in the decide job's runner-path set and imageVerify.mjs changed. That build signs from refs/heads/main, which the new regexp accepts — the heads/main arm exists precisely so tag-less builds stay verifiable. I am watching that run after merge.

Version

0.3.3 → 0.3.4 in both packages/plugin/package.json and packages/plugin/manifest.yaml — the hub reads the manifest, and build-zip.mjs's drift guard aborts if they disagree.

Verification

npm run typecheck   ✅
npm run build       ✅
npm test            ✅  708 + 76 + 468 + 50 = 1302 pass, 0 fail
npm run package     ✅  omadia-dev-platform-0.3.4.zip — 538,458 bytes
                        163 files staged, 147 sourcemaps pruned
                        0 .map / .css / node_modules / .tsbuildinfo entries

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

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).
@Weegy
Weegy merged commit b5b4b19 into main Aug 21, 2026
3 checks passed
@Weegy
Weegy deleted the fix/package-gate-cosign-narrowing branch August 21, 2026 12:11
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.
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.

npm run package does not gate on a green build — a tsc failure still produces a (broken) ZIP

1 participant