chore: merge main into v3 (decompress removal, self-update guard, launcher pin) - #702
Merged
Conversation
Pushing to `v3` ran no checks at all. `ci.yml`, `cli-bundle.yml` and `mops-test.yml` trigger on pushes to `main`/`master` and on `pull_request`, so a merge into the integration branch was validated only by whichever PR happened to be based on it next — and when something breaks that way, the failure surfaces on an unrelated PR and is awkward to attribute to the merge that caused it. Adding `v3` to all three workflows behind the required checks rather than to `ci.yml` alone: `ci-ok`, `ci-ok-cli` and `ci-ok-mops` come from three separate files, so touching one would leave two of the three required checks unrun on a `v3` push. `code-quality.yml` is included for parity — it already runs on `main` pushes and is cheap. This matters more now that admin bypass is disabled on both branches: everything reaches `v3` through a PR, and the post-merge state of the branch should be verified in its own right rather than inferred. No behavior change for PRs — those already ran on `pull_request` regardless of target branch.
When 3.0.0 reaches the release canister, `/tags/latest` flips and every 2.x user running `mops self update` absorbs a major with breaking changes — the only signal is the version number scrolling by. This is the last 2.x window to change that: the guard has to be in the installed CLI *before* the flip. A major jump now stops and asks: ``` Current version: 2.20.0 Version 3.0.0 is a new major release with breaking changes: https://github.com/caffeinelabs/mops/releases/tag/cli-v3.0.0 ? Update to 3.0.0? › (y/N) ``` Non-interactively (CI, scripts) it skips the update with a notice and **exits 0**: ``` Skipping the major update. Run mops self update --major to update. ``` Exit 0 is the load-bearing choice: erroring would itself have been the breaking change — a script running `mops self update` succeeds today and would turn permanently red the day 3.0.0 ships. With the skip, the script keeps working and simply stays on its major. Same-major updates (minor/patch) never prompt and are byte-identical to today. Also fixed in passing: an unparseable `/tags/latest` body (an error page, an empty response) was previously handed straight to `npm add -g` as an install spec; it is now an error naming the URL. ## Coverage is honest-but-partial Only users who self-update to a release carrying this guard get it — someone sitting on 2.20.0 still jumps straight to 3.0.0. That is unavoidable (the check runs client-side), which is why it should ship in a 2.x release soon rather than ride along with something else. The GA release notes remain the safety net for everyone else. The same guard lands on `v3` in a sibling PR, which covers the 3.x→4.x jump permanently. ## Scope Deliberately no `releases.json` diffing or npm consultation — `/tags/latest` plus a major comparison is the whole check. The decision table is a pure helper (`helpers/self-update-kind.ts`) with unit tests covering the prerelease edges (`2.20.0 → 3.0.0-beta.1` prompts; `3.0.0-beta.1 → 3.0.0` does not). The jest `moduleNameMapper` addition is the standard ESM `.js`→`.ts` mapping; the full suite (25 suites, 200 tests, 73 snapshots) passes with it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`npm audit` on the CLI reports two criticals, both against `decompress`, both unfixed upstream ([GHSA-mp2f-45pm-3cg9](GHSA-mp2f-45pm-3cg9), [GHSA-h39j-r5qq-r9mm](GHSA-h39j-r5qq-r9mm)): a malicious archive can write files outside the extraction directory, via `..` entries or symlinks. The one remaining call site is `downloadFromGithub`, which extracts zips from **arbitrary user-specified repos** (`repo = "..."` deps) — exactly that threat model. The toolchain extractor moved off `decompress` in 2.20.0 (#667); this finishes the job and removes the dependency. Extraction now uses [`fflate`](https://www.npmjs.com/package/fflate) (zero-dependency, actively maintained) with explicit containment in `helpers/extract-github-zip.ts`: - an entry with a `..` segment or an absolute name fails the **whole** extraction, before anything is written; - symlink entries are written as plain files holding the link target, so a link pointing outside the install directory cannot be followed later — this is the one behavior change, and it affects only repos that ship symlinks; - the `<repo>-<ref>/` root GitHub wraps archives in is stripped, as `decompress`'s `strip: 1` did. Unit tests build malicious zips with `fflate` itself (zip-slip via `..`, absolute names) and assert refusal with nothing written, plus the happy-path strip. Verified end to end by installing a real GitHub dep (`dfinity/motoko-base#moc-0.14.14`) through the CLI: files land at the same paths as before and the lockfile integrity check passes. Full suite: 197/197, `npm audit` clean. Ships in 2.x deliberately — it's non-breaking and the exposure exists today ([TODO.md](TODO.md) tracked it as "should not wait for v3"). The v3 branch has the same call site in `cli/commands/install/install-from-github.ts`; it inherits this on the next main→v3 merge (the file moved, but the extraction code is identical, so if the merge conflicts it resolves to this version). The jest `moduleNameMapper` addition is the standard ESM `.js`→`.ts` mapping, needed to unit-test the helper; [#696](#696) adds the identical lines. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Every cold replica start asks `api.github.com` for the newest `icp-cli-network-launcher` release — `npm run replica` wipes `.icp/cache/networks`, so in CI that's every run. Two problems, one root cause: 1. **The request is anonymous and gets 403 rate-limited on shared runners.** Observed failing [#698's test job](https://github.com/caffeinelabs/mops/actions/runs/31384954148/job/93443187571) with `Error: failed to fetch latest network launcher version from GitHub`. 2. **The launcher — the PocketIC under every replica test — was unpinned.** Everything else icp is pinned (icp-cli in `ci.yml`, recipes by sha256), and `AGENTS.md` warns against `icp network update` for exactly this reason; the warning was guarding a pin that didn't exist. `icp.yaml`'s managed network accepts a `version`. Pinned to what `latest` resolves to today (`15.0.0-2026-07-31-04-23`), so nothing moves — this changes drift, not behavior. The pin also eliminates the rate-limit exposure, not just hides it: a pinned version downloads straight from `github.com/…/releases/download/<tag>/…` and never touches the rate-limited API. Proven by pinning a bogus version and reading the URL in the resulting 404: ``` HTTP status client error (404 Not Found) for url (https://github.com/dfinity/icp-cli-network-launcher/releases/download/v99.99.99-bogus/…) ``` That bogus-pin failure is also the proof the field is honored rather than silently ignored. Verified with the pin: cold `npm run replica` + `npm run deploy-local` pass and `main` resolves on the local network. To move launcher versions later: bump the `version` in `icp.yaml` alongside the icp-cli/recipe pins and re-run the local pipeline — `AGENTS.md`'s pinning bullet now says so. The `v3` branch has the same replica script and inherits this on the next main→v3 merge. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Contributor
Cursor AI review👍 APPROVE — looks safe to merge
VerdictDecision: APPROVE Generated for commit 0891d24 |
automation-sa-sre
previously approved these changes
Aug 10, 2026
automation-sa-sre
left a comment
There was a problem hiding this comment.
Automated approval: the AI review verdict for 8a59ed7 is APPROVE. See the "Cursor AI review" comment for details.
The merge resolved the lock with `npm install --package-lock-only`, which on macOS pruned every cross-platform @esbuild/* optional entry — `npm ci` on Linux then correctly refused with "package.json and package-lock.json are in sync" errors. Regenerated by restoring the v3 lock and applying the dependency delta with a real install, which keeps the platform entries; verified with a from-scratch `npm ci`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
automation-sa-sre
approved these changes
Aug 10, 2026
automation-sa-sre
left a comment
There was a problem hiding this comment.
Automated approval: the AI review verdict for 0891d24 is APPROVE. See the "Cursor AI review" comment for details.
Kamirus
enabled auto-merge (squash)
August 10, 2026 13:23
Kamirus
added a commit
that referenced
this pull request
Aug 10, 2026
The merge re-added cli/vessel.ts: #702's rename detection had paired main's vessel.ts with v3's install-from-github.ts, so this second merge saw main's copy as a new file. v3 deleted vessel support in #675 and nothing imports the file — it was dead code duplicating the live GitHub install path, compiled into dist. Verified it is the only file this merge resurrected. docs/docs/09-mops.toml.md's new check-deploy row still carried 2.x's "requires pocket-ic 9.0.0 or newer in [toolchain]" — on v3 no pin is needed (the default version is used) and sub-9 pins are rejected globally. Now worded like the other pages this PR already fixed. Both flagged by the AI review. Also normalizes formatting main's global.d.ts and jest.config.js carried in. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kamirus
added a commit
that referenced
this pull request
Aug 10, 2026
…y`) (#703) Syncs [#644](#644) into v3. The feature merges mostly clean; the interesting part is that it was written against 2.x's dual-PocketIC-client world, which v3 deleted — so this is a port, not just a conflict resolution. ## What is deliberately not carried `#644` routed `--check-deploy` through a `client: "dfinity"` selector plus `assertDfinityClientSupportsPocketIc`, because on 2.x the default client path could pick the legacy `pic-ic` (< 9.0.0), which cannot drive deployment checks. On v3 the upstream client is the only client and both halves of that guarantee already exist globally: pins below 9.0.0 are rejected at binary resolution with the migration message, and **no pin resolves to the compiled-in default** — so `--check-deploy` on v3 needs no pin at all, and requiring one (as #644's error did) would contradict v3's own changelog entry. The selector, the `PocketIcResult` union and the assert are gone; `pocket-ic-startup.ts` is not carried. What **is** carried from that module: `createClientOrStopServer` — a server whose client never came up is an orphaned process on any client — now in `pocket-ic-client.ts` with its unit tests. ## Behavior differences from #644 on main, both intentional | 2.x (#644) | v3 (this PR) | |---|---| | `--check-deploy` with no `pocket-ic` pin: error, "requires \`pocket-ic\` in \`[toolchain]\`" | Works — runs on the default version | | `--check-deploy` with a `< 9.0.0` pin: rejected **before** building by the assert | Rejected at pocket-ic resolution (**after** the build) by the global floor guard, with the standard migration message | The two version-gating tests were rewritten to assert exactly that, and the `check-deploy-legacy` fixture gained buildable source — it never needed any on 2.x because the assert fired before compilation ever ran. Docs (`03-mops-build.md`), the skill and the changelog entry are reworded where they claimed a 9.0.0+ pin is required. ## Changelog resolution Same convention as [#702](#702): #644's entries land in v3's `## Next` (rolled into the version heading at release), the pin-requirement sentence adjusted; the self-update and moc-wrapper duplicates are dropped (already present on v3). `cli/package-lock.json` regenerated by restoring v3's lock and applying the delta with a real install — the platform-optionals lesson from #702. Full suite on the merged tree: 302/302, 77 snapshots, eslint and prettier clean. That count includes #644's check-deploy integration tests running against real PocketIC on v3's client for the first time. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Michael Morandi <michael.morandi@caffeine.ai> Co-authored-by: Cursor <cursoragent@cursor.com>
Kamirus
added a commit
that referenced
this pull request
Aug 10, 2026
Brings the 2.21.0 release and [#706](#706) (released 2.x line at the docs root) onto `v3`. ## Conflicts Sixteen files conflicted. Thirteen are squash-merge artifacts — `main` commits already applied to `v3` by #702/#703 re-presented because squash merges leave the originals unreachable — and were resolved to `v3`'s side unchanged. The three real ones: **`cli/CHANGELOG.md`** — `main` rolled `## Next` into `## 2.21.0`; `v3`'s five `## Next` entries are exactly those, now released. Took `main`'s file and reinserted `v3`'s `## 3.0.0 (unreleased)` section between `## Next` and `## 2.21.0`. Also dropped the `mops self update` major-guard bullet from the 3.0.0 section — it shipped in 2.21.0, and no other 2.x-released entry is repeated under 3.0.0. Resulting heading order: `Next` (empty) → `3.0.0 (unreleased)` → `2.21.0` → `2.20.0`. **`docs/docusaurus.config.js`** — values identical on both sides. Took `main`'s comment: `v3`'s said "`main` carries `'current'`", which #706 made false. **`cli/vessel.ts`** — resurrected again by rename detection pairing it with `install-from-github.ts`. Deleted; nothing imports it. Confirmed it was the only re-added file via `git diff --diff-filter=A origin/v3 HEAD`. ## Lockfile `cli/package-lock.json` carries `v3`'s dependency graph with the version field aligned to `package.json`'s `2.21.0`. Edited by hand rather than regenerated: `npm install --package-lock-only` on macOS prunes the cross-platform `@esbuild/*` optional entries and breaks Linux `npm ci`. `cli/package.json` differs from `v3` only in that version field, so `v3`'s graph is already correct. ## Verification - `npm ci` from scratch in `cli/` — clean, 0 vulnerabilities - `npm run check` (`tsc --noEmit`) — passes - No files resurrected by rename detection 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Michael Morandi <michael.morandi@caffeine.ai> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: caffeine-ci-bot[bot] <249119985+caffeine-ci-bot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Kamirus
added a commit
that referenced
this pull request
Aug 13, 2026
Brings `v3` up to `origin/main`. Three of main's fifteen commits carry anything new: the 2.22.0 release and its `cli-releases` artifacts, and the changelog-rollup fix in `prepare-cli-release.yml`. The other twelve were already ported to `v3` in adapted form — the lock `graph`, hash carry-over and cache resilience via [#717](#717), `--check-deploy` via [#703](#703), the `decompress` removal and launcher pin via [#702](#702), the self-update major guard via [#697](#697), the 2.21.0 release via [#707](#707). So every source conflict resolves to v3's version, and the net diff is ten files. ## Why 28 files conflicted for a 15-commit sync Every previous sync (`24793648`, `9d086b9b`, `eb5c1232`) was squash-merged, which discards the ancestry link. Git's merge base is consequently still `0e91af70` (#550), so each sync re-resolves everything both branches have touched since — not what actually changed since the last sync. **Merging this with a merge commit rather than a squash would stop that.** The next sync would then only have to resolve real divergence. ## Conflicts that needed a decision Each main-only hunk was checked against v3 rather than blanket-resolved: - `sync-local-cache.ts` — main's "restore a package missing from the global cache" fix is already at `sync-local-cache.ts:55`. - `build.test.ts` — main's `--check-deploy` / `--check-wasm` tests moved to `build-check-deploy.test.ts` and `build-check-wasm.test.ts` when [#731](#731) split the file. - `build.ts` — main gained a guard requiring an explicit `[toolchain] pocket-ic` pin plus a dfinity-client compatibility assert. Neither applies here: v3 dropped the legacy client and defaults `pocket-ic` to 14.0.0. - `10-mops.lock.md` — main's two new `graph` paragraphs exist on v3, adapted to a model with no `--lock update`. - `CHANGELOG.md` — main's `2.22.0` section is spliced in above `2.21.0`; v3's `Next` and `3.0.0 (unreleased)` blocks already document the same work and are untouched. - Auto-merge quietly added two obsolete `check-deploy` snapshots to `build.test.ts.snap`, where v3 no longer runs those tests. Reverted. `vessel.ts` stays deleted, and `moc-wrapper`, `dhall-to-json-cli`, `pic-ic`, the dfx fallback and `--lock <mode>` stay gone. ## One behavior-adjacent change `docs/docusaurus.config.js` claimed the docs canister is shared and "whichever branch deployed most recently decides the layout". [#709](#709) stopped `main` deploying docs, so `v3` is now the sole deployer: ```js // This branch is the only one that deploys the docs canister, so // this config decides what docs.mops.one serves. `main` keeps a // matching copy, but there it only shapes local previews. ``` The `release.yml` docs-deploy step is kept on this branch for the same reason — main's replacement comment explaining its absence does not apply here. ## What is unchanged No CLI behavior changes. The version bump to 2.22.0 is not asserted anywhere in the tests, and `cli-releases` artifacts are 2.x release history: `releases.json`, `tags/latest`, and `2.tgz` / `latest.tgz` all agree with `2.22.0.tgz` and its recorded sha256. `npm run check` is clean and the CLI suite passes 494 tests across 59 suites. `npm run lint` fails at the repo root only because `cli-releases/frontend` has no `node_modules` in a fresh worktree, so eslint cannot resolve `eslint-plugin-svelte` — pre-existing and unrelated; `npx eslint cli` is clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Michael Morandi <michael.morandi@caffeine.ai> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: caffeine-ci-bot[bot] <249119985+caffeine-ci-bot[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
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.
Brings v3 level with main. Three changes flow in, one needing a by-hand carry:
cli/vessel.ts→cli/commands/install/install-from-github.ts), so the extractor swap was carried by hand: main'sextractGithubZipwith v3's import depths. Verified on v3 by installing a real GitHub dep (dfinity/motoko-base#moc-0.14.14) — extracted at the right paths, lock integrity passes.npm auditclean on the merged tree.promptsimport and otherwise identical code.icp.yamlmerged clean; AGENTS.md's icp-cli bullet takes main's reworded version on top of v3's dfx bullets.Changelog resolution: v3's
## Nextgains the decompress entry (the release workflow rollsNextinto the version heading at release time, so it lands in 3.0.0 automatically) and keeps the 2.x-only moc-wrapper note; main's self-update entry is dropped fromNextbecause #697 already placed it in the3.0.0 (unreleased)section. The 3.0.0 section itself is untouched, so this does not conflict with #701.cli/package-lock.jsonregenerated from the mergedpackage.jsonrather than resolved textually — the ~500-line shrink isdecompress's transitive tree leaving.Full merged suite: 269/269, 75 snapshots. (Two initial failures were a stale local
dist/predating the merge, green after rebuild — same artifact-staleness mode as in #699's development, not a code issue.)🤖 Generated with Claude Code