From 06526ba2789c50b381e599efb6d5e6613485d9b9 Mon Sep 17 00:00:00 2001 From: Kamil Listopad Date: Tue, 30 Jun 2026 16:16:33 +0200 Subject: [PATCH] cli: tunnel moc exit code without changing output text moc exits 2 on a compiler crash vs 1 for user errors. Route final moc failures through cliExit(code) instead of cliError (always 1). mops test tracks moc crash codes via pipeMMF's onClose callback and exits 2 when any moc process crashed during the run. Co-authored-by: Cursor --- cli/CHANGELOG.md | 2 ++ cli/commands/build.ts | 5 +++-- cli/commands/check-stable.ts | 8 +++++--- cli/commands/check.ts | 8 +++++--- cli/commands/generate.ts | 5 +++-- cli/commands/test/test.ts | 16 ++++++++++++---- cli/commands/test/utils.ts | 8 ++++++-- cli/error.ts | 5 +++++ 8 files changed, 41 insertions(+), 16 deletions(-) diff --git a/cli/CHANGELOG.md b/cli/CHANGELOG.md index 9542382e..dd15b971 100644 --- a/cli/CHANGELOG.md +++ b/cli/CHANGELOG.md @@ -2,6 +2,8 @@ ## Next +- `mops build`, `mops check`, `mops generate`, and `mops check-stable` now tunnel `moc`'s exit code instead of always exiting `1`. moc exits `2` on a compiler crash (uncaught internal error) versus `1` for normal user errors (type/compile errors, stable-compat mismatch, bad args), so CI and scripts can now distinguish "file a Motoko bug" from "fix your code" via `$?`. `mops test` likewise exits `2` when a `moc` process crashes during a test run, instead of reporting it as an ordinary test failure. + ## 2.16.0 - `mops bench` now compiles benchmark canisters under **enhanced orthogonal persistence** (moc's default) instead of forcing `--legacy-persistence` — measuring the persistence mode real canisters run. Pass `--legacy-persistence` to opt back into legacy persistence. diff --git a/cli/commands/build.ts b/cli/commands/build.ts index b8a966c0..5ee16d33 100644 --- a/cli/commands/build.ts +++ b/cli/commands/build.ts @@ -4,7 +4,7 @@ import { exists } from "fs-extra"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { lock, unlockSync } from "proper-lockfile"; -import { cliError } from "../error.js"; +import { cliError, cliExit } from "../error.js"; import { isCandidCompatible } from "../helpers/is-candid-compatible.js"; import { filterCanisters, @@ -136,7 +136,8 @@ export async function build( console.error(result.stdout); } } - cliError( + cliExit( + result.exitCode ?? 1, `Build failed for canister ${canisterName} (exit code: ${result.exitCode})`, ); } diff --git a/cli/commands/check-stable.ts b/cli/commands/check-stable.ts index e7e40b8d..e73f28c9 100644 --- a/cli/commands/check-stable.ts +++ b/cli/commands/check-stable.ts @@ -3,7 +3,7 @@ import { existsSync, mkdirSync, mkdtempSync } from "node:fs"; import { rm } from "node:fs/promises"; import chalk from "chalk"; import { execa } from "execa"; -import { cliError } from "../error.js"; +import { cliError, cliExit } from "../error.js"; import { getCheckLimitPendingIssue, prepareMigrationArgs, @@ -261,7 +261,8 @@ export async function runStableCheck( if (result.stderr) { console.error(result.stderr); } - cliError( + cliExit( + result.exitCode ?? 1, `✗ Stable compatibility check failed for canister '${canisterName}'`, ); } @@ -314,7 +315,8 @@ async function generateStableTypes( if (result.stderr) { console.error(result.stderr); } - cliError( + cliExit( + result.exitCode ?? 1, `Failed to generate stable types for ${moFile} (exit code: ${result.exitCode})`, ); } diff --git a/cli/commands/check.ts b/cli/commands/check.ts index a12fdc91..d2d85b24 100644 --- a/cli/commands/check.ts +++ b/cli/commands/check.ts @@ -1,7 +1,7 @@ import path from "node:path"; import chalk from "chalk"; import { execa } from "execa"; -import { cliError } from "../error.js"; +import { cliError, cliExit } from "../error.js"; import { getGlobalMocArgs, getRootDir, @@ -208,7 +208,8 @@ async function checkCanisters( }); if (result.exitCode !== 0) { - cliError( + cliExit( + result.exitCode ?? 1, `✗ Check failed for canister ${canisterName} (exit code: ${result.exitCode})`, ); } @@ -285,7 +286,8 @@ async function checkFiles( }); if (result.exitCode !== 0) { - cliError( + cliExit( + result.exitCode ?? 1, `✗ Check failed for file ${file} (exit code: ${result.exitCode})`, ); } diff --git a/cli/commands/generate.ts b/cli/commands/generate.ts index 952cac0c..fe44ac3f 100644 --- a/cli/commands/generate.ts +++ b/cli/commands/generate.ts @@ -2,7 +2,7 @@ import chalk from "chalk"; import { execa } from "execa"; import { mkdir } from "node:fs/promises"; import path from "node:path"; -import { cliError } from "../error.js"; +import { cliError, cliExit } from "../error.js"; import { filterCanisters, resolveCanisterConfigs, @@ -104,7 +104,8 @@ export async function generateCandid( console.error(result.stdout); } } - cliError( + cliExit( + result.exitCode ?? 1, `Failed to generate Candid for canister ${canisterName} (exit code: ${result.exitCode})`, ); } diff --git a/cli/commands/test/test.ts b/cli/commands/test/test.ts index 8d2a7963..b5b3e768 100644 --- a/cli/commands/test/test.ts +++ b/cli/commands/test/test.ts @@ -159,7 +159,7 @@ export async function test(filter = "", options: Partial = {}) { explicitReplica, ); if (!passed) { - process.exit(1); + process.exit(maxMocExit >= 2 ? 2 : 1); } } } @@ -167,6 +167,13 @@ export async function test(filter = "", options: Partial = {}) { let mocPath = ""; let wasmtimePath = ""; +let maxMocExit = 0; +const trackMocExit = (code: number | null) => { + if (code && code >= 2) { + maxMocExit = Math.max(maxMocExit, code); + } +}; + async function runAll( reporterName: ReporterName | undefined, filter = "", @@ -197,6 +204,7 @@ export async function testWithReporter( signal?: AbortSignal, explicitReplica = false, ): Promise { + maxMocExit = 0; let rootDir = getRootDir(); let files: string[] = []; let libFiles = globSync("**/test?(s)/lib.mo", MOTOKO_GLOB_CONFIG); @@ -328,7 +336,7 @@ export async function testWithReporter( } throw error; }); - pipeMMF(proc, mmf).then(resolve); + pipeMMF(proc, mmf, trackMocExit).then(resolve); } // build and run wasm else if (mode === "wasi") { @@ -346,7 +354,7 @@ export async function testWithReporter( } throw error; }); - pipeMMF(buildProc, mmf) + pipeMMF(buildProc, mmf, trackMocExit) .then(async () => { if (mmf.failed > 0) { return; @@ -413,7 +421,7 @@ export async function testWithReporter( throw error; }); - pipeMMF(buildProc, mmf) + pipeMMF(buildProc, mmf, trackMocExit) .then(async () => { if (mmf.failed > 0) { return; diff --git a/cli/commands/test/utils.ts b/cli/commands/test/utils.ts index d35a6875..bfe4adea 100644 --- a/cli/commands/test/utils.ts +++ b/cli/commands/test/utils.ts @@ -68,18 +68,22 @@ export function pipeStderrToMMF(stderr: Readable, mmf: MMF1, dir = "") { }); } -export function pipeMMF(proc: ChildProcessWithoutNullStreams, mmf: MMF1) { +export function pipeMMF( + proc: ChildProcessWithoutNullStreams, + mmf: MMF1, + onClose?: (code: number | null) => void, +) { return new Promise((resolve) => { pipeStdoutToMMF(proc.stdout, mmf); pipeStderrToMMF(proc.stderr, mmf); - // exit proc.on("close", (code) => { if (code === 0) { mmf.strategy !== "print" && mmf.pass(); } else if (code !== 1) { mmf.fail(`unknown exit code: ${code}`); } + onClose?.(code ?? null); resolve(); }); }); diff --git a/cli/error.ts b/cli/error.ts index 18f3467c..e106440d 100644 --- a/cli/error.ts +++ b/cli/error.ts @@ -4,3 +4,8 @@ export function cliError(...args: unknown[]): never { console.error(chalk.red(...args)); process.exit(1); } + +export function cliExit(code: number, ...args: unknown[]): never { + console.error(chalk.red(...args)); + process.exit(code || 1); +}