diff --git a/.agents/skills/mops-cli/SKILL.md b/.agents/skills/mops-cli/SKILL.md index 230f4703..b90659ac 100644 --- a/.agents/skills/mops-cli/SKILL.md +++ b/.agents/skills/mops-cli/SKILL.md @@ -146,6 +146,8 @@ When `[canisters..migrations]` is configured, `mops check`, `mops build`, `chain` and `next` must live in the same parent directory. Migration files can import from sibling folders via relative paths (e.g. shared types in `src/backend/types/`); mops stages the active chain into `/.migrations-/`, preserving depth so relative imports resolve identically. The staged dir self-stamps a `.gitignore`; `mops init` also adds `.migrations-*/` to the project `.gitignore`. +`moc` diagnostics may print paths under `.migrations-/` — a staging dir that mops removes when the command finishes. The real file has the same name under `chain/` or `next/`. + Typical workflow: make a breaking change → `mops check` fails with a hint → `mops migrate new Name` → edit migration → `mops check` passes → `mops build` → deploy → `mops migrate freeze`. ### `mops remove ` diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index a0f0ce2b..19b8032c 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,6 +1,7 @@ # Mops CLI Changelog ## Next +- `mops check`/`build`/`check-stable` skip migration staging when only the pending `next` migration is needed, so `moc` diagnostics reference the real `next-migration/` path. ## 2.12.0 - Migration staging directory moved from `.mops/.migrations//` to `/.migrations-/`, so migration files can import shared modules from sibling folders (e.g. a `types/` folder next to `migrations/`) — relative imports now resolve to the same target whether moc reads the original chain dir or the staged one. The staged dir self-stamps a `.gitignore` so it doesn't pollute `git status`; `mops init` now also adds `.migrations-*/` to the project `.gitignore` diff --git a/cli/helpers/migrations.ts b/cli/helpers/migrations.ts index cb7af07c..a572a2f7 100644 --- a/cli/helpers/migrations.ts +++ b/cli/helpers/migrations.ts @@ -153,6 +153,16 @@ export async function prepareMigrationArgs( }; } + // Shortcut: when only the pending next migration is needed (empty chain or + // trimmed to 1), point moc at next-migration/ so diagnostics use the real path. + if (nextFile && nextDir && (chainFiles.length === 0 || limit === 1)) { + const migrationArgs = [`--enhanced-migration=${nextDir}`]; + if (isTrimming) { + migrationArgs.push("-A=M0254"); + } + return { migrationArgs, cleanup: async () => {} }; + } + const tempDir = stagedMigrationsDir(chainDir, canisterName); await rm(tempDir, { recursive: true, force: true }); mkdirSync(tempDir, { recursive: true }); diff --git a/cli/tests/__snapshots__/migrate.test.ts.snap b/cli/tests/__snapshots__/migrate.test.ts.snap index e9cddc77..4a0c614c 100644 --- a/cli/tests/__snapshots__/migrate.test.ts.snap +++ b/cli/tests/__snapshots__/migrate.test.ts.snap @@ -85,6 +85,42 @@ check-stable Comparing deployed.most ↔ .mops/.check-stable/new.most } `; +exports[`migrate check check-limit=1 with pending next reports real next-migration path on error 1`] = ` +{ + "exitCode": 1, + "stderr": "next-migration/20250401_000000_RenameId.mo:4.12-4.19: type error [M0050], literal of type + Text +does not have expected type + Nat +✗ Check failed for canister backend (exit code: 1)", + "stdout": "", +} +`; + +exports[`migrate check empty chain with pending next reports real next-migration path on error 1`] = ` +{ + "exitCode": 1, + "stderr": "next-migration/20250401_000000_RenameId.mo:4.12-4.19: type error [M0050], literal of type + Text +does not have expected type + Nat +✗ Check failed for canister backend (exit code: 1)", + "stdout": "", +} +`; + +exports[`migrate check error inside a chain migration reports its file location 1`] = ` +{ + "exitCode": 1, + "stderr": ".migrations-backend/20250301_000000_AddEmail.mo:4.24-4.26: type error [M0050], literal of type + Nat +does not have expected type + Text +✗ Check failed for canister backend (exit code: 1)", + "stdout": "", +} +`; + exports[`migrate migrate freeze moves the file from next to chain 1`] = ` { "exitCode": 0, diff --git a/cli/tests/migrate.test.ts b/cli/tests/migrate.test.ts index a53171d4..48e2a989 100644 --- a/cli/tests/migrate.test.ts +++ b/cli/tests/migrate.test.ts @@ -154,6 +154,51 @@ describe("migrate", () => { await patchMigrations(cwd, "check-limit = 2"); await cliSnapshot(["check", "--verbose"], { cwd }, 0); }); + + test("error inside a chain migration reports its file location", async () => { + const cwd = await makeTempFixture("with-next"); + await writeFile( + path.join(cwd, "migrations", "20250301_000000_AddEmail.mo"), + 'import State "../types/State";\n' + + "module {\n" + + " public func migration(old : State.V2) : State.V3 {\n" + + " { old with email = 42 };\n" + + " };\n" + + "};\n", + ); + await cliSnapshot(["check"], { cwd }, 1); + }); + + async function corruptNextMigration(cwd: string): Promise { + const nextDir = path.join(cwd, "next-migration"); + const nextFile = readdirSync(nextDir).find((f) => f.endsWith(".mo"))!; + await writeFile( + path.join(nextDir, nextFile), + 'import State "../types/State";\n' + + "module {\n" + + " public func migration(old : State.V3) : State.V4 {\n" + + ' { id = "wrong"; name = old.name; email = old.email };\n' + + " };\n" + + "};\n", + ); + } + + test("check-limit=1 with pending next reports real next-migration path on error", async () => { + const cwd = await makeTempFixture("with-next"); + await patchMigrations(cwd, "check-limit = 1"); + await corruptNextMigration(cwd); + await cliSnapshot(["check"], { cwd }, 1); + }); + + test("empty chain with pending next reports real next-migration path on error", async () => { + const cwd = await makeTempFixture("with-next"); + const chainDir = path.join(cwd, "migrations"); + for (const f of readdirSync(chainDir).filter((f) => f.endsWith(".mo"))) { + await rm(path.join(chainDir, f)); + } + await corruptNextMigration(cwd); + await cliSnapshot(["check"], { cwd }, 1); + }); }); describe("build", () => { diff --git a/docs/docs/09-mops.toml.md b/docs/docs/09-mops.toml.md index 951c17ff..530a6e66 100644 --- a/docs/docs/09-mops.toml.md +++ b/docs/docs/09-mops.toml.md @@ -167,6 +167,8 @@ When `[migrations]` is configured, do not add `--enhanced-migration` to `[canist :::note When a `next` migration exists or chain trimming is active, mops stages the active chain into `/.migrations-/` for compilation. This keeps the staged files at the same depth as the originals so relative imports (e.g. a shared `types/` folder next to `chain` and `next`) resolve identically. The staged dir self-stamps a `.gitignore`, and `mops init` adds `.migrations-*/` to the project `.gitignore`. + +`moc` diagnostics may point to a staged path under `.migrations-/`, which mops removes when the command finishes. ::: Shorthand — when only the entrypoint is needed: diff --git a/docs/docs/cli/4-dev/08-mops-migrate.md b/docs/docs/cli/4-dev/08-mops-migrate.md index 52266502..4a9e6b8c 100644 --- a/docs/docs/cli/4-dev/08-mops-migrate.md +++ b/docs/docs/cli/4-dev/08-mops-migrate.md @@ -61,6 +61,8 @@ build-limit = 100 `chain` and `next` must live in the same parent directory. Migration files can import from sibling folders (e.g. a shared `types/` folder) using relative paths — mops stages the active chain into `/.migrations-/` for compilation, preserving the depth of the originals so relative imports resolve identically. The staged dir self-stamps a `.gitignore`, and `mops init` adds `.migrations-*/` to the project `.gitignore`. +`moc` diagnostics may point to a staged path under `.migrations-/`, which mops removes when the command finishes. + See [`mops.toml` reference](/mops.toml#canistersnamemigrations) for all fields. ## Typical workflow