Skip to content

feat(cli): confirm major-version jumps in mops self update - #696

Merged
Kamirus merged 2 commits into
mainfrom
kamil/self-update-major-guard
Aug 10, 2026
Merged

feat(cli): confirm major-version jumps in mops self update#696
Kamirus merged 2 commits into
mainfrom
kamil/self-update-major-guard

Conversation

@Kamirus

@Kamirus Kamirus commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

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

mops self update installed whatever /tags/latest served, so the day a
new major is released, every user running it absorbs breaking changes
with no signal beyond the version number. A major jump now prints the
release-notes link and asks; in non-interactive environments it refuses
and names the fix verbatim: mops self update --major. Same-major
updates are unchanged.

An unparseable /tags/latest (an error page, an empty body) is now an
error instead of being passed to npm as an install spec.

The decision table lives in helpers/self-update-kind.ts so it can be
unit-tested without dragging in the network fetch and global npm
install around it; the jest moduleNameMapper addition is the standard
recipe for resolving ESM .js source imports back to .ts.

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 Adds a major-crossing guard to mops self update: confirm in a TTY, soft-skip non-interactively, opt in with --major; also rejects unparseable /tags/latest bodies before npm add -g.
Code Quality Pure decision helper (classifySelfUpdate) reused by the command; confirmation path matches existing prompts/onCancel pattern in owner.ts.
Consistency --major naming matches mops update --major; --help option, docs/docs/cli/6-self/01-mops-self-update.md, and cli/CHANGELOG.md ## Next stay aligned. Skill file never covered self (pre-existing scope).
Security Traced update()getLatestVersion() → install URL: invalid tags now process.exit(1) instead of being interpolated into ${url}/versions/${latest}.tgz (new vs Base). No auth/registry/lockfile paths touched.
Tests Unit coverage of the decision table in cli/tests/self-update.test.ts (same-major, major, prerelease edges, invalid); network/npm sides intentionally untested, matching prior lack of self.ts tests. Snapshot strategy N/A (corner-case assertions).
Maintainability Small focused helper + clear comments; jest moduleNameMapper is the standard ESM .js.ts remap needed for this first relative .js import in tests.

Verdict

Decision: APPROVE
Risk: Low
Reason: Contained, intentional CLI UX guard with docs/changelog/help in sync; same-major path is unchanged and non-interactive major skips exit 0 by design rather than silently installing 3.x.


Generated for commit 917c11f

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

Exiting 1 would itself have been the breaking change: a script running
mops self update succeeds today and would turn permanently red the day
a new major ships. It now prints the release-notes link, says it is
skipping, names mops self update --major, and exits 0 — the script
keeps working and stays on its major.

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

@Kamirus
Kamirus merged commit f9453ba into main Aug 10, 2026
29 checks passed
@Kamirus
Kamirus deleted the kamil/self-update-major-guard branch August 10, 2026 12:22
Kamirus added a commit that referenced this pull request Aug 10, 2026
Same guard as [#696](#696), on
the v3 line: a major jump in `mops self update` prints the release-notes
link and asks for confirmation; non-interactive environments skip the
update with a notice and exit 0 (erroring would break scripted updates
the day a major ships), naming `mops self update --major`; an
unparseable `/tags/latest` body is an error instead of an npm install
spec. Same-major updates are unchanged.

#696 is what partially protects the 2.x→3.0 jump (client-side check, so
only users who update into it get it). This one covers 3.x→4.x
permanently.

Decision table in `helpers/self-update-kind.ts` with unit tests for the
prerelease edges — `2.20.0 → 3.0.0-beta.1` prompts, `3.0.0-beta.1 →
3.0.0` does not, which matters while previews ship from this branch.
Full v3 suite passes (26 suites, 265 tests, 75 snapshots).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Kamirus added a commit that referenced this pull request Aug 10, 2026
`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>
Kamirus added a commit that referenced this pull request Aug 10, 2026
…ncher pin) (#702)

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

- **[#699](#699) — decompress
removal.** The call site moved on v3 (`cli/vessel.ts` →
`cli/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](#696) — self-update
guard.** Already on v3 via
[#697](#697); the merge
resolves to one `prompts` import and otherwise identical code.
- **[#700](#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](#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](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Kamirus added a commit that referenced this pull request Aug 14, 2026
Readers of the 2.x docs at the docs.mops.one site root never saw the
features shipped in CLI 2.21.0–2.23.0: `grep -rn "check-deploy"
docs/versioned_docs/version-2.x/` had zero hits even though `mops build
--check-deploy` shipped in 2.21.0. The versioned tree was treated as a
frozen snapshot while every feature doc landed only in `docs/docs/` —
per the amended "Keep docs in sync" rule, features shipping in a 2.x
release belong in both trees.

This replays the `docs/docs/` diffs of each 2.21.0–2.23.0 feature into
`docs/versioned_docs/version-2.x/`, wording identical:

- [#696](#696) — `mops self
update` major-version confirmation and `--major`
- [#644](#644) — `mops build
--check-wasm` / `--check-deploy`, `[build].check-wasm` / `check-deploy`,
and `wasmMemoryLimit` in the config reference
- [#713](#713) /
[#714](#714) — `mops.lock`
`graph` section and hash carry-over
- [#624](#624) —
check/check-stable/migrate diagnostics on moc 1.12.0+
- [#754](#754) — `pocket-ic
15.0.0` recommendation (6 pages)

After the replay, `diff -r docs/docs docs/versioned_docs/version-2.x` is
empty, so the sync state is trivially checkable. `docusaurus build`
passes (links and anchors resolve in both trees).

## What is unchanged

- `MOPS_POCKET_IC_URL` on the environment-variables page — already
mirrored by [#761](#761).
- 2.21.0–2.23.0 changelog entries with no `docs/docs/` counterpart (the
unzipper security fix, atomic lock writes, the ENOENT cache-state fixes,
the executable-bit and moc-wrapper fixes) — nothing to port; if any
deserve docs, that's a change to make in `docs/docs/` first.

## Follow-up worth flagging

The docs site deploys from the `v3` branch only, so this has no effect
on the live site until it is synced there. `v3`'s
`docs/versioned_docs/version-2.x/` is currently identical to
pre-backfill `main`, so the sync is exactly this patch — follow-up PR to
`v3` incoming.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Fable 5 <noreply@anthropic.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