Skip to content

chore: merge main into v3 (mops build --check-wasm / --check-deploy) - #703

Merged
Kamirus merged 7 commits into
v3from
kamil-v3/merge-main-644
Aug 10, 2026
Merged

chore: merge main into v3 (mops build --check-wasm / --check-deploy)#703
Kamirus merged 7 commits into
v3from
kamil-v3/merge-main-644

Conversation

@Kamirus

@Kamirus Kamirus commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Syncs #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: #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

Kamirus and others added 6 commits August 6, 2026 15:30
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>
## Summary
- add independent `mops build --check-wasm` and `[build].check-wasm =
true` controls for fast Walrus complexity analysis, with
`--no-check-wasm` as a one-build override
- add `mops build --check-deploy` and `[build].check-deploy = true` to
install final Wasm artifacts on fresh PocketIC canisters, with
`--no-check-deploy` as a one-build override
- keep static Walrus analysis and dynamic PocketIC validation separately
opt-in so either or both can run
- use the shared upstream `@dfinity/pic` 0.23.0 client without adding
another runtime dependency or binary download
- encode configured Candid init arguments, support per-canister Wasm
memory limits, and report PocketIC errors as provided by the client
- ask the configured `moc --stable-compatible` whether each generated
`.most` is reachable from a temporary empty-actor baseline, report
incompatible canisters as `MOPS-CHECK-DEPLOY-SKIPPED` with the compiler
diagnostic, and continue checking eligible siblings
- collect installation errors across checked canisters before failing
the command
- update CLI help, `mops.toml` configuration, documentation, changelog,
skill guidance, snapshots, and integration coverage

## Test plan
- TypeScript checks and ESLint
- CLI distribution build and `mops build --help` verification
- focused build integration tests for independent static and dynamic
controls, configuration precedence, collected sibling failures, IC0505,
IC0539, ordinary trap, invalid Candid, and PocketIC startup outcomes
- moc compatibility integration coverage for first-entry legacy state,
later missing state, and empty inputs expressed through a type alias
- Rust Wasm analyzer tests
- 31-canister deployment benchmark: 31/31 correctly classified,
including 14 IC0505 and 10 IC0539 failures

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The feature was written against the 2.x dual-client world and is ported
to v3's single upstream client:

- The `client: "versioned" | "dfinity"` selector, `PocketIcResult`
  union and `assertDfinityClientSupportsPocketIc` are not carried.
  v3 guarantees what they asserted: the 9.0.0 floor is enforced
  globally at binary resolution, and an unpinned pocket-ic resolves to
  the compiled-in default, so `--check-deploy` needs no pin at all.
- `createClientOrStopServer` is carried into pocket-ic-client.ts — a
  server whose client never came up is an orphaned process on any
  client, with its unit tests.
- build.ts drops the pin-required error and the pre-build assert; the
  moc path stays fallback-free.
- The two version-gating tests now assert v3 semantics: unpinned
  --check-deploy succeeds on the default version; a 4.0.0 pin fails
  with the migration message at pocket-ic resolution (after the build,
  so the fixture gained buildable source it never needed on 2.x).
- Docs, skill and changelog entries reworded where they claimed a
  9.0.0+ pin is required.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Cursor AI review

👍 APPROVE — looks safe to merge

Category Assessment Details
Summary Ports opt-in mops build --check-wasm / --check-deploy (and [build] toggles) from #644 into v3, adapting PocketIC client/pinning for the single-client world, plus packaging chmod on dist/bin/mops.js.
Code Quality Reuses startPocketIc / getWasmBindings; extracts createClientOrStopServer, validateCanisterConfig, and keeps check-deploy behind a dynamic import so @dfinity/pic stays lazy.
Consistency Commander flags + --no-* overrides match existing --no-optimize patterns; docs (03-mops-build.md, 09-mops.toml.md), cli/CHANGELOG.md ## Next, and .agents/skills/mops-cli/SKILL.md stay in sync; sibling build arity unchanged.
Security Traced checkDeploy → local PocketIC install only; encode_candid_args is local Candid encoding; createClientOrStopServer stops orphaned servers; no auth, registry, install, or integrity-path changes.
Tests Integration coverage in build.test.ts (success snapshots, skip/fail/legacy/path/startup, flag independence); unit coverage in wasm-complexity.test.ts, pocket-ic-client.test.ts, resolve-canisters.test.ts, plus Rust tests in wasm_utils.rs / lib.rs. Snapshot hunks match intended success output.
Maintainability Clear separation of advisory static analysis vs authoritative PocketIC; thresholds/constants centralized; v3 pin/default behavior documented where it diverges from 2.x.

Verdict

Decision: APPROVE
Risk: Low
Reason: Additive, default-off build validation with aligned docs/changelog/skills and strong test coverage; no high-risk registry, install, auth, or release-pipeline surface is touched.


Generated for commit ca3ff72

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>

@automation-sa-sre automation-sa-sre left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval: the AI review verdict for ca3ff72 is APPROVE. See the "Cursor AI review" comment for details.

@Kamirus
Kamirus merged commit 2479364 into v3 Aug 10, 2026
25 checks passed
@Kamirus
Kamirus deleted the kamil-v3/merge-main-644 branch August 10, 2026 14:28
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>
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.

3 participants