diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 55a072d9..6965aaa5 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -1,6 +1,7 @@ # Mops CLI Changelog ## Next +- Fix `mops install --lock update` silently no-op'ing on a corrupt lockfile (#515) - `mops publish` no longer rejects unknown `mops.toml` sections, `package.*` keys, or `requirements.*` entries — these typo guards were the only place in the CLI that complained about unknown keys, drifted from the docs/types, and blocked publish on harmless local-only config like `[moc]`, `[canisters]`, `[build]`, and `[lint]` (#512) ## 2.12.2 diff --git a/cli/integrity.ts b/cli/integrity.ts index d564e453..123a20e9 100644 --- a/cli/integrity.ts +++ b/cli/integrity.ts @@ -41,7 +41,7 @@ export async function checkIntegrity(lock?: "check" | "update" | "ignore") { } if (lock === "update") { - await updateLockFile(); + await updateLockFile({ force }); await checkLockFile(force); } else if (lock === "check") { await checkLockFile(force); @@ -159,9 +159,13 @@ export function checkLockFileLight(): boolean { return false; } -export async function updateLockFile() { +export async function updateLockFile({ + force = false, +}: { force?: boolean } = {}) { // if lock file exists and mops.toml hasn't changed, don't update it - if (checkLockFileLight()) { + // (unless forced: `--lock update` must unconditionally regenerate so users + // can recover from a corrupt lockfile without `rm mops.lock`) + if (!force && checkLockFileLight()) { return; } @@ -313,6 +317,11 @@ export async function checkLockFile(force = false) { console.error(`Mismatched hash for ${fileId}`); console.error(`Locked hash: ${lockedHash}`); console.error(`Actual hash: ${localHash}`); + console.error(""); + console.error( + "If you have not modified files under .mops/, your lockfile may be stale or corrupt.", + ); + console.error("Run `mops install --lock update` to regenerate it."); process.exit(1); } } diff --git a/cli/tests/cli.test.ts b/cli/tests/cli.test.ts index 132746ce..bb9c09b8 100644 --- a/cli/tests/cli.test.ts +++ b/cli/tests/cli.test.ts @@ -1,5 +1,5 @@ import { describe, expect, jest, test } from "@jest/globals"; -import { existsSync, rmSync } from "node:fs"; +import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import path from "path"; import { cli } from "./helpers"; @@ -92,4 +92,40 @@ describe("install", () => { rmSync(path.join(cwd, ".mops"), { recursive: true, force: true }); } }); + + // Regression: `install --lock update` used to early-return if mops.toml's + // deps hash was unchanged, even when the lockfile's per-file hashes were + // stale/corrupt. The subsequent checkLockFile would then fail and exit 1, + // so `--lock update` could never recover a broken lock — the only escape + // was `rm mops.lock`. See issue #514. + test("--lock update rewrites a lockfile with a corrupt file hash", async () => { + const cwd = path.join(import.meta.dirname, "install/success"); + const lockFile = path.join(cwd, "mops.lock"); + rmSync(lockFile, { force: true }); + try { + const first = await cli(["install"], { cwd, env: { CI: undefined } }); + expect(first.exitCode).toBe(0); + expect(existsSync(lockFile)).toBe(true); + + const bad = + "BAD0000000000000000000000000000000000000000000000000000000000BAD"; + const original = readFileSync(lockFile, "utf8"); + const corrupted = original.replace( + /"core@1\.0\.0\/mops\.toml":\s*"[0-9a-f]{64}"/, + `"core@1.0.0/mops.toml": "${bad}"`, + ); + expect(corrupted).not.toBe(original); + writeFileSync(lockFile, corrupted); + + const result = await cli(["install", "--lock", "update"], { + cwd, + env: { CI: undefined }, + }); + expect(result.exitCode).toBe(0); + expect(readFileSync(lockFile, "utf8")).not.toContain(bad); + } finally { + rmSync(lockFile, { force: true }); + rmSync(path.join(cwd, ".mops"), { recursive: true, force: true }); + } + }); }); diff --git a/docs/docs/cli/1-deps/02-mops-install.md b/docs/docs/cli/1-deps/02-mops-install.md index f23731fb..7892274b 100644 --- a/docs/docs/cli/1-deps/02-mops-install.md +++ b/docs/docs/cli/1-deps/02-mops-install.md @@ -26,7 +26,7 @@ See [mops.lock](/mops.lock) for details on lockfile contents and when to commit What to do with the [lockfile](/mops.lock). Possible values: -- `update` — keep the lockfile in sync with current dependencies and verify file integrity (default) +- `update` — keep the lockfile in sync with current dependencies and verify file integrity (default). Pass explicitly to force regeneration if the lockfile is stale or corrupt. - `check` — verify file integrity against an existing lockfile; fail if the lockfile is missing or out of date - `ignore` — skip the lockfile entirely