docs: add a guide for driving mops from another build system - #711
Open
Kamirus wants to merge 25 commits into
Open
docs: add a guide for driving mops from another build system#711Kamirus wants to merge 25 commits into
Kamirus wants to merge 25 commits into
Conversation
…ports (#674) Part of v3 (NEXT-MAJOR.md "Toolchain & runtime" / "Cleanup that affects users"). Targets the `v3` integration branch. Node.js 18 is no longer supported: `cli/package.json` engines is now `>=20.0.0`, so `npm i -g ic-mops@3` on Node 18 fails with an engines error (part of [#288](#288)). CI already tests 22/24/26 and the docs quick-start already states `>= 20.0.0`; the CLI README now says so too. No Node-18 compat workarounds existed in the code (`--experimental-vm-modules` in the Jest script is an ESM requirement, still needed on 20+). The rest was dead weight from the pre-toolchain era: - **mocv detection dropped.** `mops toolchain init` no longer refuses to run when `mocv` is on PATH (and no longer strips mocv-era `DFX_MOC_PATH` lines from shell configs), and `mops docs` no longer resolves `mo-doc` from a mocv-managed `DFX_MOC_PATH` — it always goes through the toolchain (pinned `[toolchain] moc`, dfx fallback). Verified in a temp project with `moc = "0.16.1"` pinned: `mops toolchain bin moc` and `mops docs generate` both work. - **`// compatibility with older versions` re-exports removed** from `cli/mops.ts` (`getNetwork`, `mainActor`, `storageActor`, `getHighestVersion`). One in-repo user was still on the legacy path: `cli/cache.ts` imported `getNetwork` from `./mops.js`; it now imports from `./api/network.js`. Nothing else in the repo (including test fixtures and `.agents/`) used them. `npm run check` clean, full CLI Jest green (24 suites, 193 tests). ## What is unchanged `cli/package.json` is otherwise untouched — dep bumps and `files` changes belong to sibling v3 PRs. `blog/` and `docs/` keep their own Docusaurus `engines`; those are site build requirements, not the CLI's.
Merged `v3` in after [#674](#674) landed; the only conflict was `cli/CHANGELOG.md`, where both PRs opened a `## 3.0.0 (unreleased)` section — both entry sets now live under the single heading, `## Next` left empty on top. No source-file overlap with #674. Part of the v3 breaking scope (NEXT-MAJOR.md "Defaults & UX"): mistyped flags now fail loudly instead of being silently swallowed, `mops watch` stops deploying and running tests unless asked, `mops test` shows `Debug.print` output by default, `--versions` output matches `toolchain info`, and the pinned-lintoko lint step in `mops check` gets a one-run opt-out. ## Unknown flags before `--` are rejected All seven `allowUnknownOption(true)` workarounds in `cli/cli.ts` are gone — none had to stay. They predate Commander 13; since the upgrade, everything after `--` is parsed as operands, so strict option parsing and the `-- <tool flags>` passthrough coexist fine (verified empirically against the pinned `commander` 13.1.0 for every passthrough command shape). Before: ``` $ mops check --nope No canisters defined in mops.toml. Either pass files: mops check <files...> ... ``` After: ``` $ mops check --nope error: unknown option '--nope' $ mops check Warning.mo -- -Werror Warning.mo:3.9-3.15: warning [M0194], unused identifier: `unused` ✗ Check failed for file Warning.mo (exit code: 1) ``` One real bug surfaced: `mops lint -- --severity warning` (advertised in `--help` and docs) has been broken since the Commander 13 upgrade — with `lint [filter]` declaring arity 1, the passthrough operands tripped Commander 13's new excess-arguments error, `allowUnknownOption` notwithstanding. `lint` now takes `[filter...]` like `test` does (the variadic exists only to absorb the passthrough operands; a single filter is still the interface), which fixes the passthrough under strict parsing. Covered by new tests. ## `mops watch`: conservative defaults, formatting on Before (no flags): ``` Errors: 0 Warnings: 0 Tests: 2 Generate: 0 Deploy: 0 ``` After (no flags): ``` Errors: 0 Warnings: 0 Format: 0 ``` Final matrix: | Task | Old default (no flags) | New default (no flags) | |---|---|---| | `--error` | on (always) | on (always) | | `--warning` | on | on | | `--format` | off | **on** | | `--test` | on | **opt-in** | | `--generate` | on | **opt-in** | | `--deploy` | on | **opt-in** | Rationale: the default set is everything informative and safe (checks + formatting, satisfying the `--format`-by-default ask in [#288](#288)); anything heavy or side-effectful beyond the working tree (running tests, generating declarations, deploying canisters) never runs unless named. Passing any flag still means "only the selected tasks" (plus the always-on error check), unchanged from v2. That last part matters for the migration recipe: because any flag narrows the set, `-tgd` alone drops the warning check too. `mops watch -wtgd` reproduces the old default exactly, and `-wtgdf` is the old default plus the new formatting — verified: ``` $ mops watch -wtgd $ mops watch -wtgdf Errors: 0 Errors: 0 Warnings: 0 Warnings: 0 Tests: 2 Format: 0 Generate: 0 Tests: 2 Deploy: 0 Generate: 0 Deploy: 0 ``` ## `mops info <pkg> --versions`: newest-first Matches `mops toolchain info --versions`, per the v3 note that was already sitting in `cli/commands/info.ts`. Before: ``` $ mops info core --versions | head -5 0.5.0 0.6.0 1.0.0 2.0.0 2.1.0 ``` After: ``` $ mops info core --versions | head -5 2.6.1 2.6.0 2.5.0 2.4.0 2.3.1 ``` ## `mops test`: verbose reporter by default The old default switched to the `files` reporter as soon as a second test file appeared, hiding `Debug.print` output ([#288](#288)). Now `verbose` regardless of file count; the auto-pick heuristic is removed rather than kept, since a default that changes with file count is itself the complaint. Internal callers (`publish`, `watch`) pass reporter instances and are unaffected. After (two files — previously this showed only two filenames): ``` $ mops test Running test/b.test.mo stdout hello from b PASS -------------------------------------------------- Running test/a.test.mo stdout hello from a PASS ``` ## `mops check --no-lint` Since 2.6, `mops check` runs `mops lint` whenever `lintoko` is pinned in `[toolchain]` — that stays the default and is not gated on anything. New `--no-lint` skips the lint step for a single run. Projects without a `lintoko` pin are unaffected. ## Migration notes - Scripts passing flags mops doesn't recognize now fail with `error: unknown option`; drop the flag or move it after `--` if it was meant for moc/lintoko. - Scripts using `mops info <pkg> --versions | tail -1` to get the latest version should use `head -1`. - `mops watch` users relying on implicit tests/declarations/deploys must name them: `mops watch -wtgd` for the old default, `-wtgdf` to add formatting. `-tgd` alone also drops the warning check. - Multi-file `mops test` output changes shape; pass `--reporter files` for the old output. ## What is unchanged - All 73 CLI snapshots pass unmodified — none of the flipped defaults were snapshot-covered (the one `mops test` integration test pins `--reporter verbose` explicitly), so no snapshot updates were needed. - The reporter flip is guarded by a new two-file fixture (`cli/tests/test-reporter/`) asserting `Debug.print` output reaches stdout by default and disappears under `--reporter files`. Verified it fails against the pre-v3 behavior. Targeted assertions rather than a snapshot: `mops test` output carries a wall-clock duration, which is why no snapshot covers it today. The `watch` default matrix (long-running chokidar loop) and `--versions` order (live registry call) stay uncovered as at base; both were verified manually, output above. - `mops docs coverage --reporter` keeps its `files` default (a coverage report, not a test run — different tradeoff). - Docs (`mops-check`, `mops-lint`, `mops-test`, `mops-watch`, `mops-info`), `--help` texts, `.agents/skills/mops-cli/SKILL.md` and the changelog (under `## 3.0.0 (unreleased)`, below the `## Next` heading per the release workflow) are updated in step.
…or conflicts (#679) Two correctness fixes in dependency resolution. Which version gets picked is unchanged. ## Cross-major conflicts are now reported `mops install` used to resolve a graph where one package asks for `1.x` and another for `2.x`, pick one, and say nothing. The "Conflicting versions" report existed, but it was gated behind a `conflicts` option that defaulted to `"ignore"` at every call site except `mops sources` — and by the time `mops install` reached its own explicit `conflicts: "warning"` check, `installAll` had already written the lockfile, so that call was served from the lock and never walked the graph. In practice you only saw the report by running `mops sources` by hand on a project with no lockfile. On a project where the root pins `test = "2.1.2"` and a local package pins `test = "1.2.0"`: Before ``` $ mops install Installing test@2.1.2 (cache) Installing core@2.0.0 (cache) Local dependency legacy = "…/p4-cross-major/legacy" Installing test@1.2.0 (cache) Installing base@0.10.2 (cache) Checking integrity... mops.lock created. Applications: commit this file. Libraries: add mops.lock to .gitignore. Packages installed ``` After ``` $ mops install Installing test@2.1.2 (cache) Installing core@2.0.0 (cache) Local dependency legacy = "…/p4-cross-major/legacy" Installing test@1.2.0 (cache) Installing base@0.10.2 (cache) Checking integrity... Warning! Conflicting major versions of dependency "test" test 1.2.0 is a dependency of legacy@1.0.0 test 2.1.2 is a dependency of p4@1.0.0 Resolved to test 2.1.2 — dependents on another major compile against an API they did not ask for. If you want a different version, pin it in your root mops.toml — a root dependency always wins. mops.lock created. Applications: commit this file. Libraries: add mops.lock to .gitignore. Packages installed ``` It stays a warning and resolution still succeeds. The remedy is deliberately phrased as a conditional: in the example above the root *already* pins `test`, and with three dependents across three majors no single pin clears the conflict, so the message states what won and leaves pinning to the user rather than naming a version that would often be a downgrade. The report also handles the case where the root wins with a `path` or `repo` dependency. Those resolve to a path or a repo rather than a version, so there was no version to print, and the resolution line and the remedy were both skipped — leaving a bare conflict header on a graph the root had deliberately resolved. It now reads `Resolved to the root override test = "vendor/test"`. Same-major skew (`2.0.0` vs `2.1.2`) stays silent, which is what max-wins is for. ## `--conflicts` now governs the whole command Conflict reporting became a per-command policy (`setConflictPolicy`) rather than a per-`resolvePackages` option. One command resolves three to five times — local cache sync, lockfile write, integrity check, then the resolve that produces the output — so a per-call option could only ever control the last of them. That is why the first version of this PR had `mops sources --conflicts error` exit 1 under a report labelled `Warning!`, and why `--conflicts ignore` still printed: the earlier passes never saw the flag. `--conflicts ignore` is a real opt-out again. `mops sources` is invoked by dfx's packtool on every build, so an unsilenceable report means a project that has knowingly accepted a cross-major override gets noise on every `dfx build` and `dfx deploy` forever with no escape. `--conflicts ignore` typed into `dfx.json` is an explicit acknowledgement, not hidden state, so honoring it keeps the report loud by default without making it inescapable. `warning` is the default everywhere, including `sourcesArgs`, which previously defaulted to `"ignore"`. `error` still exits 1, and prints its own `Error!` line after the report instead of relabelling one an earlier pass already printed. Commands other than `mops sources` have no opt-out. The report is deduped on the conflict itself — the dependency plus its sorted set of dependents and versions — rather than on the dependency name. That collapses the passes within one command and is what makes the `hasConflicts` decision consistent across them, while still letting a genuinely different set of dependents through. `mops watch` cannot surface the report yet, and this PR does not fix that. `ErrorChecker.run()` resolves (report to stderr) and then calls `onProgress()`, and watch's `print()` opens with `console.clear()` plus a `\x1Bc` terminal reset — so the report is wiped milliseconds after it is printed, and every later cycle has identical conflict content. Threading conflicts into watch's render loop is a separate subsystem that #676 just reworked, so it is filed as a follow-up rather than bolted on here. The report goes to stderr, which it already did, so `mops sources` stdout stays parseable by dfx. Only registry dependencies take part in a conflict: detection filters to them, because a `repo` or `path` dependency has no comparable major version. The docs now say so — they previously promised coverage of "any dependency". That filter is pre-existing behavior and is left alone here. This found a real conflict on its first run: the mops repo's own root `mops.toml` pins `fuzz = "1.0.0"` in dev-dependencies while `memory-region@0.1.1` asks for `fuzz 0.1.0`, so `memory-region` has been compiling against `fuzz` 1.x. Not fixed here, but it is no longer invisible. ## Why the comparator was wrong The old comparator split on `.`, `parseInt`ed the parts, and kept three of them. `compareVersions("1.2.0-rc.1", "1.2.0")` returned `0`: `parseInt("0-rc")` is `0`, so both sides became `[1, 2, 0]` and the `rc.1` was invisible. The resolver only replaces the incumbent on a strict `-1`, so whichever version was walked first won. With two packages depending on the same GitHub repo, one at `#v1.2.0-rc.1` and one at `#v1.2.0`, mops built against the release candidate: ``` $ mops sources --no-install # before --package gitdep .mops/_github/gitdep#v1.2.0-rc.1/src $ mops sources --no-install # after --package gitdep .mops/_github/gitdep#v1.2.0/src ``` Other pairs it got wrong, all verified against the old code: | a | b | old | correct | |---|---|---|---| | `1.2.0-rc.1` | `1.2.0` | `0` | `-1` | | `1.2.0-rc.2` | `1.2.0-rc.10` | `0` | `-1` | | `1.2.0-alpha` | `1.2.0-beta` | `0` | `-1` | | `0.16` | `0.16.1` | `0` | `-1` | | `1.2` | `1.2.9` | `0` | `-1` | Two-part versions failed because the comparator reached `ap[2] - bp[2]` with `ap[2]` undefined, and `undefined - 1` is `NaN`, which is falsy — so it fell through to `return 0` and the patch comparison never happened. Separately, `compareGitVersions` matched a tag with `/v(\d{1,2}\.\d{1,2}\.\d{1,2})(-.*)?$/` and then threw the capture group away in favour of `branch.substring(1)`, so a tag like `release-v1.2.0` was compared as `elease-v1.2.0` → `0.2.0` and lost to any `1.x` tag. It now uses the captured version. ## Leading zeros, and why parsing is `loose` The registry's validator (`backend/main/utils/semver.mo`) checks dots, digit-run length and charset. It does **not** reject leading zeros, so `01.2.3` is publishable, and `Semver.major("01.2.3")` is `1` there because `textToNat("01")` is `1`. Strict `semver` rejects the whole string — both `parse` and `coerce` return null — so it would have fallen through to the `0.0.0` fallback. That is a silent downgrade, not an edge case: ``` # two transitive deps on the same git dep, one tagged v01.2.0, one v1.0.0 $ mops sources --no-install # strict parsing --package gitdep .mops/_github/gitdep#v1.0.0/src $ mops sources --no-install # loose parsing (and matching the old comparator) --package gitdep .mops/_github/gitdep#v01.2.0/src ``` Secondary effect: `majorVersion("01.2.3")` would have been `0`, so a genuine 1.x/0.x conflict would have gone undetected by the very warning this PR adds. Both `parse` and `coerce` now pass `loose: true`. A 20-pair differential against the old comparator confirms this changes exactly the three leading-zero shapes (`01.2.3`, `1.02.3`, `0.16.01`) and nothing else — strict differed from the old comparator on 9 of 20 pairs, loose on 6, and the 3 removed are precisely the leading-zero rows. ## What flows through the comparator Registry versions (from the root `mops.toml` and from published packages' `mops.toml`) and GitHub refs. Alias deps are aliases on the *name* (`"core@1" = "1.0.0"`), so the version side is always a full version; `getDepPinnedVersion`'s two-part `0.16` never reaches this comparator. Registry versions are validated on publish as three numeric parts, each at most two digits, no prerelease — leading zeros allowed. Everything else — prerelease git tags, prefixed tags, hand-written two-part versions, non-version refs like `main` — goes through `semver.coerce(…, { loose: true, includePrerelease: true })`, and anything with no version in it at all sorts as `0.0.0`, which is what `parseInt(x) || 0` did, so `main` still loses to every real version. The comparator is total and never throws; there is a test asserting that over `""`, `"..."`, `"1.2.3.4.5"` and a 400-digit string. `parseVersion` must not be pointed at a raw git ref — coercion picks up the first number-like run anywhere in the string, so a branch named `release-2024` would coerce to `2024.0.0` and outrank every real version. The resolver extracts the version from the ref first (`gitRefVersion`), and the docstring says so. CLI vs backend agreement: on the set the registry can actually contain, the two now agree, *including* leading zeros — `cli/tests/compare-versions.test.ts` pins that with a case table that now covers `01.2.3` vs `1.0.0` and `0.16.01` vs `0.16.1`, the shapes the earlier version of this PR got wrong while claiming agreement. One disagreement remains outside that set: the backend ignores prerelease suffixes entirely, so it considers `1.2.3-rc.1` equal to `1.2.3`, while the CLI orders it below. The backend also refuses to publish such a version, so the CLI is strictly more correct on inputs the registry cannot hold. `mops update` and `mops outdated` already used the `semver` package (`cli/commands/available-updates.ts`), so the resolver now shares it. ## What is unchanged Resolution semantics are untouched. A bare `1.2.3` is still exact, conflicts still resolve max-wins with the root dependency always winning, and there is no caret, no constraint intersection and no `=` syntax — those stay deferred to the major gated on moc `--override`. Proof: `mops sources` stdout, byte for byte, before and after, across nine projects covering registry deps, alias deps (`"core@1" = "1.0.0"`), a local `path` dep with its own transitive registry deps, GitHub `repo` deps (versioned tag, a `moc-0.9.1` tag that carries no comparable version, and no ref at all), a genuine cross-major diamond, a same-major diamond, a diamond with two non-root dependents on the same registry dep, and leading-zero git tags. | project | stdout md5 before | stdout md5 after | |---|---|---| | registry + alias deps | `f8d021bd0e2e2ce0ea29ac2ad64a7213` | `f8d021bd0e2e2ce0ea29ac2ad64a7213` | | github repo deps | `2ebfbb62692ab5a8427d3bd29457322b` | `2ebfbb62692ab5a8427d3bd29457322b` | | local path dep | `9572240eb40d13c332808e269cfe1513` | `9572240eb40d13c332808e269cfe1513` | | cross-major diamond | `a9c6bb030e3379294698fa4267c6cb7b` | `a9c6bb030e3379294698fa4267c6cb7b` | | same-major diamond | `88f294e187173442f192d58e1e96ff93` | `88f294e187173442f192d58e1e96ff93` | | registry diamond | `f49e353bb3838eb905a67cc6718941c1` | `f49e353bb3838eb905a67cc6718941c1` | | leading-zero git tags | `#v01.2.0` | `#v01.2.0` | | git prefixed tag | `12737a431362a3f541d6830c95936679` | `ee0c1cd25ac05a98f50d4a101421a019` | | git prerelease tag | `ca71b4552890d79e815ccd49c6e56233` | `b47f5575220d6f3cddbf602b602e2bf8` | The only two that move are the projects built specifically to trigger the mis-ordering, and in both the new output is the correct answer. The cross-major project's stdout is identical — only its stderr gains the report. The suite corroborates this: 25 suites / 199 tests / 73 snapshots measured on `v3` at the merge base, 27 / 217 / 74 on this branch — exactly the two new suites, their 18 tests and one new snapshot, with all 73 pre-existing snapshots unchanged. `npm run check` and `npm run lint` are clean. One existing fixture did move, and it is worth calling out. `cli/tests/check-candid/` had no `mops.toml`, so `getRootDir()` walked up and resolved the mops repo's own dependency graph — the one with the `fuzz` conflict — and the new report landed in nine committed snapshots. Rather than baking this repo's root `mops.toml` into `check-candid`'s snapshots, the fixture now has its own minimal config. `check-candid` needs a config file to exist but reads no dependencies (the check is pure Wasm), so its snapshots are byte-identical to what was committed and it no longer depends on the repo root. ## Known limitation A valid `mops.lock` short-circuits resolution, so a fresh clone of a project with a committed lockfile installs from the lock, never re-walks the graph, and does not see the report. The report fires on every resolution that actually happens — no lockfile, or one invalidated by a `mops.toml` change — which covers the run that produced the lockfile in the first place, but not later clones of it. Closing that gap means resolving without the lock, which is not safe today: installing from a lockfile deliberately skips the versions that *lost* the conflict, so their `mops.toml` is not in the cache and the resolver would crash reading it. That belongs with the `--locked` and lockfile work in `NEXT-MAJOR.md`. ## Deliberately out of scope Collapsing the `conflicts` parameter to a boolean, now that `ignore` stays meaningful and there are three distinct behaviours. The `<unknown>` fallback for a dependent with no `[package]` section, which predates this PR. Surfacing the report in `mops watch`, and a project-level `mops.toml` opt-out for acknowledged conflicts (`--conflicts` exists only on `sources`, so `build`/`check`/`test`/`lint` report unconditionally) — both filed as follow-ups. A `cli/tests/mops.toml` fence for the whole fixture tree, which would change the resolution root for other config-less fixtures and so needs its own snapshot run. `updateVersion` in `cli/commands/bump.ts` still hand-rolls version arithmetic with `split(".")` and `parseInt`. It increments rather than compares, and only ever sees the project's own already-validated `[package] version`, so `semver.inc` would be churn without a bug behind it. `cli/commands/toolchain/release-tags.ts` already sorts with `semver`. The remaining `split(".")` hits in `cli/mops.ts` and `cli/commands/available-updates.ts` read a single part or count segments; they do not order anything.
Part of v3 (NEXT-MAJOR.md "Drop vessel / dhall"). Targets the `v3` integration branch. Vessel/dhall support is gone in mops v3, after being deprecated with a warning since 2.14 ([#296](#296)): - `mops init` no longer reads `vessel.dhall` and no longer migrates its dependencies into the new `mops.toml`. - Transitive dependencies of GitHub deps declared via `vessel.dhall` / `package-set.dhall` are no longer resolved or installed. - `.vessel` directories are no longer excluded from `mops test` / `mops watch` file scans. - `readVesselConfig`, `cli/vessel.ts` and the `dhall-to-json-cli` dependency are removed. `installFromGithub` is not vessel — it serves ordinary `repo = "..."` GitHub deps and only lived in `vessel.ts` for historical reasons. The first commit moves it (with its `downloadFromGithub` helper) to `cli/commands/install/install-from-github.ts` behavior-intact; the second commit drops vessel around it. `cli/tests` had no coverage for GitHub deps at all, so `install-github-dep.test.ts` now installs a commit-pinned `repo` dep and asserts the extracted tree, the lockfile and the cache hit on a second run — the regression net for this move. Also verified by hand: `mops add https://github.com/ZenVoich/test#master` in a fresh project downloads, extracts and pins the dep. `npm run check` clean, `npm run lint` clean, full CLI Jest green (26 suites, 200 tests, 73 snapshots — the current `v3` baseline plus the new GitHub-dep test). Regenerating `cli/bun.lock` also picks up drift that predates this branch: `bun.lock` on `v3` still lists `decomp-tarxz` and lacks `xz-decompress` / `tar@7.5.22`, because [#667](#667) updated `package.json` and `package-lock.json` but not `bun.lock`. The regenerated lockfile now matches `package.json` again. ## Migration If you still have a `vessel.dhall`, copy its dependencies into `mops.toml` yourself (registry packages as `name = "version"`, git packages as `name = "https://github.com/org/repo#ref"`), then delete `vessel.dhall` / `package-set.dhall`. If a GitHub dep relied on vessel files for its own dependencies, add those to your `mops.toml` directly. ## What is unchanged GitHub deps still extract archives with `decompress`; replacing it is separate, non-breaking 2.x work tracked in `TODO.md` (toolchain archives already moved off it in [#667](#667)).
) Stacked on #675, which introduced `cli/tests/install-github-dep.test.ts` covering install and the cache hit. This extends that suite instead of adding a second file — I had written a parallel one before #675 grew its own, and two overlapping github-dep test files is worse than one. Retarget to `v3` once #675 merges. Three gaps remain after #675: - **`sources` resolution.** Landing on disk is not enough; the dep has to reach moc as a `--package` flag. Asserts `--package test .mops/_github/test#master@<sha>/src`, plus that the lockfile records the repo url verbatim (which is what pins the commit). - **`mops add <url>` pinning.** Resolving a branch to a commit and writing it into both `mops.toml` and the lockfile is the reproducibility guarantee for github deps, and nothing checked it. This is the one case that deliberately resolves a branch rather than using a pinned sha, since that resolution is the behavior under test. - **Failure path.** A nonexistent repo must fail without recording a dep that can never install. Fixtures for the mutating cases are copied per-test via the existing `useTempFixtures` helper, so nothing touches a source-controlled fixture. This path is also the last remaining `decompress` call site — #667 moved toolchain extraction to `tar` plus a standalone xz decompressor but not this one, and `decompress` has two critical advisories with no fixed version. Whoever swaps the extractor needs this coverage. No CHANGELOG entry: test-only.
…ass, shell init, exit code) (#677) Four sources of invisible shared state are removed, per the "Hidden-state cleanup" section of NEXT-MAJOR.md, whose four items are now marked done. All four are breaking; changelog entries with migration notes are under `## 3.0.0 (unreleased)`. [#674](#674) and [#676](#676) are merged in: #674's mocv-guard removal met the `toolchain init` rewrite here, and #676's `allowUnknownOption` purge met the deletion of the network command registrations in `cli/cli.ts` — both sides kept in each case (no `allowUnknownOption` call and no fragment of the removed commands survives). The network item is a **deletion**: `mops set-network`/`get-network` and their storage file are gone, with `MOPS_NETWORK` as the sole mechanism. An earlier revision of this PR relocated the file to `.mops/network` plus an XDG global instead — see the reasoning below for why relocation was dropped. ## `mops set-network` / `get-network` are removed, not relocated The selected network was stored in a `network.txt` inside the npm-installed package, so switching one project to `local` silently switched every project sharing that `ic-mops` install, and any `npm i -g ic-mops` reset the choice. Before: ``` ~/project-a $ mops set-network local Selected 'local' network ~/project-a $ cd ~/project-b && mops get-network local # project-b now talks to a local replica too ~/project-b $ npm i -g ic-mops && mops get-network ic # upgrade wiped the setting for every project ``` After: ``` $ mops set-network local error: unknown command 'set-network' $ MOPS_NETWORK=local mops install # per command $ export MOPS_NETWORK=local # or per shell ``` **Why deleted rather than moved to a writable location.** `MOPS_NETWORK` was added in [#437](#437) (2.5.1) precisely because writing into the install directory "fails in read-only environments like CI pipelines and Docker containers" — i.e. the env var already exists as the workaround for this storage location being broken, and it won: it is what the docs recommend, what this repo's own dev loop and CI use, and no caller of `set-network` exists anywhere in the repo. Keeping persistence would mean maintaining a second mechanism for the same setting, and the first draft of this PR showed the cost: durable config under `.mops/` needed a `PROJECT_STATE_FILES` special case so `mops cache clean` would not wipe it. Config that has to be shielded from a cache wipe is in the wrong place. Deleted: both commands and their `sn`/`gn` aliases, the startup block that read the file into `globalThis.MOPS_NETWORK`, `getNetworkFile()`/`setNetwork()` and the project/global/legacy readers, the `network.txt` entry in `cli/.gitignore`, and the `docs/.../07-mops-set-network.md` page drafted earlier in this PR. `DEVELOPMENT.md`'s local-registry setup now says `export MOPS_NETWORK=local` instead of `mops-local set-network local`. `cli/cache.ts` is byte-identical to `v3` again. `cli/api/network.ts` is unchanged: `MOPS_NETWORK` env → `globalThis.MOPS_NETWORK` → `"ic"`. The `globalThis` branch stays because it is **not** dead — `frontend/components/package/Package.svelte` sets `window.MOPS_NETWORK` before calling into the mops API in the browser, where there is no `process.env` (`frontend/types.d.ts` documents both halves of that contract). Its ambient `declare global` moved from `cli/cli.ts` into `cli/api/network.ts`, next to the only code that reads it. ## `mops build`/`check`/`check-stable`/`check-candid`/`test`/`bench` respect `mops.lock` These commands forced `installAll({ lock: "ignore" })`, so a stale or tampered lock that would fail `mops install` was silently accepted on every build. They now run the same default lock flow as `mops install`: install from a valid lock, refresh a stale one, and fail on a tampered one: ``` $ echo '// tampered' >> .mops/core@1.0.0/src/Array.mo $ mops build foo Integrity check failed Mismatched hash for core@1.0.0/src/Array.mo ... Run `mops install --lock update` to regenerate it. $ echo $? 1 ``` Three deliberate details: - They pass `defaultLock: "update"` — the same escape the dependency-mutating commands (`add`/`remove`/`update`/`sync`) already use — so the deprecated `CI` auto-`--lock check` path never triggers for commands that have no `--lock` flag to satisfy its warning. This also matches the v3 end state (plain commands maintain the lock; `--locked` is the CI flow). - Lock creation by these commands is silent (cargo behavior: `cargo build` maintains `Cargo.lock` without commentary); `mops install` still prints the `mops.lock created` notice. - Dep-less projects no longer make a registry round-trip to write an empty-hash lock (`getFileHashesFromRegistry` short-circuits on zero packages), so builds in projects without registry deps keep working offline. One offline case does change: a project with registry deps, a warm `.mops/` and **no** committed lock now contacts the registry on its first `build`/`check` to fetch the file hashes it needs to write the lock. Once a valid lock exists, no request is made (`updateLockFile` returns early and `checkLockFile` verifies from disk). Applications are already told to commit `mops.lock`, so the pre-warmed-image flow stays offline. Scope notes: - `mops generate candid` was a seventh identical `lock: "ignore"` site (added after NEXT-MAJOR.md was written); it is included for consistency with `build`. - `mops sources` keeps `lock: "ignore"` on purpose: its stdout is machine-parsed by the dfx packtool on every dfx build, so it must not write the lock or print integrity output. Now marked with a comment. - `--locked` and the removal of `--lock` are separate v3 PRs and are not touched here. ## `mops toolchain init` is opt-in per shell It used to write every detected shell init file (`.bashrc`, `.zshrc`, `.bash_profile`, `.zprofile`) plus `$GITHUB_ENV`. Now it updates only the current shell's file (detected from `$SHELL`; for bash, `~/.bashrc` with a `~/.bash_profile` fallback when only that exists), and `--shell <bash|zsh>` targets a specific one: ``` $ mops toolchain init Success! Updated /Users/kamil/.zshrc Restart terminal to apply changes $ mops toolchain init --shell bash Success! Updated /Users/kamil/.bashrc Restart terminal to apply changes ``` Unchanged on purpose: `$GITHUB_ENV` is still written in GitHub Actions (that is the correct propagation mechanism there), the `checkToolchainInited()` CI auto-init stays, and `mops toolchain reset` still cleans **all** known files so it fully undoes inits made by older versions. When `$SHELL` is unset or unsupported, init falls back to the first existing known init file (keeps CI images without `$SHELL` working) and only errors — with a `--shell` hint — when there is nothing to write. It will create the target rc file if the shell has none, which is what makes init work in a fresh container; previously init refused outright when no rc file existed. ## Replica bind-failure exit code **Reviewer decision point:** the dfx replica bind failure in `cli/commands/replica.ts` exited with code `11`; it now exits `1`. Pre-agreed recommendation on the grounds that no documented workflow scripts against `11` and one failure code is a simpler contract — flagging it here in case anyone knows of a consumer of the old code. ## Tests - `build.test.ts` gains a `lock policy` block: `mops build` creates `mops.lock` without printing the install-only `mops.lock created` notice, and fails with the integrity error after a `.mops/` file is edited. - `cli/tests/check-candid/` gained its own `mops.toml`: the fixture had none, so `mops check-candid` walked up and resolved the **repo root** project — harmless while the command ignored locks, but now it would rewrite the repo's own `mops.lock` from inside the test suite. - The `network.test.ts` added for the relocated file is deleted along with the feature; there is nothing left to test beyond `getNetwork()`, which is unchanged. Verified by hand that `MOPS_NETWORK=local` still reaches the local endpoint (`mops cache show` → `.../mops/local`, `mops install` dialing `127.0.0.1:4943`), that `set-network`/`get-network`/`sn`/`gn` all exit 1 as unknown commands, and that `mops cache clean` removes the whole `.mops` tree as it did before this PR. - `cli.test.ts` guards the removal: `set-network`, `get-network` and the `sn`/`gn` aliases must all fail as unknown commands, and `MOPS_NETWORK` must still select the network. `--shell` and the exit-code change are not covered automatically — both would need a test that writes to `$HOME`, which is not worth the blast radius; verified by hand instead (per-shell transcript above). - Full suite on the merged tree: 25 suites / 206 tests / 73 snapshots pass with zero snapshot updates — the lock-flow change is output-invisible for valid projects. (This branch alone is 24/195; the rest arrives with [#676](#676).)
…tall, download-time verification (#681) CI pipelines get a real frozen-lockfile flag, and everyone else stops paying for a check that was re-reading the whole dependency tree on every command. `mops` had three lockfile modes behind `--lock <check|update|ignore>`, plus a fourth that appeared only when the `CI` environment variable happened to be set. This replaces all of it with the model cargo, pnpm and yarn converged on: two modes, one flag. Plain commands are the dev flow and always keep the lock correct. `--locked` is the CI flow — it requires an up-to-date lock and never writes one. Integrity moves with it. Verification now happens on the bytes as they are downloaded, so a corrupted or tampered download can never enter the cache, and installs no longer re-hash `.mops/`. The on-disk audit becomes an explicit command, `mops verify`. Closes #516. Closes #517. ## Before / After **A missing lock in CI.** Before, `mops test` would silently resolve, download and write a lockfile nobody reviewed. After: ```console $ mops test --locked Error: mops.lock is missing, but --locked was passed. Run `mops install` to generate it, then commit mops.lock. ``` It fails before downloading anything. **Someone edited `mops.toml` and forgot to commit the lock.** ```console $ mops install --locked Error: mops.toml has changed since mops.lock was generated, but --locked was passed. Locked dependencies hash: 90a342a4a0b150765297bd0d09221c6338d3cadc509cda66b944f37fca569f1a Actual dependencies hash: 075072533858da89ffa178f5ee95ae566d21fb166294658ca4be9777750029f6 Run `mops install` (without --locked) to update mops.lock, then commit it. ``` Same repo, without `--locked` — the lock is simply brought up to date: ```console $ mops install Packages installed ``` **A hand-edited lock that no longer matches the manifest.** ```console $ mops install --locked Error: mops.lock does not match mops.toml, but --locked was passed. dependency core: mops.toml declares 1.0.0, mops.lock has 2.0.0 Run `mops install` (without --locked) to update mops.lock, then commit it. ``` **A lock whose recorded hashes disagree with the registry.** Note the hint differs here: plain `mops install` does *not* rewrite this lock, so pointing at it would loop (see below). ```console $ mops install --locked Error: mops.lock does not match the registry, but --locked was passed. core@1.0.0/LICENSE: locked bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb, registry 840e3d57a38a8061f55d04470fbd58b9345326fa04ea10ee42add5c6e3b2aa08 Restore mops.lock from version control, or delete it and run `mops install` to regenerate it. ``` **A corrupt lock — the self-healing path.** This used to require `mops install --lock update`, and #514 was about that flag not actually working: ```console $ mops install --locked Error: mops.lock could not be parsed, but --locked was passed. Restore mops.lock from version control, or delete it and run `mops install` to regenerate it. $ mops install Packages installed ``` No flag recovers a broken lock any more, because none is needed. **An up-to-date lock under `--locked` is left alone** — byte-identical content and unchanged mtime, verified in `cli/tests/locked.test.ts`. ## The guarantee change: install no longer verifies files already on disk **Neither `mops install` nor `mops install --locked` gates a modified `.mops/` tree any more. `mops verify` is the replacement.** Read this if any pipeline relies on the old behavior. This is intentional and is the reason the item sits in a major. It is called out here, in `cli/CHANGELOG.md`, on the `mops.lock` docs page and on the new `mops verify` page, and it is pinned by a test in both `cli/tests/locked.test.ts` and `cli/tests/build.test.ts`. Integrity is now checked once, at download time, against the hashes published in the registry, before the package is committed to the cache. The per-install re-hash of `.mops/` is gone. Editing a dependency in place therefore no longer fails your next command: ```console $ echo "// oops" >> .mops/core@1.0.0/src/Array.mo $ mops install Packages installed ``` `mops verify` is the replacement, and it reports exactly what install used to: ```console $ mops verify Integrity check failed .mops/core@1.0.0/src/Array.mo does not match mops.lock Locked hash: 7cc453a2d20a3dae84c54f2c4a2db6db7b454f6fca0ba23e6440d324c2a68f3f Actual hash: 477f5f7cd24bf2a703faf22e42bc0ea3560428bcc84e8bd68ca08982fc88fcdd Delete the `.mops/core@1.0.0` directory and run `mops install` to restore it. $ mops verify Integrity verified 3 package(s), 130 file(s) ``` Two consequences worth stating plainly: - Packages already sitting in the global cache from an older CLI were never download-verified. `mops verify` audits them; `mops cache clean` forces a verified re-download. - Download-time verification is genuinely load-bearing, not decorative. Corrupting a single byte in transit is caught and the package never reaches the cache or `.mops/`: ```console $ mops install Error: integrity check failed for core@1.0.0 Hash mismatch for core@1.0.0/src/Array.mo Expected: 7cc453a2d20a3dae84c54f2c4a2db6db7b454f6fca0ba23e6440d324c2a68f3f Actual: f414fe00390251acc52b7563aef3e2118afd893a9e0e97bb4e1a06f9d9c09a71 ``` ### One case is deliberately not self-healed A lockfile whose recorded file *hash values* are wrong — reachable only by hand-editing or a botched merge — is not repaired by `mops install`, because detecting it needs `getFileHashesByPackageIds`, which is an **update** call, not a query: | | median warm `mops install` | |---|---| | valid lock, no hash fetch | 1.6 s | | lock regenerated, one hash fetch | 2.8 s | Paying ~1.2 s on every install to catch a hand-edit would cost roughly nine times the 136 ms this PR saves, and a warm cache fetches no hashes at download time either, so there is nothing already in hand to compare against. Since those values are read only by `--locked` and `mops verify` and never by the build, a wrong hash cannot produce a wrong build. What I did instead: `--locked` and `mops verify` report it with the hint that actually recovers (restore from version control, or delete and reinstall) rather than "run `mops install`", which would have looped forever. Covered by a test that asserts the working hint *and* walks the recovery. Two related inconsistencies that *are* detectable offline now self-heal, one of which was a real correctness hole: a `deps` entry disagreeing with `mops.toml` used to be installed as-is, silently giving you the wrong version. Plain `mops install` now re-resolves instead. ## Migration | Old invocation | New invocation | |---|---| | `mops install --lock check` | `mops install --locked` | | `mops install --lock update` | `mops install` | | `mops install --lock ignore` | no successor — the lock is always maintained | | `mops add\|remove\|update\|sync --lock update` | `mops add\|remove\|update\|sync` | | `mops add\|remove\|update\|sync --lock ignore` | no successor | | `CI=1 mops install` (implicitly meant check) | `mops install --locked` | | `mops test` in CI with no prior install | `mops test --locked` | | relying on install to detect a modified `.mops/` | `mops verify` | | `mops.lock` in a library's `.gitignore` | commit `mops.lock` | A typical CI job becomes: ```yaml - run: mops install --locked - run: mops test --locked ``` `--locked` is on `mops install` and on every implicitly-installing command: `build`, `check`, `check-candid`, `check-stable`, `test`, `bench`, `generate candid`. `mops sources` deliberately has none — the dfx packtool invokes it in the middle of a build and machine-parses its stdout, so failing there is a poor place to report a stale lock. Put `mops install --locked` earlier in the pipeline. Its stdout and the lock's mtime are unchanged by this PR. Dependency-mutating commands (`add`, `remove`, `update`, `sync`) have no `--locked`: their job is to change dependencies, so they always update the lock. ## Lockfile commit guidance is now "everyone commits" `mops.lock created.` used to print `Libraries: add mops.lock to .gitignore.` It now prints `Commit this file.`, and the docs say the same. A library's lockfile cannot pin anything for its consumers — they resolve their own graph and write their own lock — while committing it makes the library's own CI reproducible. If you have `mops.lock` in `.gitignore` because of the old advice, remove it. ## What `--locked` checks, and one thing it does not `--locked` requires that the lock is present, parseable, the current format version, pins every dependency declared in `mops.toml` to the same value, has `deps` and `hashes` agreeing on the registry package set, and records a hash for every file that matches the registry. It never writes `mops.lock`. It does **not** re-walk the dependency graph to byte-compare a freshly computed lock, which is what I originally implemented. That design does not work, and the reason is worth recording: Installing from a lockfile passes `ignoreTransitive: true`, which is the whole point — it skips downloading the dependency versions that *lost* a version conflict. Those versions' `mops.toml` files are therefore never in the cache, so `resolvePackages({skipLock: true})` throws on them. On a simulated fresh clone (committed lock, cold cache, a diamond where `base@0.10.2` loses to `base@0.14.9`) the re-walk crashed: ``` Error: ENOENT: no such file or directory, open '.../packages/base@0.10.2/mops.toml' at readConfig (cli/mops.ts:265:17) at collectDeps (cli/resolve-packages.ts:144:24) ``` The only ways to make the re-walk sound are to install the losing versions too (giving up the lockfile's main performance benefit, in exactly the CI case `--locked` targets) or to fetch each losing candidate's manifest individually (a new feature, and a network call per candidate). Neither belongs here. The walk-free checks above cover the realistic drift instead, and published registry versions are immutable, so a transitive version cannot change underneath a lock. The residual gap is transitive changes reached through a local `path` dependency, which are live directories by design. This is the same structural limitation the resolver-correctness work hit (#679): a valid lock short-circuits resolution, so a fresh clone never re-walks the graph and never sees the cross-major conflict report. It cannot be closed from the lockfile side either, for the reason above. Recording it here rather than leaving it to be rediscovered. ## Performance The removed re-hash was proportional to the whole dependency tree and paid on every install, build, check and test. Measured on an 842-file / 33.5 MB tree (20 packages): | | median warm `mops install` | |---|---| | before | 1.75 s | | after | 1.59 s | Isolated, the re-hash itself was ~136 ms (5 runs, 134–149 ms) on a warm page cache and a local SSD. So this is an honest but modest constant-factor win at this tree size — Node startup dominates a warm install — and it scales linearly with tree size and degrades on cold page cache, container overlay filesystems and networked volumes. The primary motivation is the guarantee model; the speed-up is a secondary benefit, and I would not sell it as the headline. ## Also in here - Plain `mops install` now migrates a lock that still carries absolute local `path` entries written by a pre-2.19.2 CLI. That previously required an explicit `mops install --lock update`, which no longer exists, so `checkLockFileLight()` treats such a lock as stale. - Fixed `mops install` running the dependency-conflict check after a *failed* install, which crashed with an unhandled ENOENT on manifests the failed install never wrote. Pre-existing, but the new download-verification failure path makes it easy to hit. - Deleted `cli/helpers/deprecate-ci-lock.ts` (nothing else used it) and the dead `checkRemote()` export. - A lockfile that is valid JSON but structurally wrong (missing or non-object `deps`/`hashes`) is treated as corrupt and self-healed, rather than crashing with an unhandled `TypeError`. Shape is now validated at the read boundary. - `checkLockFileLight` (which decides whether to install from the lock) and the `--locked` gate are derived from one `inspectLockFile` function, so they cannot disagree. When they did, `--locked` accepted a lock that install then declined to install from, silently falling back to re-resolving `mops.toml`. - Restored the `mops check-candid` `--help` description, dropped by accident when `--locked` was wired in. ## Verification Baseline on `kamil-v3/hidden-state`: 25 suites / 201 tests / 73 snapshots. Now 26 / 229 / 73, all green (the count includes tests arriving with the merged base). `npm run check` and `npm run lint` clean. New tests in `cli/tests/locked.test.ts` (targeted assertions, no snapshots, per AGENTS.md) cover: missing lock, missing lock on each of `build`/`check`/`check-stable`/`test`/`bench`, up-to-date lock left byte- and mtime-identical, changed manifest, unparseable lock, legacy format, locked version disagreeing with `mops.toml`, locked hash disagreeing with the registry, and the install-tolerates / verify-rejects pair. `mops verify` gets happy path, missing lock and not-installed cases. They use their own fixtures because Jest runs test files in parallel and sharing `install/success` races on `mops.lock`. Two existing tests changed intent deliberately: - `build.test.ts` "fails on a locally modified `.mops/` file" became "tolerates ... that mops verify rejects" — that is the guarantee change. - `local-path-lock.test.ts` "`--lock update` rewrites absolute local paths" became "plain install rewrites ...". Beyond the suite, I ran a 25-case matrix by hand against the live registry covering the same ground plus `CI=1` no longer implying check, `--lock` being rejected on all five commands, `--locked` being accepted on all seven implicit commands, `mops sources` having none, and the fresh-clone cold-cache path that caught the design flaw above. Stacked on #677, and synced onto its reworked head. Two conflicts, both resolved in favor of the newer decisions: - `cli/cli.ts` — #676 made unknown flags before `--` strict by removing `allowUnknownOption(true)`. Kept that; `--locked` is a declared option so strict parsing accepts it, and the `-- <tool flags>` passthrough still works (verified for `check --nope`, `build --locked`, `test -- -Werror`). - `cli/CHANGELOG.md` — kept all twelve entries from #677/#676. Dropped this branch's network-relocation entry, superseded by #677 deleting `set-network`/`get-network` outright. Kept this branch's version of the entry for the seven implicitly-resolving commands; #677's still referenced the removed `--lock update` flag and the `.mops/` tamper guarantee that download-time verification replaces. `cli/integrity.ts` auto-merged to this branch's version unchanged — the `defaultLock`/CI-lock machinery it replaced left no residue. Confirmed against the reworked `cleanCache()` (now plain whole-tree removal of both caches, with the `PROJECT_STATE_FILES` special case reverted): nothing in the lock or verify work depends on it. `mops.lock` lives at the project root, never inside `.mops/`, so a full `mops cache clean` leaves it byte-identical, after which `install --locked` re-downloads and re-verifies without writing the lock, and `mops verify` passes. `.mops/` is pure derived state. Retarget with `gh pr edit 681 --base v3` once #677 merges.
# Conflicts: # DEVELOPMENT.md # cli/CHANGELOG.md # docs/docs/cli/1-deps/01-mops-add.md # docs/docs/cli/1-deps/01-mops-remove.md # docs/docs/cli/1-deps/02-mops-install.md # docs/docs/cli/1-deps/04-mops-update.md # docs/docs/cli/1-deps/05-mops-sync.md # docs/docs/cli/5-toolchain/02-mops-toolchain-init.md # docs/docs/cli/7-misc/03-mops-cache.md
The v3 branch kept writing site-absolute in-body links while main moved to relative ones (#689), so the merge reintroduced 19 of them. Docusaurus does not version-resolve absolute paths, so once 2.x is frozen these send readers across versions. Relative links are anchor-checked at build time, which immediately caught a broken one: the install page pointed at `#--conflicts` on the sources page, whose heading renders as `--conflicts-action`. Absolute links had been hiding it.
`dfx` is no longer part of how mops works. Replica tests and benchmarks run on PocketIC that mops downloads and manages itself, the compiler comes from `[toolchain] moc` and nowhere else, and `mops init` no longer asks the registry for a package set keyed on your `dfx` version. A project can now use every mops command with no `dfx` installed at all. That was blocked on two things, which is why they are here rather than in follow-ups: with the dfx-bundled PocketIC gone, an unpinned `pocket-ic` needed a default to fall back to; and with the dfx replica gone, `@icp-sdk/core` could finally move to 5.x, since 5.x drops the IC HTTP API `v2` endpoints that only that replica served. Closes #652 Closes #653 ## Before / After An unpinned project, `mops test --mode replica`: **Before** — silently borrows dfx's replica, with a deprecation warning that has been printed since 2.14. ``` $ mops test --reporter verbose Test files: • test/hello.test.mo ================================================== Using `dfx` replica because no `pocket-ic` version is set in `[toolchain]`. The `dfx` replica is deprecated and will be removed in a future release. Run `mops toolchain use pocket-ic 12.0.0` to pin a PocketIC version and silence this warning. PASS Running test/hello.test.mo (replica) ================================================== Tests passed Done in 8.61s, passed 1 ``` **After** — mops fetches its own default the first time and says so: ``` $ mops test --reporter verbose Test files: • test/hello.test.mo ================================================== pocket-ic is not pinned in [toolchain]; downloading the mops default 14.0.0. Run `mops toolchain use pocket-ic 14.0.0` to pin it. PASS Running test/hello.test.mo (replica) ================================================== Tests passed Done in 9.32s, passed 1 ``` and on every run after that, nothing extra: ``` $ mops test --reporter verbose Test files: • test/hello.test.mo ================================================== PASS Running test/hello.test.mo (replica) ================================================== Tests passed Done in 5.35s, passed 1 ``` The process it actually starts, in both the pinned and unpinned case, is the toolchain-managed binary: ``` $ ps -Ao args | grep -E '^/\S*/pocket-ic ' /Users/…/Library/Caches/mops/pocket-ic/14.0.0/pocket-ic --port-file /var/…/pocket_ic_91518.port --ttl 60 # unpinned /Users/…/Library/Caches/mops/pocket-ic/12.0.0/pocket-ic --port-file /var/…/pocket_ic_15736.port --ttl 60 # pinned 12.0.0 ``` `mops bench` is the same, and `--verbose` now names the replica it used: ``` $ mops bench --verbose Benchmark pipeline: compiler: moc 1.3.0 replica: pocket-ic 14.0.0 gc: incremental (forced) context: update persistence: enhanced profile: Release optimize: none (raw moc output) ``` ## The default pocket-ic version `DEFAULT_POCKET_IC_VERSION = "14.0.0"`, in `cli/commands/toolchain/pocket-ic-versions.ts`. It is the version the bundled `@dfinity/pic` 0.23.0 pins for its own postinstall, so it is the server that client is actually tested against. When the client is upgraded, re-run the replica tests and move the constant with it. It is a **fixed constant compiled into the CLI, never a runtime "latest" lookup**. That is the Caffeine offline-runtime requirement: a cache warmed at Docker-image build time has to mean runtime never touches the network. Verified rather than assumed — with the cache warm and every non-loopback DNS lookup, `net.connect` and `fetch` blocked by a preload, a full unpinned replica run completes and `mops toolchain bin pocket-ic` still answers: ``` $ NODE_OPTIONS="--require ./no-net.cjs" mops test --reporter verbose Test files: • test/hello.test.mo ================================================== PASS Running test/hello.test.mo (replica) ================================================== Tests passed Done in 5.52s, passed 1 EXIT=0 $ NODE_OPTIONS="--require ./no-net.cjs" mops toolchain bin pocket-ic /Users/…/Library/Caches/mops/pocket-ic/14.0.0/pocket-ic EXIT=0 ``` The blocker is on the path — a command that genuinely needs the network fails loudly under the same preload: ``` $ NODE_OPTIONS="--require ./no-net.cjs" mops toolchain info pocket-ic NETWORK BLOCKED: fetch https://api.github.com/repos/dfinity/pocketic/releases?per_page=100&page=1 ``` ## Version bounds: floor only, no ceiling `MIN_POCKET_IC_VERSION = "9.0.0"` is a hard error, and it is a **migration guard, not a compatibility policy**. `< 9.0.0` pins really did work in 2.x through the legacy `pic-ic` client that this PR deletes, so without the check an upgrade produces an opaque `BinTimeoutError` from the client instead of: ``` $ mops test Error: pocket-ic 4.0.0 is no longer supported. mops 3.0.0 removed the legacy PocketIC client, so pins below 9.0.0 no longer work. Run `mops toolchain use pocket-ic 14.0.0` to move to a supported version. ``` There is deliberately **no upper bound**. An earlier draft enforced a supported range with the ceiling at the newest version we had tested. That was wrong: mops applies no version gating to `moc`, `wasmtime` or `lintoko` — the only version check in the CLI is `[requirements]`, which surfaces a *dependency-declared* minimum, not a mops-maintained list of blessed versions. A ceiling would have been a new and inconsistent policy, and it would have made our release cadence a gate on DFINITY's: pocket-ic 15 ships, works, and nobody can use it until we cut a release. A newer pin is simply used; if the protocol has moved, the client's own error says so. `latest` resolves to the actual latest, like every other tool. The blast radius of the `pic-ic` removal is smaller than "everything below 9.0.0" suggests: `pic-ic@0.5.4` only speaks the 4.0.0 protocol, so `5.x`–`8.x` pins already failed with `BinTimeoutError` today. Only `4.0.0` and `9.x`+ ever worked. ## `@icp-sdk/core` 5.x and the vendored bundle `4.0.2` → `5.4.0`, the same major `@dfinity/pic` 0.23.0 depends on, so there is exactly one copy in the tree: ``` $ npm ls @icp-sdk/core ic-mops@2.20.0 +-- @dfinity/pic@0.23.0 | `-- @icp-sdk/core@5.4.0 deduped `-- @icp-sdk/core@5.4.0 ``` With the majors aligned, `vendor:pic` passes `--external:@icp-sdk/core --external:@icp-sdk/core/*` instead of inlining pic's own copy — `dist/vendor/pic.mjs` drops from 1,172,937 to 614,603 bytes. Sharing one copy is also what removes the `AnyPocketIcServer` / `AnyPocketIc` / `AnySetupCanister` unions: they existed because the two copies produced structurally identical but mutually unassignable `IDL` types. `cli/tests/vendor-pic.test.ts` asserts the bundle resolves `@icp-sdk/core` at runtime rather than carrying it. ## Migration | If you… | Do this | |---|---| | pass `--replica dfx` or `--replica pocket-ic` | Drop the flag. There is no replacement; PocketIC is the only replica. `--replica dfx` was deprecated with a warning since 2.14. | | relied on the dfx-bundled `moc` (no `[toolchain] moc`) | `mops toolchain use moc <version>` once, commit `mops.toml`. Every command that compiles now requires the pin and errors naming this fix. | | pin `pocket-ic` below `9.0.0` | `mops toolchain use pocket-ic 14.0.0`. | | have `dfx = "..."` in `[package]` | Delete the line. Publishing now fails on it — deprecation since 2.7 was docs-only, so this is a hard break with no prior runtime warning. | | have recorded benchmark baselines | Re-record with `mops bench --save`. PocketIC and the dfx replica report different instruction and heap counts, so any project that was implicitly on a dfx replica will see a large diff on the first `--compare`. This is a change of measuring instrument, not a regression. | | have `"profile": "Debug"` in `dfx.json` | Nothing to do, but note `mops bench` no longer reads it and always compiles `--release`. Those projects were silently benchmarking debug builds. | | ran `mops init` for the default package set | It no longer contacts the registry; a fresh `mops.toml` has no `[dependencies]`. Use `mops add core`. | | point `MOPS_NETWORK=local` / `MOPS_REGISTRY_HOST` at a replica | It has to serve the IC HTTP API `v3` synchronous-call endpoint, which is what `@icp-sdk/core` 5.x uses for update calls. `icp` and recent `dfx` do. The default `ic` network and `staging` are unaffected. | ## What is unchanged Everything dfx-facing that exists for projects that *deploy* with dfx stays, deliberately: - `mops sources` as a `dfx.json` packtool, including the "no `--locked`, stdout is machine-parsed" contract. - `mops toolchain init` and the `DFX_MOC_PATH=moc-wrapper` bridge. Removing it would mean type-checking with the pinned `moc` and deploying with a different one. `moc-wrapper` drops `--fallback` and now requires the pin. - `mops init` writing `defaults.build.packtool` into an existing `dfx.json`. - `mops watch --deploy` / `--generate` shelling out to `dfx`, and `readDfxJson` for canister discovery. `mops watch`'s *replica* path did change — it went to PocketIC with everything else. ## Verification `cli/` Jest went from 250 passing on `origin/v3` to 254, with 29 suites green either way. The new coverage is the part types cannot check: - `cli/tests/pocket-ic.test.ts` — a pinned replica run, an **unpinned** one against the new default, and the `< 9.0.0` rejection. - `cli/tests/build-no-dfx.test.ts` — now puts a `dfx` on `PATH` that exits 127 the way a missing one does, and asserts `mops build` and `mops check` are unaffected. Previously it only pinned `moc` and trusted that dfx was never reached. - `cli/tests/vendor-pic.test.ts` — the bundle leaves `@icp-sdk/core` external. `npm run check`, `npm run lint` and `npm run build-docs` are clean. ## Follow-up worth flagging `mops init` no longer pins a compiler and no longer adds dependencies, so `mops init && mops build` in an empty directory now stops at `Tool 'moc' is not defined in [toolchain] section in mops.toml`. The error names `mops toolchain use moc <version>` and Quick Start makes it a numbered step, but having `mops init` offer to pin the latest `moc` would be better — that is a network call and a ~30 MB download at init time, which deserves its own decision rather than being smuggled in here.
Tagging `cli-v3.0.0-beta.N` on `v3` publishes a preview to npm under the `next` dist-tag and deploys the docs site. `npm i -g ic-mops` keeps serving 2.x; preview users opt in with `npm i -g ic-mops@next`. The preview path lives inside `release.yml` rather than a workflow of its own, because npm allows one trusted publisher per package and it is bound to that file — a second workflow could not authenticate. ## Prerelease behaviour Detected from the parsed version, not the tag text, so a malformed tag aborts instead of falling through to the stable path. | | stable | prerelease | |---|---|---| | tag must be on | `main` | `v3` | | npm dist-tag | `latest` | `next` | | cli-releases canister | uploaded | skipped | | artifacts commit | yes | skipped | | GitHub release | release | prerelease | | docs deploy | yes | yes | After publishing a preview the job asserts `latest` is not a prerelease, so a mistake here cannot silently redirect every `npm i -g ic-mops`. Worth knowing: `cli-v*` already matched `cli-v3.0.0-beta.1` before this change, so tagging a prerelease would have run the full stable path — publishing to `latest` and bumping the production pin. ## Docs `lastVersion` moves to `2.x`, so the released line serves at the site root and the in-development line at `/next`. Flip it back at GA. Docs deploy moves from dfx to icp-cli. It is the smaller of the two mainnet deploys and the natural first one to move; the cli-releases canister deliberately stays on dfx so only one production path changes at a time. The deploy asserts that `https://docs.mops.one/.well-known/ic-domains` still names the domain. That file is served only because `docs/static/.ic-assets.json` opts dot-directories into the upload, and losing it takes the custom domain offline. Rollback is the previous command: ``` dfx deploy --network ic --no-wallet docs --identity mops ``` ## Known interaction: one docs canister, two branches Both `main` and `v3` deploy the docs canister, and they carry different `lastVersion` values, so whichever released most recently decides the layout. A stable 2.x release during the preview period restores 3.x-at-root and removes `/next`, invalidating `/next` links in earlier preview notes. Recovery is re-running the v3 docs deploy; it resolves itself at GA when the configs converge. Not gated, because gating `main` would stop docs-only fixes there from ever publishing. Landing this `lastVersion` on `main` was also rejected: `main`'s `docs/docs` *is* the 2.x content, so the flip would serve the frozen snapshot at the root and push main's live docs to `/next` labelled "3.x (unreleased)". ## Unproven until the first real tag No mainnet deploy is exercisable from CI, so these run for the first time when a preview is tagged: - `icp identity import` on a headless Linux runner - `icp canister link` against a runner with no ID store - `icp deploy` to the docs canister, and whether its uploader honours `.ic-assets.json` for `.well-known` - the npm publish and the dist-tag read-back - the prerelease `if:` conditions, which were read off the parsed workflow but never triggered Verified locally: the docs build with both URL trees present, prerelease detection across valid and malformed versions, `npm version` keeping `package-lock.json` in sync, and icp-cli importing the existing dfx pem to the same principal — so `MOPS_IDENTITY_PEM` needs no change. ## Follow-up `icp canister link` writes `.icp/data/mappings/<env>.ids.json` and `.gitignore` only covers `.icp/cache/`, so local deploy state is committable. Pre-existing since #550.
Deploying now needs `icp`, never `dfx`. `dfx.json`, `dfx.schema.json` and `canister_ids.json` are deleted; `icp.yaml` is the only deploy config left. The commands keep their names: ```bash npm run deploy-staging # main + assets on the staging canisters npm run deploy-ic # everything the ic environment declares npm run deploy-ic blog # or one canister ``` Canister IDs move into icp-cli's own store. It cannot declare an ID in the manifest, but it splits its state: `.icp/cache/` is machine-local, while `.icp/data/mappings/<environment>.ids.json` is [meant to be committed](https://cli.internetcomputer.org/1.2/migration/from-dfx/#4-migrate-canister-ids-optional), which is what lets a fresh clone deploy without relinking. Both mainnet mappings are in this PR. Deploys pass `--no-create`, so a missing mapping fails loudly instead of creating a second production canister. Three IDs `canister_ids.json` held — `dao-frontend`, `dao-backend`, `play-backend` — have no source in this repo and nothing deploys them. Dropped rather than carried; git history has them. Follow-up to [#692](#692), which moved the docs canister and deliberately left everything else on dfx. ## Two footguns closed on the way `icp.yaml` now declares both mainnet environments explicitly rather than inheriting "every canister". `ic` excludes `bench`, which `dfx.json` kept off mainnet with a `remote` id. `staging` covers only `main` and `assets`: `docs`, `blog` and `cli` have no staging canister of their own, so a bare `deploy-staging` was a production deploy under another name. `network:` on an environment defaults to `local`, so an environment named `ic` without it would deploy nothing, quietly — hence the comment in the file. The frontend selects its network with `MOPS_FRONTEND_NETWORK` instead of `DFX_NETWORK`, and it is required — no silent default. Deliberately *not* icp-cli's own `ICP_ENVIRONMENT`: `-e` wins for icp but an exported `ICP_ENVIRONMENT` still reaches vite, so the two could disagree and bake local replica ids into a mainnet bundle. `npm run deploy` derives it from the value it passes to `-e`, so they cannot; a raw `icp deploy assets` leaves it unset and fails instead of shipping. Previously a build with no network set took ids from the local replica while baking `MOPS_NETWORK = "ic"`. ## The build assertion was decorative `frontend/verify-build.mjs` checked that the bundle contained *some* principal-shaped string. But `cli/api/network.ts` hardcodes the ic and staging endpoint ids and the frontend bundles that module, so a principal is always present — the check passed on a build with no canister mappings at all, which is precisely the bundle it exists to reject. Confirmed by deleting the mappings and rebuilding: exit 0, no ids baked in. It now asserts the specific `main` id read from the mappings file, and fails without them. ## The composite action is parameterised, not duplicated `deploy-docs` becomes `deploy-canister`, taking the canister name, its custom domain, and the directory whose `npm ci` its build step needs. A second action would mean a copy of the identity import and the retry loop, and both canisters' baseline health check is the same assertion — the custom domain resolves only while `.well-known/ic-domains` is served, and that file is only uploaded because an `.ic-assets.json` opts dot-directories in. What is specific to the `cli` deploy stays in `release.yml`: `/tags/latest` must name the new version and both tarballs must unpack, which is what `mops self update` and the `mops-test.yml` matrix consume. icp-cli and the identity are installed and imported once for the job, not once per canister. Both deploys run on the same runner, so a per-canister import would only give `icp identity import` — which has no `--force` — an existing name to fail on, and it keeps `MOPS_IDENTITY_PEM` to a single step instead of passing it across the action boundary twice. ## What is unchanged Everything in `cli/` that supports dfx *users* — `mops sources` as a packtool, `mops toolchain init`, `mops watch --deploy`, `mops init`'s packtool write — is untouched. A separate PR removes those, and it has to land after this one: we cannot tell users to drop dfx while our own release still needs it. `setup-dfx` stays in `mops-test.yml` and `setup-mops.yml`, which was the one thing I expected to delete. Those jobs need dfx as a *replica*, not a compiler: `test/storage-actor.test.mo` runs in replica mode, `mops.toml` pins no `[toolchain] pocket-ic`, and mops 1.x/2.x fall back to the dfx replica. Pinning `pocket-ic` would not rescue `setup-mops.yml`, which runs mops 1.0.0, and 1.0.0 speaks only the PocketIC 4.0.0 API. Both jobs now pass `dfx-version` explicitly, because `setup-dfx`'s `auto` mode reads it out of `dfx.json` (`jq -er '.dfx // ""' dfx.json`) and with that file deleted would have silently installed the latest dfx. In `mops-test.yml` the install is skipped for the current CLI, so a regression that made 3.x need dfx fails there instead of passing on a borrowed binary. Argument forwarding through the nested `npm run` also survives — a previous AI review flagged it as broken and it is not. npm 10 appends positionals to the already-named inner script rather than reading them as a script name: ``` $ npm run deploy-ic blog WOULD RUN: icp deploy -y --no-create --identity mops -e ic blog ``` ## Rollback The `cli` canister, from a checkout of the previous release commit: ```bash icp deploy cli -e ic --identity mops --no-create --yes ``` Same shape for `docs`. The failure message in the action prints it too. ## Unproven until a real release Neither canister deploy can be exercised against mainnet from a PR. The local replica covered the rest, including a full `cli` deploy through `icp.yaml`'s `cli` entry — the first time it has actually run. Untested until the next `cli-v*` tag: - `icp deploy --no-create` upgrading the live `cli` and `docs` canisters, and the asset sync diffing against their existing contents rather than an empty one. - The identity import from `MOPS_IDENTITY_PEM`, and the delete-then-import that now runs for the second canister. - Every post-deploy assertion against the real `cli.mops.one` / `docs.mops.one`. - `npm run deploy-staging` / `deploy-ic`, which no workflow runs. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Mops no longer supports `dfx` in any way, and this repo no longer installs it anywhere. [#691](#691) took dfx out of mops's own internals — the dfx replica, the dfx-bundled `moc` fallback, `mops init`'s dfx default packages. What it deliberately left behind was the surface that existed to serve users who *deploy* with dfx. That goes now. Gone: `mops toolchain init` and `mops toolchain reset`, the `moc-wrapper` binary the published package installed, `mops watch --deploy` and `mops watch --generate`, and `mops init` writing a packtool entry into a `dfx.json` it happens to find. `mops sources` stays, unchanged down to its stdout, because it is tool-agnostic — it prints `--package` flags and has no opinion about who reads them. ## The repo's own last dfx dependency goes too `mops-test.yml` and `setup-mops.yml` were the only workflows still installing dfx, and only ever as a *test replica* for `test/storage-actor.test.mo`. Both are gone, so nothing in this repo touches dfx. The published 2.x CLI picks its replica with `config.toolchain?.["pocket-ic"] ? "pocket-ic" : "dfx"` ([cli-v2.20.0 `test.ts:68`](https://github.com/caffeinelabs/mops/blob/cli-v2.20.0/cli/commands/test/test.ts#L68)) — it reached for dfx purely because `mops.toml` pinned no version. Pinning `[toolchain] pocket-ic = "14.0.0"` switches it to PocketIC, and 2.x and 3.x ship the same `@dfinity/pic` 0.23.0 client, so one pin serves the whole matrix. `setup-mops.yml`'s `mops-version: 1.0.0` dated from 2024 (`cfa434ce`) and was the one version no pin could rescue — 1.0.0 speaks only the PocketIC 4.0.0 API. Dropping the input lets the action install its `latest` default, which is what that workflow exists to test. Verified with `dfx` stubbed to exit 127 and print on invocation: | | result | |---|---| | 3.x CLI, pin | 122 passed, `storage-actor.test.mo (replica)` included, stub untouched | | 2.20.0, pin | 20 files passed, replica cases included, stub untouched | | 2.20.0, **no** pin (control) | ``Using `dfx` replica because no `pocket-ic` version is set`` → `DFX WAS INVOKED: --version` | The control matters: without it, a green run only proves the tests pass, not that removing dfx is what made them stop using it. ## Migration **If you deploy with dfx, read this.** `mops sources` still works as a `dfx.json` packtool, so dfx keeps resolving your mops dependencies. What no longer reaches dfx is the *compiler*. `mops toolchain init` existed to write `export DFX_MOC_PATH=moc-wrapper` into your shell config; `moc-wrapper` then resolved the `moc` pinned in `[toolchain]` and handed it to `dfx build`. Without that bridge, `dfx build` uses the `moc` bundled with dfx, while `mops check` / `mops build` / `mops test` use your pinned one. **Type-checking and deploying with two different compilers is a real hazard** — a program that passes `mops check` can still fail to build, or build differently, under `dfx deploy`. It is the direct consequence of this change and worth being explicit about rather than discovering later. The supported path is [`icp`](https://js.icp.build/), whose Motoko recipe builds each canister by invoking `mops build`, so the pin propagates with nothing in between. If you stay on dfx, set `DFX_MOC_PATH` yourself and keep the compiler it points at in step with `[toolchain] moc`. Everything else: | Removed | Do instead | |---|---| | `mops toolchain init` / `reset` | Nothing to run. Remove the `export DFX_MOC_PATH=moc-wrapper` line `init` added to your shell config. | | `mops watch --deploy` | `icp deploy` (or `dfx deploy`) in a second terminal. | | `mops watch --generate` | `icp-bindgen`, or `dfx generate`, in a second terminal. | | `mops watch -tgd` | `mops watch -t`. Since [#676](#676) rejects unknown options, the bundle now fails with `unknown option '-gd'` rather than quietly ignoring the two dead letters. | | `mops init` writing `defaults.build.packtool` | Add `"packtool": "mops sources"` under `defaults.build` by hand. | ## Before / after `mops toolchain --help`: ```diff Commands: - init [options] One-time initialization of toolchain management - (updates the current shell's config file) - reset Uninstall toolchain management (cleans all known shell - config files) use <tool> [version] Install specified tool version and update mops.toml update [tool] Update specified tool or all tools to the latest version and update mops.toml info [options] <tool> Show release information about a toolchain tool bin <tool> Get path to the tool binary ``` ``` $ mops toolchain init error: unknown command 'init' (Did you mean info?) ``` `mops watch --help`, before: ``` Watch *.mo files and check for syntax errors and warnings and format code. Pass flags to run only the selected tasks; --test, --generate and --deploy are opt-in only Options: -e, --error Check Motoko canisters or *.mo files for syntax errors (on by default) -w, --warning Check Motoko canisters or *.mo files for warnings (on by default) -f, --format Format Motoko code (on by default) -t, --test Run tests (opt-in) -g, --generate Generate declarations for Motoko canisters (opt-in) -d, --deploy Deploy Motoko canisters (opt-in) -h, --help display help for command With no flags, runs the default set: errors, warnings and formatting. Passing any flag runs only the selected tasks (error checking is always on). Tests, declaration generation and deploys never run unless requested: $ mops watch -t # errors + tests $ mops watch -tgd # errors + tests + generate + deploy ``` after: ``` Watch *.mo files and check for syntax errors and warnings and format code. Pass flags to run only the selected tasks; --test is opt-in only Options: -e, --error Check *.mo files for syntax errors (always on) -w, --warning Check *.mo files for warnings (on by default) -f, --format Format Motoko code (on by default) -t, --test Run tests (opt-in) -h, --help display help for command With no flags, runs the default set: errors, warnings and formatting. Passing any flag runs only the selected tasks (error checking is always on). Tests never run unless requested: $ mops watch -t # errors + tests $ mops watch -tw # errors + tests + warnings ``` The `-e` / `-w` descriptions said "Motoko canisters or *.mo files" because canister discovery came from `dfx.json`. Both checkers ignored that list entirely — they glob `**/*.mo` — so the field was dead as well as misleading, and it is dropped along with `parseDfxJson.ts`. `mops sources --help`: ```diff -for dfx packtool +Print the resolved dependencies as `--package` flags for the Motoko compiler ``` ## Why `--generate` was dropped rather than ported `--deploy` being dropped was decided up front: it shells out to `dfx ping` / `dfx canister create` / `dfx build` / `dfx canister install`, and mops is not a deployment tool. `--generate` needed a decision. It turned out to be a thin wrapper around `dfx generate <canister>` — a dfx binary invocation, not merely a `dfx.json` read — with the canister list coming from `dfx.json` entries carrying a `declarations` field. `dfx generate` emits JavaScript/TypeScript bindings, which mops has never produced: `mops generate candid` writes `.did` files and nothing more. Re-pointing it at `[canisters]` in `mops.toml` would not have helped, because there is no mops-side generator behind it. So it goes with the deployer. `globMoFiles.ts` also matched a `dfx` grep, but only in its ignore list (`**/.dfx/**`, alongside `**/node_modules/**`). Skipping a build directory a user may still have on disk is not dfx support, so it stays. ## What is unchanged - **`mops sources`** — same behaviour, same flags, and stdout verified byte-identical against `origin/v3` on a fixture project. The note that its stdout is machine-parsed (hence `lock: "skip"` and no `--locked`) is kept; that constraint holds for any caller, not just dfx. - **`mops publish` rejecting `package.dfx`** — a migration error for a field v3 removed, not dfx support. - **`mops user import` accepting dfx-exported keys** — a PEM is a PEM. The docs now lead with `icp identity export` and note dfx's works too. - **`cli/tests/build-no-dfx.test.ts`** — still valuable as a regression test that mops works with no `dfx` on `PATH`. - **The repo's own `dfx.json` and deploy scripts** — out of scope here, see below. ## Sequencing Must not merge before [`kamil-v3/deploys-on-icp`](https://github.com/caffeinelabs/mops/tree/kamil-v3/deploys-on-icp), which moves this repo's own deploys off dfx. Telling users to drop dfx while our release pipeline still needs it would not be credible. Expect a conflict in `AGENTS.md`, and possibly `DEVELOPMENT.md`, since both branches touch the dfx guidance. ## Packaging `bin/moc-wrapper.sh` was the only reason `/bin` appeared in `files`, so both are dropped from `cli/package.json`. `npm pack --dry-run` confirms the tarball no longer carries `moc-wrapper` in any form; `dist/bin/mops.js`, which the `mops` and `ic-mops` bins point at, is unaffected. ## Verification `cli/` Jest: **29 suites, 257 tests, 75 snapshots**, all passing — against 29 / 254 / 75 on `origin/v3`. The count went up, not down: no tests were deleted, because none existed for any of the removed surfaces. Worth noting on its own — `mops toolchain init`, `mops watch --deploy` and the `sources` description were all uncovered. The three new ones cover `toolchain init` / `toolchain reset` being unknown commands and `mops watch --help` no longer advertising the dfx tasks. `npm run check` and `npm run lint` are clean. Manually: `mops toolchain init` and `reset` both report `unknown command`; `mops watch` still runs errors, warnings and formatting to completion; `mops init --yes` in a directory containing a `dfx.json` leaves that file byte-for-byte untouched. The three snapshot files changed only because the path placeholder in `tests/helpers.ts` was literally `moc-wrapper` — a name that no longer refers to anything. It is now `<MOC>`. No assertion changed. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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>
Housekeeping from the pre-GA audit. Three files, no code. **NEXT-MAJOR.md** — two lockfile bullets shipped with the `--locked` work (#516) but were never struck; struck now with what actually shipped. The "decide before release" custom-registry question is recorded as decided: **keep as supported** — `MOPS_REGISTRY_HOST` / `MOPS_REGISTRY_CANISTER_ID` are already documented in the 3.x env-vars page, so dropping them would now be the breaking change. With this, the only remaining unstruck items in committed scope are non-gating (GH #274, the release blog post, upstream-gated #651/#655). **cli/DEVELOPMENT.md** — was an old release doc wearing the wrong name: "Publish to npm", "Publish on-chain", ending in a `dfx deploy --network ic` that no longer exists (`dfx.json` is deleted; the pipeline owns deploys). Rewritten as an actual development doc: prerequisites with the reason each exists (gnu-tar is for `bundle:tar`'s `--sort name`), the dev loop, and reproducible-build verification pointing at `build.sh`. **cli/RELEASE.md** — the manual-deploy fallback still ran `dfx deploy`; now `npm run deploy-staging <canister>` / `deploy-ic <canister>`. Adds a **Preview releases** section, since that's how every 3.0.0 beta ships and it was documented nowhere: manual tag, and the exact set of deviations from a stable release (npm `next` dist-tag, GitHub prerelease, no `cli` canister upload so `mops self update` keeps serving stable, docs still deploy — previews are what publish `/next/`). ## Left alone deliberately `cli-builder/` still points at the `zenvoich/mops-builder` Docker Hub image. The `cli/Dockerfile` pins it by sha256 digest, so reproducible-build verification doesn't trust the registry — but the image is owned by an account nobody here controls, so it can't be updated, only replaced. Moving to a caffeinelabs-owned image is infra work, not docs; flagging it as an open decision rather than half-doing it here. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Closes [#685](#685). The `3.0.0 (unreleased)` section is now release notes rather than a merge log. **Structure**: a `### Migrating from 2.x` preamble — every action item in one list, one line each — followed by themed sections (dfx removal, PocketIC and toolchain, lockfile and integrity, dependency resolution, defaults and CLI strictness, removals, runtime). Every breaking entry leads with what to do; rationale is cut to a sentence with the PR reference carrying the detail. **Defects fixed, not just style** — the merge-order accretion had produced things a reader must never see: - **Duplicates**: Node >= 20 and the mocv removal were each listed twice. - **Never-shipped states**: the per-project `set-network` storage (`.mops/network`, `--global`) and the single-shell `toolchain init` rewrite both landed on v3 and were later superseded by outright removal on the same branch. A 2.20 → 3.0 reader would have "migrated" to commands that don't exist. - **Self-contradiction**: the `mops watch` entry advised reproducing the old default with `-wtgd`, but `-g`/`-d` are removed two entries up — following the advice produces `unknown option`. - **Removed-flag reference**: the exit-code-11 entry described `--replica dfx` bind failures; the flag and the code path are both gone, so the entry is subsumed by the dfx-removal section. 22.7k → 15k characters. Every technical claim that describes shipping behavior survives, including the fine-print ones (path-dep transitives not detected by `--locked`/`verify`, the hand-edited-hash limitation and its costed rationale, the `@icp-sdk/core` v3-endpoint requirement for custom registries). Verification was line-by-line against the old section; nothing was verified against code beyond what the original entries already claimed, except the exit-11 path (checked gone on v3 before dropping the entry). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…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>
…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>
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>
…708) The legacy asset recipe cannot redeploy a canister that already holds assets: its sync plugin either exhausts its sandbox memory after minutes of silence or emits a create-everything batch that traps with `'asset already exists'` ([dfinity/icp-cli#703](dfinity/icp-cli#703)). That is what broke the docs deploy in the `cli-v3.0.0-beta.1` release run — 37 silent minutes, then the job timeout. Only the empty-canister path works, and a reinstall buys exactly one deploy before the next one fails again. This moves `docs` to [`@dfinity/static-site@v0.3.3`](https://github.com/dfinity/certified-assets) — dfinity's current certified-assets canister, which Raymond pointed at ([skill](https://skills.internetcomputer.org/skills/static-site/)). The recipe tag pins the canister wasm and its sync plugin as a matched pair, and redeploys work incrementally, change detection included. Also gained in the move: - Unknown paths return a real **404**, using the `404.html` Docusaurus already builds. The legacy canister answers them with the root `index.html` at HTTP 200 — the soft-404 that forces every deploy assertion in `release.yml` and `.github/actions/deploy-canister/` to grep response bodies instead of statuses, and that served `/.ic-assets.json` as the intro page. - `.well-known/` uploads despite the leading dot, so the custom domain resolves with no opt-in file. `docs/static/.ic-assets.json` existed only to force that upload and is deleted — it was load-bearing for `docs.mops.one` and easy to delete by accident. - ETags on every response and certified 206 range responses (legacy sends neither). ## `trailingSlash: false` is required, not incidental The new canister canonicalises clean URLs. With Docusaurus's default output (`quick-start/index.html`) the canonical form becomes `/quick-start/`, so every deep link would 307 to a URL that disagrees with the `<link rel="canonical">` Docusaurus itself writes. `false` makes Docusaurus emit `<route>.html`, which the canister serves at the extension-less URL — byte-identical to today's URLs, at 200, with no redirect hop. Both variants were built and deployed to confirm this. ## Verification Full cutover simulation on a local replica, in production order — and step 3 is the exact case the legacy recipe fails at: | step | result | |---|---| | deploy with the legacy recipe (mainnet's state today) | 230 assets, 14s | | switch manifest, `icp deploy -m reinstall` | 230 assets, 21s | | **normal deploy on the populated canister** | 5s — change detection, no re-upload | | content change, normal deploy | 6s — new file served | | one more repeat deploy | 5s | Path parity against the legacy recipe over the same 230-asset build: `/`, `/quick-start`, `/next`, `/mops.toml`, `/sitemap.xml`, `/img/logo.svg` and `/.well-known/ic-domains` serve identically; `/does-not-exist` returns the Docusaurus 404 page instead of a soft 200; `/.ic-assets.json` no longer leaks. The `.well-known/ic-domains` assertion in `.github/actions/deploy-canister/` passes with no `.ic-assets.json` present. Neither canister sets `Cache-Control`, so the migration is header-neutral; a `_headers` file for content-hashed `/assets/*` is a follow-up, deliberately not bundled here. ## Cutover Not a plain deploy — the wasms have incompatible state, so `icp deploy`'s default `auto` mode picks an upgrade that cannot work. One-off, with a snapshot as the rollback handle (snapshots require the canister stopped; a stopped asset canister keeps serving queries, so this is not an outage): ``` icp canister stop docs -e ic --identity mops icp canister snapshot create docs -e ic --identity mops icp canister start docs -e ic --identity mops icp deploy docs -e ic --identity mops --no-create -m reinstall ``` The canister ID — and with it the custom-domain registration — survives; the cost is downtime for the length of the sync (~21s locally). Rollback is `snapshot restore`, which brings back the old wasm and assets together. Subsequent releases are ordinary deploys. ## Interaction with `main` [#709](#709) (merged) already removed `main`'s dfx docs deploy, which could not drive the new wasm. `v3` owns the docs canister: its build is a superset of `main`'s — the same `docs/versioned_docs/version-2.x/` at the site root plus the in-development line under `/next` — so 2.x doc edits reach production once merged into `v3`. ## Not migrated `assets`, `blog` and `play-frontend` are unchanged here. `cli` cannot move yet — `static-site` takes a single `dir` and that canister serves two (`cli-releases/` and `cli-releases/frontend/dist/`). Note `static-site` is 0.x, where a minor bump is breaking by its own policy (reinstall + full re-upload) — read release notes before moving the pin. All of this is written down in AGENTS.md. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude <noreply@anthropic.com>
`[optimize]` without a `[toolchain] wasm-opt` pin made `mops build` and `mops bench` ask GitHub for the latest Binaryen release and write the result into mops.toml. Two things a build must not do: mutate checked-in source, and let the network decide what it compiles with. It also made builds unreproducible by construction — the same commit built either side of a Binaryen release produced different artifacts. Build commands now fail before compiling anything and name the fix, matching how an unset `[toolchain] moc` is already handled. Validation runs in a preflight next to the existing canister checks, so a misconfigured project fails in milliseconds rather than after moc runs. A `wasm-opt` failure now fails the build too, instead of warning and keeping the unoptimized module. `[optimize]` describes the artifact, so substituting a different one left nothing for a downstream consumer that hashes, certifies or deploys it — and the warning scrolled past in CI. Adds a reproducibility test: two builds of one fixture into separate output dirs must produce identical .wasm/.did/.most. Verified passing, which also establishes that moc plus candid-metadata embedding is deterministic — previously unknown. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Contributor
Cursor AI review👍 APPROVE — looks safe to merge
VerdictDecision: APPROVE Generated for commit 3c6696c |
Everything else in the docs assumes a person at a terminal. A CI pipeline or canister orchestrator has different questions, and today has to reverse-engineer the answers: which commands write which files, what reaches the network and when, what the exit codes distinguish, and how to extract the facts a build record needs. The warning that prompted this: `--locked` is widely assumed to detect tampering with files on disk. It does not — 3.0.0 verifies integrity at download time — and `mops verify` is the gate that does. That was only recorded in the changelog. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Both query the registry for published file hashes on every run, so a warm cache removes the downloads but not the query. The page claimed a warm rebuild made no registry call at all, which would have led someone to design a no-network pipeline that fails at the lock check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Kamirus
force-pushed
the
kamil-v3/docs-build-integration
branch
from
August 11, 2026 11:48
6af6a28 to
61e9acf
Compare
automation-sa-sre
previously approved these changes
Aug 11, 2026
automation-sa-sre
left a comment
There was a problem hiding this comment.
Automated approval: the AI review verdict for 61e9acf is APPROVE. See the "Cursor AI review" comment for details.
…710) A project with `[optimize]` and no `[toolchain] wasm-opt` pin had its **`mops.toml` rewritten by the next `mops build` or `mops bench`**, with the version chosen by a "latest release" lookup against GitHub at build time. Two things a build must never do: mutate checked-in source, and let the network decide what it compiles with. It also made builds unreproducible by construction — the same commit, built either side of a Binaryen release, produced different artifacts. This was the last runtime "latest" lookup on the build path. `moc` and `pocket-ic` already resolve from a pin or a compiled-in constant; `wasm-opt` now does too. ## Before / after ```console $ cat mops.toml [toolchain] moc = "1.3.0" [optimize] $ mops build # before Pinned wasm-opt 131 in mops.toml ([optimize] enabled) build canister main $ git diff --stat # ...and your manifest changed mops.toml | 1 + ``` ```console $ mops build # after [optimize] is enabled but wasm-opt is not pinned in [toolchain]. Run `mops toolchain use wasm-opt 131` (or another version), or drop [optimize] from mops.toml. Pass --no-optimize to skip the pass for a single run. $ echo $? 1 ``` Validation runs in a preflight next to the existing canister checks, so a misconfigured project fails in milliseconds instead of after `moc` has run. ## `wasm-opt` failures now fail the build Previously a `wasm-opt` crash warned and kept the unoptimized module, exit 0. `[optimize]` describes the artifact you asked for, so silently substituting a different one left no signal for anything downstream that hashes, certifies or deploys it — and the warning scrolled past in CI logs. `--no-optimize` is still the way to skip the pass deliberately, and `--verbose` still prints full `wasm-opt` output. ## Builds are reproducible, and now tested Adds a test asserting that two builds of the same fixture into separate output directories produce byte-identical `.wasm`, `.did` and `.most`. This was an open question — nothing previously established whether `moc` plus mops' candid-metadata embedding was deterministic. It is, verified before writing the test. Note the compiler version is recorded in a `motoko:compiler` custom section, so changing `[toolchain] moc` changes the artifact hash by design. ## Migration Projects using `[optimize]`: pin Binaryen once and commit it. ```bash mops toolchain use wasm-opt 131 ``` Projects without `[optimize]` are unaffected. Anyone who previously relied on the auto-pin already has the pin in `mops.toml` from the first build that wrote it. ## What is unchanged `mops toolchain use wasm-opt` with no version still resolves the latest release — that command exists to write the manifest, and asking the network is its job. This only removes the resolution from commands that compile. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Kamirus
dismissed
automation-sa-sre’s stale review
August 11, 2026 12:04
The base branch was changed.
automation-sa-sre
previously approved these changes
Aug 11, 2026
automation-sa-sre
left a comment
There was a problem hiding this comment.
Automated approval: the AI review verdict for 3c6696c is APPROVE. See the "Cursor AI review" comment for details.
Kamirus
dismissed
automation-sa-sre’s stale review
August 19, 2026 18:00
The base branch was changed.
Kamirus
added a commit
that referenced
this pull request
Aug 20, 2026
Folds the 3.x line into `main` ahead of the 3.0.0 GA. 68 commits, 291 files. `main` becomes the 3.x development and release line; `v3` is deleted on merge. `v2` stays as an archive of the 2.x line with no release path. ## Squash this `main` is governed by ruleset `main` (id 20838766) with `required_linear_history` and no bypass actors, so a merge commit is rejected — `allowed_merge_methods: ["squash", "merge"]` is contradictory config that fails at merge time. Every one of main's last 40 commits has a single parent, so linear history here is deliberate and consistently applied, not incidental. **Squash and merge** is therefore the method. Before merging, tag the branch so the 68 commits survive its deletion: ```bash git tag v3-final origin/v3 && git push origin v3-final ``` Safe to push: `release.yml` triggers only on `cli-v*`, so `v3-final` fires nothing. Without it the commits become unreachable from any branch once `v3` is auto-deleted — recoverable only through `refs/pull/*/head`. Two alternatives, both rejected: - **Rebase-linearize onto `main`.** Would keep all 68 commits *and* satisfy linear history, but replaying them individually conflicts (first failure at `319951b3 chore(cli)!: require Node >= 20…`) even though the merge itself is clean. Resolving conflicts across 62 replays to land a promotion is not worth it. - **Temporarily drop `required_linear_history`, merge, restore.** Works and puts all 68 commits in main's log, but fights a policy the repo applies uniformly. The tag gets the same preservation without touching the ruleset. ## Still blocking - **One approving review.** `required_approving_review_count: 1` with no bypass actors, and the PR author cannot self-approve. - **Status checks**: `ci-ok`, `ci-ok-cli`, `ci-ok-mops`. ## No conflicts, and no sync needed Test-merged locally: clean, zero conflicts. The two `nanoid` GHSA bumps that were only on `main` ([#769](#769), [#770](#770)) merge without intervention — `nanoid 3.3.18` is present in both `blog/` and `docs/` lockfiles in the merged tree. An extra `main` → `v3` sync beforehand would have added a merge commit for nothing. I also checked that `v3` content wins everywhere it must: `lastVersion: 'current'` in the Docusaurus config, the `main`-only release guard, the consolidated `## Next` with no `## 3.0.0 (unreleased)` heading, no `cli/legacy-lock-flag.ts`, and the release post and 3.x `mops-verify` page both present. ## What lands Breaking changes, each with its migration in the changelog's **Migrating from 2.x** list: - `dfx` support removed outright — no `moc` fallback, no `--replica`, no `toolchain init` / `moc-wrapper`, no `watch --deploy` / `--generate`, `dfx` rejected in `[package]` - Every toolchain tool must be pinned: `moc`, and `pocket-ic` for replica tests / `bench` / `--check-deploy`, with no silent default. `[optimize]` requires a `wasm-opt` pin and no longer rewrites `mops.toml` from a build-time network lookup - One lockfile model: `--locked` replaces `--lock <check|update|ignore>`, plain installs self-heal, the `CI` env-var auto-detection is gone - Integrity verified at download time; `mops verify` is the on-disk audit - `.most` required for `check-stable` baselines - Node.js >= 20; `set-network` / `get-network` and vessel migration removed - Stricter defaults: unknown flags before `--` rejected, `watch` conservative, `test` verbose, `info --versions` newest-first Plus the install-engine work — parallel downloads, batched registry hash fetches, single-pass resolution, self-healing on transient network failures — and a long tail of resolution, `sync`/`add`/`remove` and cache fixes. ## After merging 1. Re-enable squash merging. 2. Rename `blog/blog/2026-08-19/` to the GA date — the post is dated by directory and the release tag is what publishes it. 3. Run **Prepare CLI release** → `major`. `cli/package.json` is at `2.24.0`, so this produces `3.0.0` and rolls the single `## Next` into one `## 3.0.0`. 4. Merging the release PR pushes `cli-v3.0.0`, which publishes npm, the GitHub Release, `cli.mops.one`, and the docs canister — the deploy that makes 3.x the site root and moves 2.x to `/2.x`. 5. Repoint or drop the npm `next` dist-tag, which otherwise keeps serving `3.0.0-beta.5`. 6. [#711](#711) currently shows the whole `v3` delta because its head branches off `v3`; that collapses back to its own three commits once this lands, with no rebase. CI here runs against the full 68-commit delta, so it is a real signal rather than a formality. 🤖 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.
Every other page in the docs assumes a person at a terminal. A CI pipeline, canister orchestrator or attestation system has different questions, and today has to reverse-engineer the answers from source: which commands write which files, what reaches the network and when, what the exit codes distinguish, and how to pull out the facts a build record needs.
Prompted by the Canic integration, whose questions were almost all answerable from a page that did not exist.
Stacked on #710 — this page documents the build-does-not-write-
mops.tomlbehaviour that PR introduces, so it is based on that branch. Merge #710 first; GitHub retargets this tov3automatically.The two warnings worth landing on their own
--lockeddoes not detect on-disk tampering. It checks the lockfile againstmops.tomland against the hashes the registry publishes — not the bytes in.mops/. 3.0.0 moved integrity verification to download time, so editing a file under.mops/afterwards does not fail a later build.mops verifyis the gate that catches it. That distinction only existed in the changelog.There is no offline mode.
--lockedreaches the registry on every run, because validating the lock means comparing against currently-published file hashes — a warm cache removes the downloads, not the query. Anyone designing a no-network pipeline needs to know that up front rather than discovering it when the lock check fails.Contents
install --locked→verify→check --locked→build --locked) and why order matters.mops/and theXDG_CACHE_HOME-relocatable global cachemoc's1(source rejected, user error) versus2(compiler crashed, toolchain bug) being passed through unchangedmops sources,mops moc-args,mops toolchain bin,mops --versionmops sourcesdeliberately has no--locked, and what a pipeline that only calls it gives up-- <moc flags>passthrough, the argument-assembly order that makes an injected--packagewin, and the caveat thatcheckandbuildresolve arguments independentlyWhat is unchanged
No code, and no behavior — this documents what v3 already does. The Docusaurus build passes, which is what validates the cross-links.
Facts that read as gaps rather than documentation (an
--offlineflag, a machine-readable build record) are deliberately absent rather than described as future work.🤖 Generated with Claude Code