feat: mcpm verify + GitHub Action — repo-only lockfile integrity gate (D2)#122
Merged
Conversation
… (D2) None of the existing commands run on a hosted CI runner (up hard-fails on zero detected clients; sync --check/audit are vacuously green there), so verify is a genuinely new client-free path — not a thin wrapper. - Extract classifyIntegrity + a new pure frozenVerdict from up.ts into src/stack/frozen-verify.ts. up --frozen now consumes them; its output and block matrix are byte-identical (13 frozen tests pass unchanged). - mcpm verify [--json]: loads mcpm-lock.yaml and runs that same fail-closed integrity pass with the same BLOCK semantics + exit codes as up --frozen (exit 1 on drift / unverifiable / format mismatch / mixed missing baseline; benign refuse on a lock-wide no-baseline; pypi/oci/url reported unenforceable; exit 1 on no-lock — and verify NEVER auto-locks). No client detection, no ~/.mcpm, no writes. --json emits a structured VerifyModel. - Composite Action .github/actions/mcpm-verify: wraps verify, writes a job step summary from --json, static badge + pre-commit snippet in its README. - completions (bash/zsh/fish) + the invariant test updated for the new command. ONE verb: B3 later extends mcpm verify with Sigstore provenance, never forks it. v1 scope = npm dist.integrity; stack-vs-lock staleness deferred. 1940 tests green; verified e2e on the built CLI.
…s (review MEDIUM) Review follow-ups on the D2 integrity gate: - verifyHandler now wraps its whole body in try/catch: a malformed / Zod-invalid lock (parseLock throws) — a plausible CI trigger via a bad merge or hand-edit — now resolves to a structured fail-closed error model + exit 1 through verify's OWN output (colored text / --json), instead of only exiting 1 via the global index.ts catch. Makes the exported handler total (never throws) for programmatic reuse. New test asserts it resolves to 1, not rejects. - up.ts's frozen block-message switch gains a default exhaustiveness guard (const _never: never), so a future 5th FrozenBlock reason fails the build rather than silently emitting undefined in a security-relevant block message (parity with verify.ts's already-exhaustive REASON_PHRASE Record).
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.
What
Wave-1 item D2 from
docs/ROADMAP-ADOPTION.md: a repo-only, client-free CI gate. The critique's premise held — none of the existing commands run on a hosted runner (uphard-fails at Step 3 on zero detected clients;sync --check/auditare vacuously green there) — somcpm verifyis a genuinely new path, not a thin wrapper.Changes
src/stack/frozen-verify.ts(new, shared) —classifyIntegrity(moved fromup.ts) + a new purefrozenVerdict(the structured pass/block decision). Both are client-free and output-free.up --frozenrewired to consume them. Its output and block matrix are byte-identical — the 13 exact-string frozen tests pass unchanged (they're the refactor's safety net).mcpm verify [--json]— loadsmcpm-lock.yamland runs that same fail-closed pass with the same BLOCK semantics + exit codes asup --frozen:1on integrity drift / unverifiable record / format mismatch / suspicious mixed missing-baseline1on no lock file — and verify never auto-locks (a deterministic gate)~/.mcpmreads, no writes--jsonemits a structuredVerifyModel(schemaVersion: 1).github/actions/mcpm-verify— wrapsmcpm verify, writes a job step summary from--json, ships a README with a static badge + pre-commit snippet.Design notes
mcpm verifywith Sigstore provenance — integrity now, provenance later, never two meanings.dist.integrity. Stack-vs-lock staleness (declared server missing from the lock) is a deferred follow-up — the first slice is exactly the extracted--frozenintegrity pass per the roadmap.not /different bytes|you are protected|.../).Tests & verification
verify.test.ts(11 new): the full block matrix (equal / drift / could-not-verify / format / lock-wide-no-baseline / mixed-gap / pypi-only / no-lock), honesty copy,--json(block + clean). TestsverifyHandlerdirectly with injectedparseLock/fetchNpmIntegrity— no temp files, no clients.up-frozen.test.ts(13) unchanged and green → the extraction is behavior-preserving.FORCE_COLOR=0); typecheck + lint clean.--jsonclean; no-lock → exit 1. Action step-summary script validated against both ok + blocked--json.Docs
CHANGELOG
[Unreleased](Added + Internal), CONTRACTS (mcpm verifyexit-code row +verifyin the--jsonlist), ROADMAP-ADOPTION (D2 → ✅ SHIPPED), README (command row + a CI section), CLAUDE.md (commands list + decisions log).