ic-mops 2.15.1 → 2.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/bundle/cli.tgz +0 -0
- package/cli.ts +25 -10
- package/commands/check-stable.ts +20 -3
- package/commands/check.ts +6 -1
- package/dist/cli.js +17 -9
- package/dist/commands/check-stable.d.ts +2 -1
- package/dist/commands/check-stable.js +8 -2
- package/dist/commands/check.js +6 -1
- package/dist/helpers/migrations.d.ts +16 -0
- package/dist/helpers/migrations.js +71 -1
- package/dist/helpers/parse-most.d.ts +5 -0
- package/dist/helpers/parse-most.js +28 -0
- package/dist/package.json +1 -1
- package/dist/tests/check-stable.test.js +15 -0
- package/dist/tests/check.test.js +5 -0
- package/dist/tests/lint.test.js +1 -1
- package/dist/tests/migrations-most.test.d.ts +1 -0
- package/dist/tests/migrations-most.test.js +47 -0
- package/helpers/migrations.ts +117 -0
- package/helpers/parse-most.ts +32 -0
- package/package.json +1 -1
- package/tests/__snapshots__/check-stable.test.ts.snap +11 -0
- package/tests/__snapshots__/lint.test.ts.snap +29 -1
- package/tests/check-stable/check-limit-warning/deployed.most +8 -0
- package/tests/check-stable/check-limit-warning/migrations/20250101_000000_Init.mo +5 -0
- package/tests/check-stable/check-limit-warning/migrations/20250201_000000_AddField.mo +9 -0
- package/tests/check-stable/check-limit-warning/migrations/20250301_000000_AddD.mo +10 -0
- package/tests/check-stable/check-limit-warning/mops.toml +15 -0
- package/tests/check-stable/check-limit-warning/src/main.mo +12 -0
- package/tests/check-stable.test.ts +24 -0
- package/tests/check.test.ts +9 -0
- package/tests/lint-extra-example-rules/lint/migration-self-contained/migration-self-contained.toml +7 -0
- package/tests/lint-extra-example-rules/migrations/20250101_000000_Init.mo +11 -0
- package/tests/lint-extra-example-rules/mops.toml +1 -0
- package/tests/lint.test.ts +1 -1
- package/tests/migrations-most.test.ts +64 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
## Next
|
|
4
4
|
|
|
5
|
+
## 2.15.2
|
|
6
|
+
|
|
7
|
+
- `mops check-stable` (and the stable check inside `mops check`) reports when `[canisters.<name>.migrations].check-limit` is set but more migrations are pending than the limit allows. If the compatibility check failed, the check-limit diagnostic replaces the misleading `moc` error; if it passed anyway, a warning is shown. Compares the deployed `.most` baseline against the local chain; use `--no-check-limit` to suppress.
|
|
8
|
+
|
|
5
9
|
## 2.15.1
|
|
6
10
|
|
|
7
11
|
- `mops check --fix` no longer aborts when a fixable file is read-only (e.g. a frozen migration chain file deliberately `chmod`'d to remove write access). The autofixer now skips such files with a warning and continues fixing the rest, instead of crashing the whole run on `EACCES`/`EPERM`.
|
package/bundle/cli.tgz
CHANGED
|
Binary file
|
package/cli.ts
CHANGED
|
@@ -102,14 +102,26 @@ function parseExtraArgs(variadicArgs?: string[]): {
|
|
|
102
102
|
// Shared `--help` section describing the enhanced migration `check-limit`
|
|
103
103
|
// trimming and its override flag, so the limit behaviour is discoverable
|
|
104
104
|
// from `--help`. `withFix` appends the `--fix` hint for commands that support it.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
// `withPendingWarning` adds the stable-check pending-migration warning (check / check-stable only).
|
|
106
|
+
function enhancedMigrationHelp(
|
|
107
|
+
options: {
|
|
108
|
+
withFix?: boolean;
|
|
109
|
+
withPendingWarning?: boolean;
|
|
110
|
+
} = {},
|
|
111
|
+
): string {
|
|
112
|
+
const example = options.withFix ? " (e.g. to --fix older migrations)" : "";
|
|
113
|
+
let text =
|
|
108
114
|
"\nEnhanced migration ([canisters.<name>.migrations]):\n" +
|
|
109
115
|
" The canister is checked against its migration chain. [migrations].check-limit\n" +
|
|
110
116
|
" trims it to the last N migrations (older ones are skipped). Pass --no-check-limit\n" +
|
|
111
|
-
` to use the full chain${example}
|
|
112
|
-
)
|
|
117
|
+
` to use the full chain${example}.`;
|
|
118
|
+
if (options.withPendingWarning) {
|
|
119
|
+
text +=
|
|
120
|
+
"\n When check-limit is set, the stable check reports if more migrations are pending\n" +
|
|
121
|
+
" (relative to the deployed .most baseline) than the limit allows — as an error if\n" +
|
|
122
|
+
" compat failed, otherwise a warning.";
|
|
123
|
+
}
|
|
124
|
+
return text;
|
|
113
125
|
}
|
|
114
126
|
|
|
115
127
|
program.name("mops");
|
|
@@ -372,14 +384,17 @@ program
|
|
|
372
384
|
.addOption(
|
|
373
385
|
new Option(
|
|
374
386
|
"--no-check-limit",
|
|
375
|
-
"
|
|
387
|
+
"Use the full migration chain, ignoring [migrations].check-limit; also suppresses the pending-migration warning",
|
|
376
388
|
),
|
|
377
389
|
)
|
|
378
390
|
.addHelpText(
|
|
379
391
|
"after",
|
|
380
392
|
"\nArguments after -- are forwarded directly to moc, e.g.:\n $ mops check -- -Werror",
|
|
381
393
|
)
|
|
382
|
-
.addHelpText(
|
|
394
|
+
.addHelpText(
|
|
395
|
+
"after",
|
|
396
|
+
enhancedMigrationHelp({ withFix: true, withPendingWarning: true }),
|
|
397
|
+
)
|
|
383
398
|
.allowUnknownOption(true)
|
|
384
399
|
.action(async (args, options) => {
|
|
385
400
|
checkConfigFile(true);
|
|
@@ -419,14 +434,14 @@ program
|
|
|
419
434
|
.addOption(
|
|
420
435
|
new Option(
|
|
421
436
|
"--no-check-limit",
|
|
422
|
-
"
|
|
437
|
+
"Use the full migration chain, ignoring [migrations].check-limit; also suppresses the pending-migration warning",
|
|
423
438
|
),
|
|
424
439
|
)
|
|
425
440
|
.addHelpText(
|
|
426
441
|
"after",
|
|
427
442
|
"\nArguments after -- are forwarded directly to moc, e.g.:\n $ mops check-stable -- -Werror",
|
|
428
443
|
)
|
|
429
|
-
.addHelpText("after", enhancedMigrationHelp(
|
|
444
|
+
.addHelpText("after", enhancedMigrationHelp({ withPendingWarning: true }))
|
|
430
445
|
.allowUnknownOption(true)
|
|
431
446
|
.action(async (args, options) => {
|
|
432
447
|
checkConfigFile(true);
|
|
@@ -979,7 +994,7 @@ program
|
|
|
979
994
|
"after",
|
|
980
995
|
"\nArguments after -- are forwarded directly to lintoko, e.g.:\n $ mops lint -- --severity warning",
|
|
981
996
|
)
|
|
982
|
-
.addHelpText("after", enhancedMigrationHelp(true))
|
|
997
|
+
.addHelpText("after", enhancedMigrationHelp({ withFix: true }))
|
|
983
998
|
.allowUnknownOption(true)
|
|
984
999
|
.action(async (filter, options) => {
|
|
985
1000
|
checkConfigFile(true);
|
package/commands/check-stable.ts
CHANGED
|
@@ -4,9 +4,13 @@ import { rm } from "node:fs/promises";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { execa } from "execa";
|
|
6
6
|
import { cliError } from "../error.js";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
getCheckLimitPendingIssue,
|
|
9
|
+
prepareMigrationArgs,
|
|
10
|
+
reportCheckLimitPendingIssue,
|
|
11
|
+
} from "../helpers/migrations.js";
|
|
8
12
|
import { getGlobalMocArgs, readConfig, resolveConfigPath } from "../mops.js";
|
|
9
|
-
import { CanisterConfig } from "../types.js";
|
|
13
|
+
import { CanisterConfig, MigrationsConfig } from "../types.js";
|
|
10
14
|
import {
|
|
11
15
|
filterCanisters,
|
|
12
16
|
looksLikeFile,
|
|
@@ -103,6 +107,7 @@ export async function checkStable(
|
|
|
103
107
|
mocPath,
|
|
104
108
|
globalMocArgs,
|
|
105
109
|
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
|
|
110
|
+
migrations: canister.migrations,
|
|
106
111
|
options,
|
|
107
112
|
});
|
|
108
113
|
} finally {
|
|
@@ -146,6 +151,7 @@ export async function checkStable(
|
|
|
146
151
|
globalMocArgs,
|
|
147
152
|
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
|
|
148
153
|
sources,
|
|
154
|
+
migrations: canister.migrations,
|
|
149
155
|
options,
|
|
150
156
|
});
|
|
151
157
|
} finally {
|
|
@@ -173,6 +179,7 @@ export interface RunStableCheckParams {
|
|
|
173
179
|
globalMocArgs: string[];
|
|
174
180
|
canisterArgs: string[];
|
|
175
181
|
sources?: string[];
|
|
182
|
+
migrations?: MigrationsConfig;
|
|
176
183
|
options?: Partial<CheckStableOptions>;
|
|
177
184
|
}
|
|
178
185
|
|
|
@@ -240,7 +247,17 @@ export async function runStableCheck(
|
|
|
240
247
|
reject: false,
|
|
241
248
|
});
|
|
242
249
|
|
|
243
|
-
|
|
250
|
+
const issue = getCheckLimitPendingIssue(
|
|
251
|
+
params.migrations,
|
|
252
|
+
canisterName,
|
|
253
|
+
oldMostPath,
|
|
254
|
+
options.checkLimit === false,
|
|
255
|
+
isOldMostFile,
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
if (issue) {
|
|
259
|
+
reportCheckLimitPendingIssue(issue, result.exitCode !== 0);
|
|
260
|
+
} else if (result.exitCode !== 0) {
|
|
244
261
|
if (result.stderr) {
|
|
245
262
|
console.error(result.stderr);
|
|
246
263
|
}
|
package/commands/check.ts
CHANGED
|
@@ -230,7 +230,12 @@ async function checkCanisters(
|
|
|
230
230
|
globalMocArgs,
|
|
231
231
|
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
|
|
232
232
|
sources,
|
|
233
|
-
|
|
233
|
+
migrations: canister.migrations,
|
|
234
|
+
options: {
|
|
235
|
+
verbose: options.verbose,
|
|
236
|
+
extraArgs: options.extraArgs,
|
|
237
|
+
checkLimit: options.checkLimit,
|
|
238
|
+
},
|
|
234
239
|
});
|
|
235
240
|
}
|
|
236
241
|
} finally {
|
package/dist/cli.js
CHANGED
|
@@ -65,12 +65,20 @@ function parseExtraArgs(variadicArgs) {
|
|
|
65
65
|
// Shared `--help` section describing the enhanced migration `check-limit`
|
|
66
66
|
// trimming and its override flag, so the limit behaviour is discoverable
|
|
67
67
|
// from `--help`. `withFix` appends the `--fix` hint for commands that support it.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
// `withPendingWarning` adds the stable-check pending-migration warning (check / check-stable only).
|
|
69
|
+
function enhancedMigrationHelp(options = {}) {
|
|
70
|
+
const example = options.withFix ? " (e.g. to --fix older migrations)" : "";
|
|
71
|
+
let text = "\nEnhanced migration ([canisters.<name>.migrations]):\n" +
|
|
71
72
|
" The canister is checked against its migration chain. [migrations].check-limit\n" +
|
|
72
73
|
" trims it to the last N migrations (older ones are skipped). Pass --no-check-limit\n" +
|
|
73
|
-
` to use the full chain${example}
|
|
74
|
+
` to use the full chain${example}.`;
|
|
75
|
+
if (options.withPendingWarning) {
|
|
76
|
+
text +=
|
|
77
|
+
"\n When check-limit is set, the stable check reports if more migrations are pending\n" +
|
|
78
|
+
" (relative to the deployed .most baseline) than the limit allows — as an error if\n" +
|
|
79
|
+
" compat failed, otherwise a warning.";
|
|
80
|
+
}
|
|
81
|
+
return text;
|
|
74
82
|
}
|
|
75
83
|
program.name("mops");
|
|
76
84
|
// --version
|
|
@@ -286,9 +294,9 @@ program
|
|
|
286
294
|
.description("Check Motoko canisters or files for syntax errors and type issues. Arguments can be canister names or file paths. If no arguments are given, checks all canisters from mops.toml. Also runs stable compatibility checks for canisters with [check-stable] configured, and runs linting if lintoko is configured in [toolchain]")
|
|
287
295
|
.option("--verbose", "Verbose console output")
|
|
288
296
|
.addOption(new Option("--fix", "Apply autofixes to all files, including transitively imported ones"))
|
|
289
|
-
.addOption(new Option("--no-check-limit", "
|
|
297
|
+
.addOption(new Option("--no-check-limit", "Use the full migration chain, ignoring [migrations].check-limit; also suppresses the pending-migration warning"))
|
|
290
298
|
.addHelpText("after", "\nArguments after -- are forwarded directly to moc, e.g.:\n $ mops check -- -Werror")
|
|
291
|
-
.addHelpText("after", enhancedMigrationHelp(true))
|
|
299
|
+
.addHelpText("after", enhancedMigrationHelp({ withFix: true, withPendingWarning: true }))
|
|
292
300
|
.allowUnknownOption(true)
|
|
293
301
|
.action(async (args, options) => {
|
|
294
302
|
checkConfigFile(true);
|
|
@@ -321,9 +329,9 @@ program
|
|
|
321
329
|
.command("check-stable [args...]")
|
|
322
330
|
.description("Check stable variable compatibility. With no arguments, checks all canisters with [check-stable] configured. Arguments can be canister names or an old file path followed by an optional canister name")
|
|
323
331
|
.option("--verbose", "Verbose console output")
|
|
324
|
-
.addOption(new Option("--no-check-limit", "
|
|
332
|
+
.addOption(new Option("--no-check-limit", "Use the full migration chain, ignoring [migrations].check-limit; also suppresses the pending-migration warning"))
|
|
325
333
|
.addHelpText("after", "\nArguments after -- are forwarded directly to moc, e.g.:\n $ mops check-stable -- -Werror")
|
|
326
|
-
.addHelpText("after", enhancedMigrationHelp(
|
|
334
|
+
.addHelpText("after", enhancedMigrationHelp({ withPendingWarning: true }))
|
|
327
335
|
.allowUnknownOption(true)
|
|
328
336
|
.action(async (args, options) => {
|
|
329
337
|
checkConfigFile(true);
|
|
@@ -693,7 +701,7 @@ program
|
|
|
693
701
|
.addOption(new Option("-r, --rules <directory...>", "Directories containing rules (can be used multiple times)"))
|
|
694
702
|
.addOption(new Option("--no-check-limit", "Lint the full migration chain, ignoring [migrations].check-limit"))
|
|
695
703
|
.addHelpText("after", "\nArguments after -- are forwarded directly to lintoko, e.g.:\n $ mops lint -- --severity warning")
|
|
696
|
-
.addHelpText("after", enhancedMigrationHelp(true))
|
|
704
|
+
.addHelpText("after", enhancedMigrationHelp({ withFix: true }))
|
|
697
705
|
.allowUnknownOption(true)
|
|
698
706
|
.action(async (filter, options) => {
|
|
699
707
|
checkConfigFile(true);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CanisterConfig } from "../types.js";
|
|
1
|
+
import { CanisterConfig, MigrationsConfig } from "../types.js";
|
|
2
2
|
export interface CheckStableOptions {
|
|
3
3
|
verbose: boolean;
|
|
4
4
|
extraArgs: string[];
|
|
@@ -17,6 +17,7 @@ export interface RunStableCheckParams {
|
|
|
17
17
|
globalMocArgs: string[];
|
|
18
18
|
canisterArgs: string[];
|
|
19
19
|
sources?: string[];
|
|
20
|
+
migrations?: MigrationsConfig;
|
|
20
21
|
options?: Partial<CheckStableOptions>;
|
|
21
22
|
}
|
|
22
23
|
export declare function runStableCheck(params: RunStableCheckParams): Promise<void>;
|
|
@@ -4,7 +4,7 @@ import { rm } from "node:fs/promises";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { execa } from "execa";
|
|
6
6
|
import { cliError } from "../error.js";
|
|
7
|
-
import { prepareMigrationArgs } from "../helpers/migrations.js";
|
|
7
|
+
import { getCheckLimitPendingIssue, prepareMigrationArgs, reportCheckLimitPendingIssue, } from "../helpers/migrations.js";
|
|
8
8
|
import { getGlobalMocArgs, readConfig, resolveConfigPath } from "../mops.js";
|
|
9
9
|
import { filterCanisters, looksLikeFile, resolveCanisterConfigs, resolveSingleCanister, validateCanisterArgs, } from "../helpers/resolve-canisters.js";
|
|
10
10
|
import { sourcesArgs } from "./sources.js";
|
|
@@ -61,6 +61,7 @@ export async function checkStable(args, options = {}) {
|
|
|
61
61
|
mocPath,
|
|
62
62
|
globalMocArgs,
|
|
63
63
|
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
|
|
64
|
+
migrations: canister.migrations,
|
|
64
65
|
options,
|
|
65
66
|
});
|
|
66
67
|
}
|
|
@@ -95,6 +96,7 @@ export async function checkStable(args, options = {}) {
|
|
|
95
96
|
globalMocArgs,
|
|
96
97
|
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
|
|
97
98
|
sources,
|
|
99
|
+
migrations: canister.migrations,
|
|
98
100
|
options,
|
|
99
101
|
});
|
|
100
102
|
}
|
|
@@ -136,7 +138,11 @@ export async function runStableCheck(params) {
|
|
|
136
138
|
stdio: "pipe",
|
|
137
139
|
reject: false,
|
|
138
140
|
});
|
|
139
|
-
|
|
141
|
+
const issue = getCheckLimitPendingIssue(params.migrations, canisterName, oldMostPath, options.checkLimit === false, isOldMostFile);
|
|
142
|
+
if (issue) {
|
|
143
|
+
reportCheckLimitPendingIssue(issue, result.exitCode !== 0);
|
|
144
|
+
}
|
|
145
|
+
else if (result.exitCode !== 0) {
|
|
140
146
|
if (result.stderr) {
|
|
141
147
|
console.error(result.stderr);
|
|
142
148
|
}
|
package/dist/commands/check.js
CHANGED
|
@@ -144,7 +144,12 @@ async function checkCanisters(config, canisters, options) {
|
|
|
144
144
|
globalMocArgs,
|
|
145
145
|
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
|
|
146
146
|
sources,
|
|
147
|
-
|
|
147
|
+
migrations: canister.migrations,
|
|
148
|
+
options: {
|
|
149
|
+
verbose: options.verbose,
|
|
150
|
+
extraArgs: options.extraArgs,
|
|
151
|
+
checkLimit: options.checkLimit,
|
|
152
|
+
},
|
|
148
153
|
});
|
|
149
154
|
}
|
|
150
155
|
}
|
|
@@ -8,6 +8,22 @@ export declare function getNextMigrationFile(nextDir: string): string | null;
|
|
|
8
8
|
export declare function validateNextMigrationOrder(chainDirOrFiles: string | string[], nextFile: string): void;
|
|
9
9
|
export declare function validateMigrationsConfig(migrations: MigrationsConfig, canisterName: string): void;
|
|
10
10
|
export declare function prepareMigrationArgs(migrations: MigrationsConfig | undefined, canisterName: string, mode: "check" | "build", verbose?: boolean, ignoreLimit?: boolean): Promise<MigrationArgsResult>;
|
|
11
|
+
/**
|
|
12
|
+
* Local chain (+ next) files not yet recorded in the deployed `.most` baseline.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getPendingMigrationFiles(migrations: MigrationsConfig, canisterName: string, appliedNames: string[]): string[];
|
|
15
|
+
/**
|
|
16
|
+
* Pending migrations exceed `check-limit` relative to the deployed `.most` baseline.
|
|
17
|
+
*/
|
|
18
|
+
export interface CheckLimitPendingIssue {
|
|
19
|
+
canisterName: string;
|
|
20
|
+
pending: string[];
|
|
21
|
+
checkLimit: number;
|
|
22
|
+
latestApplied: string | null;
|
|
23
|
+
}
|
|
24
|
+
export declare function getCheckLimitPendingIssue(migrations: MigrationsConfig | undefined, canisterName: string, oldMostPath: string, ignoreCheckLimit: boolean, baselineIsMostFile: boolean): CheckLimitPendingIssue | null;
|
|
25
|
+
/** Warn on accidental compat pass; fail with this diagnostic when compat already failed. */
|
|
26
|
+
export declare function reportCheckLimitPendingIssue(issue: CheckLimitPendingIssue, compatFailed: boolean): void;
|
|
11
27
|
/**
|
|
12
28
|
* Absolute paths of chain migration files that `mops lint` should skip,
|
|
13
29
|
* mirroring the `check-limit` trimming applied to `moc` during `mops check`.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, mkdtempSync, readdirSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { rm } from "node:fs/promises";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { cliError } from "../error.js";
|
|
6
6
|
import { getRootDir, resolveConfigPath } from "../mops.js";
|
|
7
7
|
import { resolveCanisterConfigs } from "./resolve-canisters.js";
|
|
8
|
+
import { latestAppliedMigrationName, migrationBasename, parseMostAppliedMigrationNames, } from "./parse-most.js";
|
|
8
9
|
function stagedMigrationsDir(chainDir, canisterName) {
|
|
9
10
|
return join(dirname(chainDir), `.migrations-${canisterName}`);
|
|
10
11
|
}
|
|
@@ -151,6 +152,75 @@ export async function prepareMigrationArgs(migrations, canisterName, mode, verbo
|
|
|
151
152
|
},
|
|
152
153
|
};
|
|
153
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Local chain (+ next) files not yet recorded in the deployed `.most` baseline.
|
|
157
|
+
*/
|
|
158
|
+
export function getPendingMigrationFiles(migrations, canisterName, appliedNames) {
|
|
159
|
+
validateMigrationsConfig(migrations, canisterName);
|
|
160
|
+
const chainDir = resolveConfigPath(migrations.chain);
|
|
161
|
+
const nextDir = migrations.next
|
|
162
|
+
? resolveConfigPath(migrations.next)
|
|
163
|
+
: undefined;
|
|
164
|
+
const nextFile = nextDir ? getNextMigrationFile(nextDir) : null;
|
|
165
|
+
const all = getMigrationFiles(chainDir);
|
|
166
|
+
if (nextFile) {
|
|
167
|
+
all.push(nextFile);
|
|
168
|
+
}
|
|
169
|
+
const highWaterMark = appliedNames.length > 0 ? latestAppliedMigrationName(appliedNames) : "";
|
|
170
|
+
return all.filter((file) => migrationBasename(file) > highWaterMark);
|
|
171
|
+
}
|
|
172
|
+
export function getCheckLimitPendingIssue(migrations, canisterName, oldMostPath, ignoreCheckLimit, baselineIsMostFile) {
|
|
173
|
+
if (!migrations || ignoreCheckLimit || !baselineIsMostFile) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
const checkLimit = migrations["check-limit"];
|
|
177
|
+
if (checkLimit === undefined) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
let appliedNames;
|
|
181
|
+
try {
|
|
182
|
+
appliedNames = parseMostAppliedMigrationNames(readFileSync(oldMostPath, "utf8"));
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
if (appliedNames === null) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
const pending = getPendingMigrationFiles(migrations, canisterName, appliedNames);
|
|
191
|
+
if (pending.length <= checkLimit) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
canisterName,
|
|
196
|
+
pending,
|
|
197
|
+
checkLimit,
|
|
198
|
+
latestApplied: appliedNames.length > 0 ? latestAppliedMigrationName(appliedNames) : null,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function formatCheckLimitPendingLines(issue) {
|
|
202
|
+
const { canisterName, pending, checkLimit } = issue;
|
|
203
|
+
return [
|
|
204
|
+
`Canister '${canisterName}' has ${pending.length} pending migration(s) but check-limit=${checkLimit}. ` +
|
|
205
|
+
`Fold all changes into the latest pending migration: ${pending[pending.length - 1]}`,
|
|
206
|
+
` Pending: ${pending.join(", ")}`,
|
|
207
|
+
...(issue.latestApplied
|
|
208
|
+
? [` Latest already applied migration: ${issue.latestApplied}`]
|
|
209
|
+
: []),
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
/** Warn on accidental compat pass; fail with this diagnostic when compat already failed. */
|
|
213
|
+
export function reportCheckLimitPendingIssue(issue, compatFailed) {
|
|
214
|
+
const lines = formatCheckLimitPendingLines(issue);
|
|
215
|
+
if (compatFailed) {
|
|
216
|
+
cliError(`✗ Stable compatibility check failed for canister '${issue.canisterName}': too many pending migrations for check-limit=${issue.checkLimit}\n` +
|
|
217
|
+
lines.join("\n"));
|
|
218
|
+
}
|
|
219
|
+
console.warn(chalk.yellow(`WARN: ${lines[0]}`));
|
|
220
|
+
for (const line of lines.slice(1)) {
|
|
221
|
+
console.warn(chalk.yellow(line));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
154
224
|
/**
|
|
155
225
|
* Absolute paths of chain migration files that `mops lint` should skip,
|
|
156
226
|
* mirroring the `check-limit` trimming applied to `moc` during `mops check`.
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Enhanced-migration names recorded in a Version 4.0.0 `.most` chain block. */
|
|
2
|
+
export declare function parseMostAppliedMigrationNames(content: string): string[] | null;
|
|
3
|
+
export declare function migrationBasename(file: string): string;
|
|
4
|
+
/** Lexicographic max of applied migration names (matches chain file sort order). */
|
|
5
|
+
export declare function latestAppliedMigrationName(appliedNames: string[]): string;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Enhanced-migration names recorded in a Version 4.0.0 `.most` chain block. */
|
|
2
|
+
export function parseMostAppliedMigrationNames(content) {
|
|
3
|
+
const version = content.match(/^\/\/ Version: ([^\n]+)/m)?.[1]?.trim();
|
|
4
|
+
if (!version) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
if (version !== "4.0.0") {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
// Safe: actor types in type aliases are always indented (col ≥ 2); the main actor is at col 0.
|
|
11
|
+
const actorIdx = content.search(/\nactor\b/);
|
|
12
|
+
if (actorIdx < 0) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const chainBlock = content.slice(0, actorIdx);
|
|
16
|
+
const names = [];
|
|
17
|
+
for (const match of chainBlock.matchAll(/[{;]\s*"([^"]+)"\s*:/gs)) {
|
|
18
|
+
names.push(match[1]);
|
|
19
|
+
}
|
|
20
|
+
return names;
|
|
21
|
+
}
|
|
22
|
+
export function migrationBasename(file) {
|
|
23
|
+
return file.endsWith(".mo") ? file.slice(0, -3) : file;
|
|
24
|
+
}
|
|
25
|
+
/** Lexicographic max of applied migration names (matches chain file sort order). */
|
|
26
|
+
export function latestAppliedMigrationName(appliedNames) {
|
|
27
|
+
return appliedNames.reduce((max, name) => (name > max ? name : max), "");
|
|
28
|
+
}
|
package/dist/package.json
CHANGED
|
@@ -94,4 +94,19 @@ describe("check-stable", () => {
|
|
|
94
94
|
expect(result.stdout).toMatch(/Stable compatibility check passed/);
|
|
95
95
|
}
|
|
96
96
|
}, 60_000);
|
|
97
|
+
test("fails with check-limit diagnostic when pending migrations exceed check-limit", async () => {
|
|
98
|
+
const cwd = path.join(import.meta.dirname, "check-stable/check-limit-warning");
|
|
99
|
+
await cliSnapshot(["check-stable"], { cwd }, 1);
|
|
100
|
+
});
|
|
101
|
+
test("does not warn when deployed baseline matches the chain", async () => {
|
|
102
|
+
const cwd = path.join(import.meta.dirname, "check-stable/migrations-chain");
|
|
103
|
+
const result = await cli(["check-stable"], { cwd });
|
|
104
|
+
expect(result.exitCode).toBe(0);
|
|
105
|
+
expect(result.stderr).not.toMatch(/pending migration\(s\) but check-limit/);
|
|
106
|
+
});
|
|
107
|
+
test("--no-check-limit suppresses pending migration warning", async () => {
|
|
108
|
+
const cwd = path.join(import.meta.dirname, "check-stable/check-limit-warning");
|
|
109
|
+
const result = await cli(["check-stable", "--no-check-limit"], { cwd });
|
|
110
|
+
expect(result.stderr).not.toMatch(/pending migration\(s\) but check-limit/);
|
|
111
|
+
});
|
|
97
112
|
});
|
package/dist/tests/check.test.js
CHANGED
|
@@ -158,4 +158,9 @@ describe("check", () => {
|
|
|
158
158
|
expect(result.stdout).not.toMatch(/\.migrations-/);
|
|
159
159
|
expect(result.stdout).not.toMatch(/-A=M0254/);
|
|
160
160
|
});
|
|
161
|
+
test("--no-check-limit suppresses pending migration warning in check", async () => {
|
|
162
|
+
const cwd = path.join(import.meta.dirname, "check-stable/check-limit-warning");
|
|
163
|
+
const result = await cli(["check", "--no-check-limit"], { cwd });
|
|
164
|
+
expect(result.stderr).not.toMatch(/pending migration\(s\) but check-limit/);
|
|
165
|
+
});
|
|
161
166
|
});
|
package/dist/tests/lint.test.js
CHANGED
|
@@ -68,7 +68,7 @@ describe("lint", () => {
|
|
|
68
68
|
const cwd = path.join(import.meta.dirname, "lint-extra-with-cli-rules");
|
|
69
69
|
await cliSnapshot(["lint", "--rules", "empty-rules", "--verbose"], { cwd }, 1);
|
|
70
70
|
});
|
|
71
|
-
test("example rules: no-types, types-only, migration-only", async () => {
|
|
71
|
+
test("example rules: no-types, types-only, migration-only, migration-self-contained", async () => {
|
|
72
72
|
const cwd = path.join(import.meta.dirname, "lint-extra-example-rules");
|
|
73
73
|
await cliSnapshot(["lint"], { cwd }, 1);
|
|
74
74
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import { latestAppliedMigrationName, parseMostAppliedMigrationNames, } from "../helpers/parse-most";
|
|
3
|
+
describe("parseMostAppliedMigrationNames", () => {
|
|
4
|
+
test("parses Version 4.0.0 enhanced-migration chain", () => {
|
|
5
|
+
const content = `// Version: 4.0.0
|
|
6
|
+
{
|
|
7
|
+
"20250101_000000_Init" : {} -> {a : Nat; b : Text};
|
|
8
|
+
"20250201_000000_AddField" : (old : {a : Nat; b : Text}) -> {a : Nat; b : Text; c : Bool};
|
|
9
|
+
}
|
|
10
|
+
actor {
|
|
11
|
+
stable a : Nat;
|
|
12
|
+
stable b : Text;
|
|
13
|
+
stable c : Bool
|
|
14
|
+
};
|
|
15
|
+
`;
|
|
16
|
+
expect(parseMostAppliedMigrationNames(content)).toEqual([
|
|
17
|
+
"20250101_000000_Init",
|
|
18
|
+
"20250201_000000_AddField",
|
|
19
|
+
]);
|
|
20
|
+
});
|
|
21
|
+
test("returns empty array for Version 1.0.0 (no EM chain)", () => {
|
|
22
|
+
expect(parseMostAppliedMigrationNames("// Version: 1.0.0\nactor { };\n")).toEqual([]);
|
|
23
|
+
});
|
|
24
|
+
test("returns empty array for Version 3.0.0 (legacy migration)", () => {
|
|
25
|
+
const content = `// Version: 3.0.0
|
|
26
|
+
actor ({
|
|
27
|
+
}, {
|
|
28
|
+
stable var field1 : Nat;
|
|
29
|
+
stable var field2 : Text
|
|
30
|
+
});
|
|
31
|
+
`;
|
|
32
|
+
expect(parseMostAppliedMigrationNames(content)).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
test("returns null when version header is missing", () => {
|
|
35
|
+
expect(parseMostAppliedMigrationNames("actor { };\n")).toBeNull();
|
|
36
|
+
});
|
|
37
|
+
test("returns null when Version 4.0.0 has no actor block", () => {
|
|
38
|
+
expect(parseMostAppliedMigrationNames('// Version: 4.0.0\n{ "Init" : {} -> {} }')).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
test("latestAppliedMigrationName picks lexicographic max", () => {
|
|
41
|
+
expect(latestAppliedMigrationName([
|
|
42
|
+
"20250101_000000_Init",
|
|
43
|
+
"20250301_000000_AddD",
|
|
44
|
+
"20250201_000000_AddField",
|
|
45
|
+
])).toBe("20250301_000000_AddD");
|
|
46
|
+
});
|
|
47
|
+
});
|
package/helpers/migrations.ts
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
existsSync,
|
|
3
3
|
mkdirSync,
|
|
4
4
|
mkdtempSync,
|
|
5
|
+
readFileSync,
|
|
5
6
|
readdirSync,
|
|
6
7
|
symlinkSync,
|
|
7
8
|
writeFileSync,
|
|
@@ -13,6 +14,11 @@ import { cliError } from "../error.js";
|
|
|
13
14
|
import { getRootDir, resolveConfigPath } from "../mops.js";
|
|
14
15
|
import { resolveCanisterConfigs } from "./resolve-canisters.js";
|
|
15
16
|
import { Config, MigrationsConfig } from "../types.js";
|
|
17
|
+
import {
|
|
18
|
+
latestAppliedMigrationName,
|
|
19
|
+
migrationBasename,
|
|
20
|
+
parseMostAppliedMigrationNames,
|
|
21
|
+
} from "./parse-most.js";
|
|
16
22
|
|
|
17
23
|
function stagedMigrationsDir(chainDir: string, canisterName: string): string {
|
|
18
24
|
return join(dirname(chainDir), `.migrations-${canisterName}`);
|
|
@@ -234,6 +240,117 @@ export async function prepareMigrationArgs(
|
|
|
234
240
|
};
|
|
235
241
|
}
|
|
236
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Local chain (+ next) files not yet recorded in the deployed `.most` baseline.
|
|
245
|
+
*/
|
|
246
|
+
export function getPendingMigrationFiles(
|
|
247
|
+
migrations: MigrationsConfig,
|
|
248
|
+
canisterName: string,
|
|
249
|
+
appliedNames: string[],
|
|
250
|
+
): string[] {
|
|
251
|
+
validateMigrationsConfig(migrations, canisterName);
|
|
252
|
+
|
|
253
|
+
const chainDir = resolveConfigPath(migrations.chain);
|
|
254
|
+
const nextDir = migrations.next
|
|
255
|
+
? resolveConfigPath(migrations.next)
|
|
256
|
+
: undefined;
|
|
257
|
+
const nextFile = nextDir ? getNextMigrationFile(nextDir) : null;
|
|
258
|
+
|
|
259
|
+
const all = getMigrationFiles(chainDir);
|
|
260
|
+
if (nextFile) {
|
|
261
|
+
all.push(nextFile);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const highWaterMark =
|
|
265
|
+
appliedNames.length > 0 ? latestAppliedMigrationName(appliedNames) : "";
|
|
266
|
+
return all.filter((file) => migrationBasename(file) > highWaterMark);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Pending migrations exceed `check-limit` relative to the deployed `.most` baseline.
|
|
271
|
+
*/
|
|
272
|
+
export interface CheckLimitPendingIssue {
|
|
273
|
+
canisterName: string;
|
|
274
|
+
pending: string[];
|
|
275
|
+
checkLimit: number;
|
|
276
|
+
latestApplied: string | null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function getCheckLimitPendingIssue(
|
|
280
|
+
migrations: MigrationsConfig | undefined,
|
|
281
|
+
canisterName: string,
|
|
282
|
+
oldMostPath: string,
|
|
283
|
+
ignoreCheckLimit: boolean,
|
|
284
|
+
baselineIsMostFile: boolean,
|
|
285
|
+
): CheckLimitPendingIssue | null {
|
|
286
|
+
if (!migrations || ignoreCheckLimit || !baselineIsMostFile) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
const checkLimit = migrations["check-limit"];
|
|
290
|
+
if (checkLimit === undefined) {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let appliedNames: string[] | null;
|
|
295
|
+
try {
|
|
296
|
+
appliedNames = parseMostAppliedMigrationNames(
|
|
297
|
+
readFileSync(oldMostPath, "utf8"),
|
|
298
|
+
);
|
|
299
|
+
} catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
if (appliedNames === null) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const pending = getPendingMigrationFiles(
|
|
307
|
+
migrations,
|
|
308
|
+
canisterName,
|
|
309
|
+
appliedNames,
|
|
310
|
+
);
|
|
311
|
+
if (pending.length <= checkLimit) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
canisterName,
|
|
317
|
+
pending,
|
|
318
|
+
checkLimit,
|
|
319
|
+
latestApplied:
|
|
320
|
+
appliedNames.length > 0 ? latestAppliedMigrationName(appliedNames) : null,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function formatCheckLimitPendingLines(issue: CheckLimitPendingIssue): string[] {
|
|
325
|
+
const { canisterName, pending, checkLimit } = issue;
|
|
326
|
+
return [
|
|
327
|
+
`Canister '${canisterName}' has ${pending.length} pending migration(s) but check-limit=${checkLimit}. ` +
|
|
328
|
+
`Fold all changes into the latest pending migration: ${pending[pending.length - 1]}`,
|
|
329
|
+
` Pending: ${pending.join(", ")}`,
|
|
330
|
+
...(issue.latestApplied
|
|
331
|
+
? [` Latest already applied migration: ${issue.latestApplied}`]
|
|
332
|
+
: []),
|
|
333
|
+
];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Warn on accidental compat pass; fail with this diagnostic when compat already failed. */
|
|
337
|
+
export function reportCheckLimitPendingIssue(
|
|
338
|
+
issue: CheckLimitPendingIssue,
|
|
339
|
+
compatFailed: boolean,
|
|
340
|
+
): void {
|
|
341
|
+
const lines = formatCheckLimitPendingLines(issue);
|
|
342
|
+
if (compatFailed) {
|
|
343
|
+
cliError(
|
|
344
|
+
`✗ Stable compatibility check failed for canister '${issue.canisterName}': too many pending migrations for check-limit=${issue.checkLimit}\n` +
|
|
345
|
+
lines.join("\n"),
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
console.warn(chalk.yellow(`WARN: ${lines[0]}`));
|
|
349
|
+
for (const line of lines.slice(1)) {
|
|
350
|
+
console.warn(chalk.yellow(line));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
237
354
|
/**
|
|
238
355
|
* Absolute paths of chain migration files that `mops lint` should skip,
|
|
239
356
|
* mirroring the `check-limit` trimming applied to `moc` during `mops check`.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Enhanced-migration names recorded in a Version 4.0.0 `.most` chain block. */
|
|
2
|
+
export function parseMostAppliedMigrationNames(
|
|
3
|
+
content: string,
|
|
4
|
+
): string[] | null {
|
|
5
|
+
const version = content.match(/^\/\/ Version: ([^\n]+)/m)?.[1]?.trim();
|
|
6
|
+
if (!version) {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
if (version !== "4.0.0") {
|
|
10
|
+
return [];
|
|
11
|
+
}
|
|
12
|
+
// Safe: actor types in type aliases are always indented (col ≥ 2); the main actor is at col 0.
|
|
13
|
+
const actorIdx = content.search(/\nactor\b/);
|
|
14
|
+
if (actorIdx < 0) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const chainBlock = content.slice(0, actorIdx);
|
|
18
|
+
const names: string[] = [];
|
|
19
|
+
for (const match of chainBlock.matchAll(/[{;]\s*"([^"]+)"\s*:/gs)) {
|
|
20
|
+
names.push(match[1]!);
|
|
21
|
+
}
|
|
22
|
+
return names;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function migrationBasename(file: string): string {
|
|
26
|
+
return file.endsWith(".mo") ? file.slice(0, -3) : file;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Lexicographic max of applied migration names (matches chain file sort order). */
|
|
30
|
+
export function latestAppliedMigrationName(appliedNames: string[]): string {
|
|
31
|
+
return appliedNames.reduce((max, name) => (name > max ? name : max), "");
|
|
32
|
+
}
|
package/package.json
CHANGED
|
@@ -8,6 +8,17 @@ exports[`check-stable compatible upgrade from .mo file 1`] = `
|
|
|
8
8
|
}
|
|
9
9
|
`;
|
|
10
10
|
|
|
11
|
+
exports[`check-stable fails with check-limit diagnostic when pending migrations exceed check-limit 1`] = `
|
|
12
|
+
{
|
|
13
|
+
"exitCode": 1,
|
|
14
|
+
"stderr": "✗ Stable compatibility check failed for canister 'backend': too many pending migrations for check-limit=1
|
|
15
|
+
Canister 'backend' has 2 pending migration(s) but check-limit=1. Fold all changes into the latest pending migration: 20250301_000000_AddD.mo
|
|
16
|
+
Pending: 20250201_000000_AddField.mo, 20250301_000000_AddD.mo
|
|
17
|
+
Latest already applied migration: 20250101_000000_Init",
|
|
18
|
+
"stdout": "",
|
|
19
|
+
}
|
|
20
|
+
`;
|
|
21
|
+
|
|
11
22
|
exports[`check-stable incompatible upgrade from .mo file 1`] = `
|
|
12
23
|
{
|
|
13
24
|
"exitCode": 1,
|
|
@@ -77,7 +77,7 @@ exports[`lint [lint.extra] edge cases: pass, empty value, no-match, missing dir
|
|
|
77
77
|
}
|
|
78
78
|
`;
|
|
79
79
|
|
|
80
|
-
exports[`lint [lint.extra] example rules: no-types, types-only, migration-only 1`] = `
|
|
80
|
+
exports[`lint [lint.extra] example rules: no-types, types-only, migration-only, migration-self-contained 1`] = `
|
|
81
81
|
{
|
|
82
82
|
"exitCode": 1,
|
|
83
83
|
"stderr": " × [ERROR]: no-types
|
|
@@ -114,6 +114,34 @@ Error: Found 1 errors
|
|
|
114
114
|
╰────
|
|
115
115
|
|
|
116
116
|
Error: Found 1 errors
|
|
117
|
+
× [ERROR]: migration-self-contained
|
|
118
|
+
╭─[<TEST_DIR>/lint-extra-example-rules/migrations/20250101_000000_Init.mo:3:14]
|
|
119
|
+
2 │ import Backend "canister:backend";
|
|
120
|
+
3 │ import Types "Types";
|
|
121
|
+
· ───┬───
|
|
122
|
+
· ╰── Migration files must not import local modules (relative paths). Got: "Types"
|
|
123
|
+
4 │ import TypesDot "./Types";
|
|
124
|
+
╰────
|
|
125
|
+
|
|
126
|
+
× [ERROR]: migration-self-contained
|
|
127
|
+
╭─[<TEST_DIR>/lint-extra-example-rules/migrations/20250101_000000_Init.mo:4:17]
|
|
128
|
+
3 │ import Types "Types";
|
|
129
|
+
4 │ import TypesDot "./Types";
|
|
130
|
+
· ────┬────
|
|
131
|
+
· ╰── Migration files must not import local modules (relative paths). Got: "./Types"
|
|
132
|
+
5 │ import TypesParent "../src/Types";
|
|
133
|
+
╰────
|
|
134
|
+
|
|
135
|
+
× [ERROR]: migration-self-contained
|
|
136
|
+
╭─[<TEST_DIR>/lint-extra-example-rules/migrations/20250101_000000_Init.mo:5:20]
|
|
137
|
+
4 │ import TypesDot "./Types";
|
|
138
|
+
5 │ import TypesParent "../src/Types";
|
|
139
|
+
· ───────┬──────
|
|
140
|
+
· ╰── Migration files must not import local modules (relative paths). Got: "../src/Types"
|
|
141
|
+
6 │
|
|
142
|
+
╰────
|
|
143
|
+
|
|
144
|
+
Error: Found 3 errors
|
|
117
145
|
Lint failed",
|
|
118
146
|
"stdout": "",
|
|
119
147
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[toolchain]
|
|
2
|
+
moc = "1.5.0"
|
|
3
|
+
|
|
4
|
+
[moc]
|
|
5
|
+
args = ["--default-persistent-actors"]
|
|
6
|
+
|
|
7
|
+
[canisters.backend]
|
|
8
|
+
main = "src/main.mo"
|
|
9
|
+
|
|
10
|
+
[canisters.backend.migrations]
|
|
11
|
+
chain = "migrations"
|
|
12
|
+
check-limit = 1
|
|
13
|
+
|
|
14
|
+
[canisters.backend.check-stable]
|
|
15
|
+
path = "deployed.most"
|
|
@@ -106,4 +106,28 @@ describe("check-stable", () => {
|
|
|
106
106
|
expect(result.stdout).toMatch(/Stable compatibility check passed/);
|
|
107
107
|
}
|
|
108
108
|
}, 60_000);
|
|
109
|
+
|
|
110
|
+
test("fails with check-limit diagnostic when pending migrations exceed check-limit", async () => {
|
|
111
|
+
const cwd = path.join(
|
|
112
|
+
import.meta.dirname,
|
|
113
|
+
"check-stable/check-limit-warning",
|
|
114
|
+
);
|
|
115
|
+
await cliSnapshot(["check-stable"], { cwd }, 1);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("does not warn when deployed baseline matches the chain", async () => {
|
|
119
|
+
const cwd = path.join(import.meta.dirname, "check-stable/migrations-chain");
|
|
120
|
+
const result = await cli(["check-stable"], { cwd });
|
|
121
|
+
expect(result.exitCode).toBe(0);
|
|
122
|
+
expect(result.stderr).not.toMatch(/pending migration\(s\) but check-limit/);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("--no-check-limit suppresses pending migration warning", async () => {
|
|
126
|
+
const cwd = path.join(
|
|
127
|
+
import.meta.dirname,
|
|
128
|
+
"check-stable/check-limit-warning",
|
|
129
|
+
);
|
|
130
|
+
const result = await cli(["check-stable", "--no-check-limit"], { cwd });
|
|
131
|
+
expect(result.stderr).not.toMatch(/pending migration\(s\) but check-limit/);
|
|
132
|
+
});
|
|
109
133
|
});
|
package/tests/check.test.ts
CHANGED
|
@@ -195,4 +195,13 @@ describe("check", () => {
|
|
|
195
195
|
expect(result.stdout).not.toMatch(/\.migrations-/);
|
|
196
196
|
expect(result.stdout).not.toMatch(/-A=M0254/);
|
|
197
197
|
});
|
|
198
|
+
|
|
199
|
+
test("--no-check-limit suppresses pending migration warning in check", async () => {
|
|
200
|
+
const cwd = path.join(
|
|
201
|
+
import.meta.dirname,
|
|
202
|
+
"check-stable/check-limit-warning",
|
|
203
|
+
);
|
|
204
|
+
const result = await cli(["check", "--no-check-limit"], { cwd });
|
|
205
|
+
expect(result.stderr).not.toMatch(/pending migration\(s\) but check-limit/);
|
|
206
|
+
});
|
|
198
207
|
});
|
package/tests/lint.test.ts
CHANGED
|
@@ -83,7 +83,7 @@ describe("lint", () => {
|
|
|
83
83
|
);
|
|
84
84
|
});
|
|
85
85
|
|
|
86
|
-
test("example rules: no-types, types-only, migration-only", async () => {
|
|
86
|
+
test("example rules: no-types, types-only, migration-only, migration-self-contained", async () => {
|
|
87
87
|
const cwd = path.join(import.meta.dirname, "lint-extra-example-rules");
|
|
88
88
|
await cliSnapshot(["lint"], { cwd }, 1);
|
|
89
89
|
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import {
|
|
3
|
+
latestAppliedMigrationName,
|
|
4
|
+
parseMostAppliedMigrationNames,
|
|
5
|
+
} from "../helpers/parse-most";
|
|
6
|
+
|
|
7
|
+
describe("parseMostAppliedMigrationNames", () => {
|
|
8
|
+
test("parses Version 4.0.0 enhanced-migration chain", () => {
|
|
9
|
+
const content = `// Version: 4.0.0
|
|
10
|
+
{
|
|
11
|
+
"20250101_000000_Init" : {} -> {a : Nat; b : Text};
|
|
12
|
+
"20250201_000000_AddField" : (old : {a : Nat; b : Text}) -> {a : Nat; b : Text; c : Bool};
|
|
13
|
+
}
|
|
14
|
+
actor {
|
|
15
|
+
stable a : Nat;
|
|
16
|
+
stable b : Text;
|
|
17
|
+
stable c : Bool
|
|
18
|
+
};
|
|
19
|
+
`;
|
|
20
|
+
expect(parseMostAppliedMigrationNames(content)).toEqual([
|
|
21
|
+
"20250101_000000_Init",
|
|
22
|
+
"20250201_000000_AddField",
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("returns empty array for Version 1.0.0 (no EM chain)", () => {
|
|
27
|
+
expect(
|
|
28
|
+
parseMostAppliedMigrationNames("// Version: 1.0.0\nactor { };\n"),
|
|
29
|
+
).toEqual([]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("returns empty array for Version 3.0.0 (legacy migration)", () => {
|
|
33
|
+
const content = `// Version: 3.0.0
|
|
34
|
+
actor ({
|
|
35
|
+
}, {
|
|
36
|
+
stable var field1 : Nat;
|
|
37
|
+
stable var field2 : Text
|
|
38
|
+
});
|
|
39
|
+
`;
|
|
40
|
+
expect(parseMostAppliedMigrationNames(content)).toEqual([]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("returns null when version header is missing", () => {
|
|
44
|
+
expect(parseMostAppliedMigrationNames("actor { };\n")).toBeNull();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("returns null when Version 4.0.0 has no actor block", () => {
|
|
48
|
+
expect(
|
|
49
|
+
parseMostAppliedMigrationNames(
|
|
50
|
+
'// Version: 4.0.0\n{ "Init" : {} -> {} }',
|
|
51
|
+
),
|
|
52
|
+
).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("latestAppliedMigrationName picks lexicographic max", () => {
|
|
56
|
+
expect(
|
|
57
|
+
latestAppliedMigrationName([
|
|
58
|
+
"20250101_000000_Init",
|
|
59
|
+
"20250301_000000_AddD",
|
|
60
|
+
"20250201_000000_AddField",
|
|
61
|
+
]),
|
|
62
|
+
).toBe("20250301_000000_AddD");
|
|
63
|
+
});
|
|
64
|
+
});
|