ic402 has two independent version numbers. Most releases move only the first.
| Version | What it is | Bumps | How |
|---|---|---|---|
Package version (e.g. 2.2.4) |
the mops/npm release number | every release | scripts/version.sh |
STABLE_SCHEMA_VERSION (e.g. 1) |
the library's stable-state contract (Ic402.STABLE_SCHEMA_VERSION in src/ic402/lib.mo) |
only when a Stable*State type changes incompatibly |
hand-edit lib.mo + check-stable-compat.sh --update |
- mops:
ic402(source of truth:mops.toml; shipssrc/ic402/**/*.mo). - npm:
@ic402/client(packages/client) and@ic402/mcp(integrations/mcp). - The root
ic402and@ic402/example-clientpackages areprivate— they carry the version for consistency but are not published.
scripts/version.sh syncs the version across 9 files (mops.toml, the four package.jsons,
example/zk-verifier/Cargo.toml + Cargo.lock, and the runtime version literals in
integrations/mcp/src/index.ts + example/client/src/index.ts). It does not touch
integrations/mcp/README.md or CHANGELOG.md — those are edited by hand each release.
This is almost every release. The stable-compat CI job passes on its own, so there's nothing
extra to do for the stable contract.
- Land your changes on
masterand confirm CI is green on the commit you're about to tag. - Bump the version:
./scripts/version.sh 2.2.5 # or: patch | minor | major (also: pnpm version:bump 2.2.5) - By hand (the two files version.sh doesn't touch):
integrations/mcp/README.md— update both2.2.xreferences (theversion:line + thenew McpServer({ … version: '2.2.x' })prose).CHANGELOG.md— prepend a## v2.2.5 — YYYY-MM-DDentry (most-recent-first), with a release-type summary line + categorized###subsections. State whether there are any wire/HTTP or@ic402/clientbreaking changes.
- Commit + tag (annotated tag,
ic402 vX.Y.Z (…)subject — matches the existing tags):git commit -am "release: v2.2.5" git tag -a v2.2.5 -m "ic402 v2.2.5 (short description)"
- Push (your action):
git push origin master && git push origin v2.2.5 - npm auth (one-time). The two npm packages publish with a per-repo token in a gitignored
.npmrc, so ic402 and sibling repos (e.g. engramx) each publish with their own token — no shared global~/.npmrctoken. pnpm won't expand env vars in a project.npmrc, so the token is a literal value (safe:.npmrcis gitignored, never committed). Create it once:(orpnpm config set "//registry.npmjs.org/:_authToken" npm_xxxxxxxx --location project
cp .npmrc.example .npmrcand paste your automation token).mops publishuses separate mops auth, not this token. - Publish (after CI on the pushed tag is green):
Publishing is optional for a release with no consumer-facing code change (e.g. a docs/CI-only patch) — the git tag still marks it.
pnpm build:client && pnpm build:demo # ensure dist/ is fresh mops publish # publishes the `ic402` mops package (cd packages/client && npm publish) # @ic402/client (first ever publish: --access public) (cd integrations/mcp && npm publish) # @ic402/mcp (first ever publish: --access public)
PATH gotcha:
mops publishandnpm publishrun directly in your shell (not through a project script that sanitizes PATH). A sibling repo'snode_modules/.binahead onPATHcan shadowmopswith a broken copy (Cannot find package '@dfinity/identity'). If you hit that, run the project's toolchain explicitly (e.g./opt/homebrew/bin/mops publish) or drop the offending entry fromPATH.
Failed to replace env in config: ${NPM_TOKEN}warning: this comes from the global~/.npmrc(_authToken=${NPM_TOKEN}) whenNPM_TOKENis unset. It's harmless (install/build from the public registry needs no auth). Now that each repo carries its own gitignored.npmrc, the global token line is redundant — remove it from~/.npmrc(orexport NPM_TOKEN=…) to silence the warning. A project.npmrcwith a literal token does not warn.
A "breaking" change to one of the four library Stable*State types is a removed or retyped field,
or a new field on an existing stable record (breaking because consumers hold these in mutable
stable vars, whose type is invariant across upgrade). Adding a new variant case or a brand-new
?optional stable variable is not breaking.
You'll know you have one because CI's stable-compat job fails with
BREAKING ic402 stable change with NO version bump. To release it safely:
- Bump
Ic402.STABLE_SCHEMA_VERSIONinsrc/ic402/lib.mo(1→2). - Provide a migration. Wire the
#migratebranch of thecheckSchemaVersionguard (see the pattern inexample/main.moanddocs/upgrade-safety.md), or document a state-dropping fresh deploy in the CHANGELOG. - Advance the baseline:
./scripts/check-stable-compat.sh --update # refuses unless step 1 happened; re-stamps the baseline - Commit the bumped
lib.mo+ the newtest/stable-anchor.most+ your migration, then do the standard release (A). Call out theSTABLE_SCHEMA_VERSIONchange and the migration prominently in the CHANGELOG.
Don't hand-edit
test/stable-anchor.most— it's generated, and advancing it without a version bump defeats the gate (it carries a "do not hand-edit" header). Always use--update.
For an additive stable change you skip section B entirely: the gate stays green and you don't
bump STABLE_SCHEMA_VERSION or advance the baseline. Leaving the baseline at an older signature is
fine — it keeps proving "anyone from that version can still upgrade."
scripts/check-stable-compat.sh (CI job stable-compat) compiles test/stable-anchor.mo — a
fixture that persists exactly the four library Stable*State types — to a .most stable signature
and compares it to the committed baseline with moc's own --stable-compatible oracle. It fails the
build on a stable change that is upgrade-incompatible and doesn't bump STABLE_SCHEMA_VERSION,
so a downstream consumer can never silently trap in loadStable on a live, fund-holding canister.
A --self-test run proves the gate still discriminates. Full design + the consumer-side pattern:
docs/upgrade-safety.md.
CI runs all of this on push; to check locally before tagging:
mops test # Motoko unit suites
pnpm test:client # @ic402/client
pnpm exec vitest run # MCP guards/security + integration (skips w/o a replica)
pnpm build:client && pnpm build:demo # type-check + build the published TS
bash scripts/build-example.sh /tmp/ex.wasm # B0: example stays installable (wasm locals budget)
./scripts/check-stable-compat.sh # stable contract gate
./scripts/check-stable-compat.sh --self-test # gate self-test
bash scripts/gen-did.sh && git diff --exit-code example/example.did # Candid in sync
pnpm format:check && pnpm lintSee CONTRIBUTING.md for the dev setup and docs/upgrade-safety.md
for how consumers handle an ic402 bump.