Skip to content

chore: merge main into v3 (decompress removal, self-update guard, launcher pin) - #702

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

chore: merge main into v3 (decompress removal, self-update guard, launcher pin)#702
Kamirus merged 7 commits into
v3from
kamil-v3/merge-main

Conversation

@Kamirus

@Kamirus Kamirus commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

Brings v3 level with main. Three changes flow in, one needing a by-hand carry:

  • #699 — decompress removal. The call site moved on v3 (cli/vessel.tscli/commands/install/install-from-github.ts), so the extractor swap was carried by hand: main's extractGithubZip with 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 audit clean on the merged tree.
  • #696 — self-update guard. Already on v3 via #697; the merge resolves to one prompts import and otherwise identical code.
  • #700 — network-launcher pin. icp.yaml merged clean; AGENTS.md's icp-cli bullet takes main's reworded version on top of v3's dfx bullets.

Changelog resolution: v3's ## Next gains the decompress entry (the release workflow rolls Next into 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 from Next because #697 already placed it in the 3.0.0 (unreleased) section. The 3.0.0 section itself is untouched, so this does not conflict with #701.

cli/package-lock.json regenerated from the merged package.json rather than resolved textually — the ~500-line shrink is decompress'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

Kamirus and others added 5 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>
Carries the decompress removal (#699) into the moved file
cli/commands/install/install-from-github.ts, the 2.x self-update guard
(#696, already on v3 via #697 — resolved to one prompts import), and
the network-launcher pin (#700). cli/package-lock.json regenerated from
the merged package.json.
@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 Merges main into v3: replaces vulnerable decompress with fflate+path-contained extractGithubZip for GitHub deps, pins the local network launcher, and enables CI pushes on v3.
Code Quality Extractor is a small focused helper; call site in downloadFromGithub only swaps the unpacker and keeps staging/cleanup semantics. No speculative API surface.
Consistency Matches prior toolchain unpack direction (drop decompress); changelog under ## Next; self.ts is import-order only; docs/skills unchanged (no command/flag/mops.toml surface change).
Security Traced install-dep.tsinstallFromGithubdownloadFromGithubextractGithubZip: rejects .. segments and absolute names, re-checks path.relative containment, writes symlink payloads as regular files; decompress fully removed from cli/package-lock.json; workflow perms stay contents: read.
Tests extract-github-zip.test.ts covers strip-1, zip-slip, absolute paths, top-level skip; end-to-end path still covered by install-github-dep.test.ts (return value unused, so File[]→string[] is safe).
Maintainability TODO/AGENTS updated for extractor and launcher pin; lockfile regenerated rather than text-merged.

Verdict

Decision: APPROVE
Risk: Low
Reason: Install-path change is an extractor-only security fix with containment checks and tests; staging/cache flow and other merge hunks (CI v3 trigger, launcher pin, import reorder) are routine and low blast radius.


Generated for commit 0891d24

@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 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 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 0891d24 is APPROVE. See the "Cursor AI review" comment for details.

@Kamirus
Kamirus enabled auto-merge (squash) August 10, 2026 13:23
@Kamirus
Kamirus merged commit eb5c123 into v3 Aug 10, 2026
25 checks passed
@Kamirus
Kamirus deleted the kamil-v3/merge-main branch August 10, 2026 13:25
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>
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.

2 participants