ic-mops 2.13.1 → 2.13.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 +6 -0
- package/bundle/cli.tgz +0 -0
- package/commands/check-stable.ts +27 -23
- package/commands/check.ts +0 -1
- package/dist/commands/check-stable.d.ts +0 -1
- package/dist/commands/check-stable.js +21 -18
- package/dist/commands/check.js +0 -1
- package/dist/helpers/migrations.js +7 -5
- package/dist/package.json +1 -1
- package/dist/tests/check-stable.test.js +17 -0
- package/dist/tests/check.test.js +4 -3
- package/dist/tests/helpers.js +5 -1
- package/dist/tests/migrate.test.js +2 -2
- package/dist/types.d.ts +1 -0
- package/helpers/migrations.ts +7 -4
- package/package.json +1 -1
- package/tests/__snapshots__/migrate.test.ts.snap +1 -2
- package/tests/check-stable/migrations-chain/deployed.most +14 -0
- package/tests/check-stable/migrations-chain/migrations/20250101_000000_Init.mo +8 -0
- package/tests/check-stable/migrations-chain/migrations/20250201_000000_AddField.mo +9 -0
- package/tests/check-stable/migrations-chain/migrations/20250301_000000_AddD.mo +10 -0
- package/tests/check-stable/migrations-chain/migrations/20250401_000000_AddE.mo +11 -0
- package/tests/check-stable/migrations-chain/mops.toml +15 -0
- package/tests/check-stable/migrations-chain/src/main.mo +13 -0
- package/tests/check-stable.test.ts +20 -0
- package/tests/check.test.ts +4 -3
- package/tests/helpers.ts +5 -1
- package/tests/migrate.test.ts +2 -2
- package/types.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Next
|
|
4
4
|
|
|
5
|
+
## 2.13.2
|
|
6
|
+
- Fix race conditions when two `mops` processes run on the same project (e.g. an editor watcher and `caffeine check --fix`, or back-to-back invocations). `mops check-stable` used a shared `.mops/.check-stable/` scratch dir and `mops check`/`build`/`check-stable` used a shared `<parent>/.migrations-<canister>/` staging dir; concurrent runs would clobber each other and surface as misleading errors like `.mops/.check-stable/new.most: No such file or directory` or `EEXIST: file already exists, symlink ...`. Both directories are now per-invocation (created via `mkdtemp` and removed when the command finishes).
|
|
7
|
+
- Deprecate `skipIfMissing` in `[canisters.<name>.check-stable]`. Behavior is unchanged for now, but `mops check`/`check-stable` print a warning when it is set. For initial deployments, commit a `.most` file at the configured `path` containing an empty actor (`// Version: 1.0.0\nactor { };`) instead — the stable check then runs against an empty baseline.
|
|
8
|
+
- Drop the "you may need a migration" hint after a failed stable compatibility check in `mops check`/`check-stable`. The hint guessed at whether the user needed a new migration or a fix to an existing one, and `moc`'s underlying compatibility error already links to the migration docs.
|
|
9
|
+
- The missing-chain-directory error from `mops check`/`build`/`check-stable` now points at adding a `.mo` file to the `chain` directory instead of running the experimental `mops migrate new <Name>` command.
|
|
10
|
+
|
|
5
11
|
## 2.13.1
|
|
6
12
|
- `mops lint` now honors `[canisters.<name>.migrations].check-limit`, skipping trimmed chain migrations so projects with large migration histories lint as fast as they type-check. Pass an explicit filter (`mops lint <name>`) to opt back in for a one-off lint of a trimmed file.
|
|
7
13
|
|
package/bundle/cli.tgz
CHANGED
|
Binary file
|
package/commands/check-stable.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync } from "node:fs";
|
|
3
3
|
import { rm } from "node:fs/promises";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { execa } from "execa";
|
|
@@ -17,7 +17,10 @@ import {
|
|
|
17
17
|
import { sourcesArgs } from "./sources.js";
|
|
18
18
|
import { toolchain } from "./toolchain/index.js";
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// Per-invocation scratch dir lives under `.mops/`; `mkdtempSync` makes it unique so
|
|
21
|
+
// concurrent `mops` processes don't clobber each other's `old.most`/`new.most`.
|
|
22
|
+
const CHECK_STABLE_PARENT = ".mops";
|
|
23
|
+
const CHECK_STABLE_PREFIX = ".check-stable-";
|
|
21
24
|
|
|
22
25
|
export interface CheckStableOptions {
|
|
23
26
|
verbose: boolean;
|
|
@@ -39,15 +42,25 @@ export function resolveStablePath(
|
|
|
39
42
|
return null;
|
|
40
43
|
}
|
|
41
44
|
const stablePath = resolveConfigPath(stableConfig.path);
|
|
45
|
+
if (stableConfig.skipIfMissing) {
|
|
46
|
+
console.warn(
|
|
47
|
+
chalk.yellow(
|
|
48
|
+
`WARN: \`skipIfMissing\` in [canisters.${canisterName}.check-stable] is deprecated. ` +
|
|
49
|
+
`Instead, create ${stableConfig.path} with an empty actor:\n` +
|
|
50
|
+
" // Version: 1.0.0\n" +
|
|
51
|
+
" actor { };",
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
}
|
|
42
55
|
if (!existsSync(stablePath)) {
|
|
43
56
|
if (stableConfig.skipIfMissing) {
|
|
44
57
|
return null;
|
|
45
58
|
}
|
|
46
59
|
cliError(
|
|
47
60
|
`Deployed file not found: ${stablePath} (canister '${canisterName}')\n` +
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
"
|
|
61
|
+
`Create ${stableConfig.path} with an empty actor to enable the check:\n` +
|
|
62
|
+
" // Version: 1.0.0\n" +
|
|
63
|
+
" actor { };",
|
|
51
64
|
);
|
|
52
65
|
}
|
|
53
66
|
return stablePath;
|
|
@@ -88,7 +101,6 @@ export async function checkStable(
|
|
|
88
101
|
globalMocArgs,
|
|
89
102
|
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
|
|
90
103
|
options,
|
|
91
|
-
hasMigrations: !!canister.migrations,
|
|
92
104
|
});
|
|
93
105
|
} finally {
|
|
94
106
|
await migration.cleanup();
|
|
@@ -131,7 +143,6 @@ export async function checkStable(
|
|
|
131
143
|
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
|
|
132
144
|
sources,
|
|
133
145
|
options,
|
|
134
|
-
hasMigrations: !!canister.migrations,
|
|
135
146
|
});
|
|
136
147
|
} finally {
|
|
137
148
|
await migration.cleanup();
|
|
@@ -159,7 +170,6 @@ export interface RunStableCheckParams {
|
|
|
159
170
|
canisterArgs: string[];
|
|
160
171
|
sources?: string[];
|
|
161
172
|
options?: Partial<CheckStableOptions>;
|
|
162
|
-
hasMigrations?: boolean;
|
|
163
173
|
}
|
|
164
174
|
|
|
165
175
|
export async function runStableCheck(
|
|
@@ -182,15 +192,17 @@ export async function runStableCheck(
|
|
|
182
192
|
cliError(`File not found: ${oldFile}`);
|
|
183
193
|
}
|
|
184
194
|
|
|
185
|
-
|
|
186
|
-
|
|
195
|
+
mkdirSync(CHECK_STABLE_PARENT, { recursive: true });
|
|
196
|
+
const scratchDir = mkdtempSync(
|
|
197
|
+
join(CHECK_STABLE_PARENT, CHECK_STABLE_PREFIX),
|
|
198
|
+
);
|
|
187
199
|
try {
|
|
188
200
|
const oldMostPath = isOldMostFile
|
|
189
201
|
? oldFile
|
|
190
202
|
: await generateStableTypes(
|
|
191
203
|
mocPath,
|
|
192
204
|
oldFile,
|
|
193
|
-
join(
|
|
205
|
+
join(scratchDir, "old.most"),
|
|
194
206
|
sources,
|
|
195
207
|
globalMocArgs,
|
|
196
208
|
canisterArgs,
|
|
@@ -200,7 +212,7 @@ export async function runStableCheck(
|
|
|
200
212
|
const newMostPath = await generateStableTypes(
|
|
201
213
|
mocPath,
|
|
202
214
|
canisterMain,
|
|
203
|
-
join(
|
|
215
|
+
join(scratchDir, "new.most"),
|
|
204
216
|
sources,
|
|
205
217
|
globalMocArgs,
|
|
206
218
|
canisterArgs,
|
|
@@ -228,13 +240,6 @@ export async function runStableCheck(
|
|
|
228
240
|
if (result.stderr) {
|
|
229
241
|
console.error(result.stderr);
|
|
230
242
|
}
|
|
231
|
-
if (params.hasMigrations) {
|
|
232
|
-
console.error(
|
|
233
|
-
chalk.yellow(
|
|
234
|
-
"Hint: You may need a migration. Run `mops migrate new <Name>` to create one.",
|
|
235
|
-
),
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
243
|
cliError(
|
|
239
244
|
`✗ Stable compatibility check failed for canister '${canisterName}'`,
|
|
240
245
|
);
|
|
@@ -246,7 +251,7 @@ export async function runStableCheck(
|
|
|
246
251
|
),
|
|
247
252
|
);
|
|
248
253
|
} finally {
|
|
249
|
-
await rm(
|
|
254
|
+
await rm(scratchDir, { recursive: true, force: true });
|
|
250
255
|
}
|
|
251
256
|
}
|
|
252
257
|
|
|
@@ -259,8 +264,7 @@ async function generateStableTypes(
|
|
|
259
264
|
canisterArgs: string[],
|
|
260
265
|
options: Partial<CheckStableOptions>,
|
|
261
266
|
): Promise<string> {
|
|
262
|
-
const
|
|
263
|
-
const wasmPath = join(CHECK_STABLE_DIR, base + ".wasm");
|
|
267
|
+
const wasmPath = outputPath.replace(/\.most$/, ".wasm");
|
|
264
268
|
const args = [
|
|
265
269
|
"--stable-types",
|
|
266
270
|
"-o",
|
package/commands/check.ts
CHANGED
|
@@ -216,7 +216,6 @@ async function checkCanisters(
|
|
|
216
216
|
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
|
|
217
217
|
sources,
|
|
218
218
|
options: { verbose: options.verbose, extraArgs: options.extraArgs },
|
|
219
|
-
hasMigrations: !!canister.migrations,
|
|
220
219
|
});
|
|
221
220
|
}
|
|
222
221
|
} finally {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, mkdirSync } from "node:fs";
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync } from "node:fs";
|
|
3
3
|
import { rm } from "node:fs/promises";
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { execa } from "execa";
|
|
@@ -9,7 +9,10 @@ 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";
|
|
11
11
|
import { toolchain } from "./toolchain/index.js";
|
|
12
|
-
|
|
12
|
+
// Per-invocation scratch dir lives under `.mops/`; `mkdtempSync` makes it unique so
|
|
13
|
+
// concurrent `mops` processes don't clobber each other's `old.most`/`new.most`.
|
|
14
|
+
const CHECK_STABLE_PARENT = ".mops";
|
|
15
|
+
const CHECK_STABLE_PREFIX = ".check-stable-";
|
|
13
16
|
export function resolveStablePath(canister, canisterName, options) {
|
|
14
17
|
const stableConfig = canister["check-stable"];
|
|
15
18
|
if (!stableConfig) {
|
|
@@ -19,14 +22,20 @@ export function resolveStablePath(canister, canisterName, options) {
|
|
|
19
22
|
return null;
|
|
20
23
|
}
|
|
21
24
|
const stablePath = resolveConfigPath(stableConfig.path);
|
|
25
|
+
if (stableConfig.skipIfMissing) {
|
|
26
|
+
console.warn(chalk.yellow(`WARN: \`skipIfMissing\` in [canisters.${canisterName}.check-stable] is deprecated. ` +
|
|
27
|
+
`Instead, create ${stableConfig.path} with an empty actor:\n` +
|
|
28
|
+
" // Version: 1.0.0\n" +
|
|
29
|
+
" actor { };"));
|
|
30
|
+
}
|
|
22
31
|
if (!existsSync(stablePath)) {
|
|
23
32
|
if (stableConfig.skipIfMissing) {
|
|
24
33
|
return null;
|
|
25
34
|
}
|
|
26
35
|
cliError(`Deployed file not found: ${stablePath} (canister '${canisterName}')\n` +
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
36
|
+
`Create ${stableConfig.path} with an empty actor to enable the check:\n` +
|
|
37
|
+
" // Version: 1.0.0\n" +
|
|
38
|
+
" actor { };");
|
|
30
39
|
}
|
|
31
40
|
return stablePath;
|
|
32
41
|
}
|
|
@@ -53,7 +62,6 @@ export async function checkStable(args, options = {}) {
|
|
|
53
62
|
globalMocArgs,
|
|
54
63
|
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
|
|
55
64
|
options,
|
|
56
|
-
hasMigrations: !!canister.migrations,
|
|
57
65
|
});
|
|
58
66
|
}
|
|
59
67
|
finally {
|
|
@@ -88,7 +96,6 @@ export async function checkStable(args, options = {}) {
|
|
|
88
96
|
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
|
|
89
97
|
sources,
|
|
90
98
|
options,
|
|
91
|
-
hasMigrations: !!canister.migrations,
|
|
92
99
|
});
|
|
93
100
|
}
|
|
94
101
|
finally {
|
|
@@ -111,13 +118,13 @@ export async function runStableCheck(params) {
|
|
|
111
118
|
if (!existsSync(oldFile)) {
|
|
112
119
|
cliError(`File not found: ${oldFile}`);
|
|
113
120
|
}
|
|
114
|
-
|
|
115
|
-
|
|
121
|
+
mkdirSync(CHECK_STABLE_PARENT, { recursive: true });
|
|
122
|
+
const scratchDir = mkdtempSync(join(CHECK_STABLE_PARENT, CHECK_STABLE_PREFIX));
|
|
116
123
|
try {
|
|
117
124
|
const oldMostPath = isOldMostFile
|
|
118
125
|
? oldFile
|
|
119
|
-
: await generateStableTypes(mocPath, oldFile, join(
|
|
120
|
-
const newMostPath = await generateStableTypes(mocPath, canisterMain, join(
|
|
126
|
+
: await generateStableTypes(mocPath, oldFile, join(scratchDir, "old.most"), sources, globalMocArgs, canisterArgs, options);
|
|
127
|
+
const newMostPath = await generateStableTypes(mocPath, canisterMain, join(scratchDir, "new.most"), sources, globalMocArgs, canisterArgs, options);
|
|
121
128
|
if (options.verbose) {
|
|
122
129
|
console.log(chalk.blue("check-stable"), chalk.gray(`Comparing ${oldMostPath} ↔ ${newMostPath}`));
|
|
123
130
|
}
|
|
@@ -133,20 +140,16 @@ export async function runStableCheck(params) {
|
|
|
133
140
|
if (result.stderr) {
|
|
134
141
|
console.error(result.stderr);
|
|
135
142
|
}
|
|
136
|
-
if (params.hasMigrations) {
|
|
137
|
-
console.error(chalk.yellow("Hint: You may need a migration. Run `mops migrate new <Name>` to create one."));
|
|
138
|
-
}
|
|
139
143
|
cliError(`✗ Stable compatibility check failed for canister '${canisterName}'`);
|
|
140
144
|
}
|
|
141
145
|
console.log(chalk.green(`✓ Stable compatibility check passed for canister '${canisterName}'`));
|
|
142
146
|
}
|
|
143
147
|
finally {
|
|
144
|
-
await rm(
|
|
148
|
+
await rm(scratchDir, { recursive: true, force: true });
|
|
145
149
|
}
|
|
146
150
|
}
|
|
147
151
|
async function generateStableTypes(mocPath, moFile, outputPath, sources, globalMocArgs, canisterArgs, options) {
|
|
148
|
-
const
|
|
149
|
-
const wasmPath = join(CHECK_STABLE_DIR, base + ".wasm");
|
|
152
|
+
const wasmPath = outputPath.replace(/\.most$/, ".wasm");
|
|
150
153
|
const args = [
|
|
151
154
|
"--stable-types",
|
|
152
155
|
"-o",
|
package/dist/commands/check.js
CHANGED
|
@@ -137,7 +137,6 @@ async function checkCanisters(config, canisters, options) {
|
|
|
137
137
|
canisterArgs: [...migration.migrationArgs, ...(canister.args ?? [])],
|
|
138
138
|
sources,
|
|
139
139
|
options: { verbose: options.verbose, extraArgs: options.extraArgs },
|
|
140
|
-
hasMigrations: !!canister.migrations,
|
|
141
140
|
});
|
|
142
141
|
}
|
|
143
142
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readdirSync, symlinkSync, writeFileSync, } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, 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";
|
|
@@ -77,7 +77,7 @@ function resolveMigrationChain(migrations, canisterName, mode) {
|
|
|
77
77
|
const nextFile = nextDir ? getNextMigrationFile(nextDir) : null;
|
|
78
78
|
if (!existsSync(chainDir) && !nextFile) {
|
|
79
79
|
cliError(`Migration chain directory not found: ${chainDir}\n` +
|
|
80
|
-
"
|
|
80
|
+
"Create the directory and add a `.mo` migration file to initialize the chain.");
|
|
81
81
|
}
|
|
82
82
|
const chainFiles = getMigrationFiles(chainDir);
|
|
83
83
|
if (nextFile) {
|
|
@@ -122,9 +122,11 @@ export async function prepareMigrationArgs(migrations, canisterName, mode, verbo
|
|
|
122
122
|
}
|
|
123
123
|
return { migrationArgs, cleanup: async () => { } };
|
|
124
124
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
// Per-invocation staging dir; `mkdtempSync` makes it unique so concurrent `mops`
|
|
126
|
+
// processes don't clobber each other's symlinks. Cleaned up below in `cleanup()`.
|
|
127
|
+
const baseDir = stagedMigrationsDir(chainDir, canisterName);
|
|
128
|
+
mkdirSync(dirname(baseDir), { recursive: true });
|
|
129
|
+
const tempDir = mkdtempSync(`${baseDir}-`);
|
|
128
130
|
writeFileSync(join(tempDir, ".gitignore"), "*\n");
|
|
129
131
|
for (const { file, dir } of included) {
|
|
130
132
|
symlinkSync(resolve(dir, file), join(tempDir, file));
|
package/dist/package.json
CHANGED
|
@@ -77,4 +77,21 @@ describe("check-stable", () => {
|
|
|
77
77
|
expect(result.exitCode).toBe(1);
|
|
78
78
|
expect(result.stderr).toMatch(/File not found/);
|
|
79
79
|
});
|
|
80
|
+
// Regression: two concurrent `mops check-stable` runs on the same project used to clobber
|
|
81
|
+
// each other's `.mops/.check-stable/new.most` and the staged migration symlinks, surfacing
|
|
82
|
+
// as a misleading `new.most: No such file or directory` or an `EEXIST: symlink` crash.
|
|
83
|
+
test("concurrent runs do not clobber each other's scratch state", async () => {
|
|
84
|
+
const cwd = path.join(import.meta.dirname, "check-stable/migrations-chain");
|
|
85
|
+
const results = await Promise.all(Array.from({ length: 10 }, () => cli(["check-stable"], { cwd })));
|
|
86
|
+
for (const result of results) {
|
|
87
|
+
expect({
|
|
88
|
+
exitCode: result.exitCode,
|
|
89
|
+
stderr: result.stderr,
|
|
90
|
+
}).toEqual({
|
|
91
|
+
exitCode: 0,
|
|
92
|
+
stderr: "",
|
|
93
|
+
});
|
|
94
|
+
expect(result.stdout).toMatch(/Stable compatibility check passed/);
|
|
95
|
+
}
|
|
96
|
+
}, 60_000);
|
|
80
97
|
});
|
package/dist/tests/check.test.js
CHANGED
|
@@ -86,18 +86,19 @@ describe("check", () => {
|
|
|
86
86
|
expect(result.exitCode).toBe(0);
|
|
87
87
|
expect(result.stdout).toMatch(/Stable compatibility check passed/);
|
|
88
88
|
});
|
|
89
|
-
test("deployed:
|
|
89
|
+
test("deployed: skips when file missing and skipIfMissing, with deprecation warning", async () => {
|
|
90
90
|
const cwd = path.join(import.meta.dirname, "check/deployed-missing-skip");
|
|
91
91
|
const result = await cli(["check"], { cwd });
|
|
92
92
|
expect(result.exitCode).toBe(0);
|
|
93
93
|
expect(result.stdout).not.toMatch(/stable/i);
|
|
94
|
+
expect(result.stderr).toMatch(/skipIfMissing.*deprecated/);
|
|
94
95
|
});
|
|
95
|
-
test("deployed: errors when file missing
|
|
96
|
+
test("deployed: errors when file missing", async () => {
|
|
96
97
|
const cwd = path.join(import.meta.dirname, "check/deployed-missing-error");
|
|
97
98
|
const result = await cli(["check"], { cwd });
|
|
98
99
|
expect(result.exitCode).toBe(1);
|
|
99
100
|
expect(result.stderr).toMatch(/Deployed file not found/);
|
|
100
|
-
expect(result.stderr).toMatch(/
|
|
101
|
+
expect(result.stderr).toMatch(/empty actor/);
|
|
101
102
|
});
|
|
102
103
|
test("--fix runs stable check after fixing", async () => {
|
|
103
104
|
const cwd = path.join(import.meta.dirname, "check/deployed-compatible");
|
package/dist/tests/helpers.js
CHANGED
|
@@ -26,7 +26,11 @@ export const normalizePaths = (text) => {
|
|
|
26
26
|
.replace(/\/[^\s"]+\/\.cache\/mops/g, "<CACHE>")
|
|
27
27
|
.replace(/\/[^\s"]+\/Library\/Caches\/mops/g, "<CACHE>")
|
|
28
28
|
.replace(/\/[^\s"[\]]+\/moc(?:-wrapper)?(?=\s|$)/g, "moc-wrapper")
|
|
29
|
-
.replace(/\/[^\s"[\]]+\.motoko\/bin\/moc/g, "moc-wrapper")
|
|
29
|
+
.replace(/\/[^\s"[\]]+\.motoko\/bin\/moc/g, "moc-wrapper")
|
|
30
|
+
// Per-invocation scratch / staging dirs use mkdtemp; redact the random suffix
|
|
31
|
+
// (Node's exact suffix format isn't a stable contract) so snapshots stay stable.
|
|
32
|
+
.replace(/\.mops\/\.check-stable-\w+/g, ".mops/.check-stable")
|
|
33
|
+
.replace(/(\.migrations-[\w.-]+?)-\w+(?=[/\s"]|$)/g, "$1"));
|
|
30
34
|
};
|
|
31
35
|
export const cliSnapshot = async (args, options, exitCode) => {
|
|
32
36
|
const result = await cli(args, options);
|
|
@@ -173,8 +173,8 @@ describe("migrate", () => {
|
|
|
173
173
|
expect(most).toMatchSnapshot();
|
|
174
174
|
});
|
|
175
175
|
});
|
|
176
|
-
describe("stable check
|
|
177
|
-
test("stable check fails
|
|
176
|
+
describe("stable check", () => {
|
|
177
|
+
test("stable check fails when deployed.most is incompatible", async () => {
|
|
178
178
|
const cwd = await makeTempFixture("basic");
|
|
179
179
|
await writeFile(path.join(cwd, "deployed.most"), "// Version: 1.0.0\nactor {\n stable var a : Nat;\n stable var name : Int\n};\n");
|
|
180
180
|
await cliSnapshot(["check"], { cwd }, 1);
|
package/dist/types.d.ts
CHANGED
package/helpers/migrations.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
existsSync,
|
|
3
3
|
mkdirSync,
|
|
4
|
+
mkdtempSync,
|
|
4
5
|
readdirSync,
|
|
5
6
|
symlinkSync,
|
|
6
7
|
writeFileSync,
|
|
@@ -130,7 +131,7 @@ function resolveMigrationChain(
|
|
|
130
131
|
if (!existsSync(chainDir) && !nextFile) {
|
|
131
132
|
cliError(
|
|
132
133
|
`Migration chain directory not found: ${chainDir}\n` +
|
|
133
|
-
"
|
|
134
|
+
"Create the directory and add a `.mo` migration file to initialize the chain.",
|
|
134
135
|
);
|
|
135
136
|
}
|
|
136
137
|
|
|
@@ -193,9 +194,11 @@ export async function prepareMigrationArgs(
|
|
|
193
194
|
return { migrationArgs, cleanup: async () => {} };
|
|
194
195
|
}
|
|
195
196
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
197
|
+
// Per-invocation staging dir; `mkdtempSync` makes it unique so concurrent `mops`
|
|
198
|
+
// processes don't clobber each other's symlinks. Cleaned up below in `cleanup()`.
|
|
199
|
+
const baseDir = stagedMigrationsDir(chainDir, canisterName);
|
|
200
|
+
mkdirSync(dirname(baseDir), { recursive: true });
|
|
201
|
+
const tempDir = mkdtempSync(`${baseDir}-`);
|
|
199
202
|
writeFileSync(join(tempDir, ".gitignore"), "*\n");
|
|
200
203
|
|
|
201
204
|
for (const { file, dir } of included) {
|
package/package.json
CHANGED
|
@@ -143,12 +143,11 @@ exports[`migrate migrate new creates a migration file with timestamp and templat
|
|
|
143
143
|
}
|
|
144
144
|
`;
|
|
145
145
|
|
|
146
|
-
exports[`migrate stable check
|
|
146
|
+
exports[`migrate stable check stable check fails when deployed.most is incompatible 1`] = `
|
|
147
147
|
{
|
|
148
148
|
"exitCode": 1,
|
|
149
149
|
"stderr": "(unknown location): Compatibility error [M0169], the stable variable \`a\` of the previous version cannot be implicitly discarded. The variable can only be dropped by an explicit migration function, please see https://internetcomputer.org/docs/motoko/fundamentals/actors/compatibility#explicit-migration-using-a-migration-function
|
|
150
150
|
(unknown location): Compatibility error [M0169], the stable variable \`name\` of the previous version cannot be implicitly discarded. The variable can only be dropped by an explicit migration function, please see https://internetcomputer.org/docs/motoko/fundamentals/actors/compatibility#explicit-migration-using-a-migration-function
|
|
151
|
-
Hint: You may need a migration. Run \`mops migrate new <Name>\` to create one.
|
|
152
151
|
✗ Stable compatibility check failed for canister 'backend'",
|
|
153
152
|
"stdout": "✓ backend",
|
|
154
153
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Version: 4.0.0
|
|
2
|
+
{
|
|
3
|
+
"20250101_000000_Init" : {} -> {a : Nat; b : Text};
|
|
4
|
+
"20250201_000000_AddField" : (old : {a : Nat; b : Text}) -> {a : Nat; b : Text; c : Bool};
|
|
5
|
+
"20250301_000000_AddD" : (old : {a : Nat; b : Text; c : Bool}) -> {a : Nat; b : Text; c : Bool; d : Int};
|
|
6
|
+
"20250401_000000_AddE" : (old : {a : Nat; b : Text; c : Bool; d : Int}) -> {a : Nat; b : Text; c : Bool; d : Int; e : Text}
|
|
7
|
+
}
|
|
8
|
+
actor {
|
|
9
|
+
stable a : Nat;
|
|
10
|
+
stable b : Text;
|
|
11
|
+
stable c : Bool;
|
|
12
|
+
stable d : Int;
|
|
13
|
+
stable e : Text
|
|
14
|
+
};
|
|
@@ -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 = 3
|
|
13
|
+
|
|
14
|
+
[canisters.backend.check-stable]
|
|
15
|
+
path = "deployed.most"
|
|
@@ -86,4 +86,24 @@ describe("check-stable", () => {
|
|
|
86
86
|
expect(result.exitCode).toBe(1);
|
|
87
87
|
expect(result.stderr).toMatch(/File not found/);
|
|
88
88
|
});
|
|
89
|
+
|
|
90
|
+
// Regression: two concurrent `mops check-stable` runs on the same project used to clobber
|
|
91
|
+
// each other's `.mops/.check-stable/new.most` and the staged migration symlinks, surfacing
|
|
92
|
+
// as a misleading `new.most: No such file or directory` or an `EEXIST: symlink` crash.
|
|
93
|
+
test("concurrent runs do not clobber each other's scratch state", async () => {
|
|
94
|
+
const cwd = path.join(import.meta.dirname, "check-stable/migrations-chain");
|
|
95
|
+
const results = await Promise.all(
|
|
96
|
+
Array.from({ length: 10 }, () => cli(["check-stable"], { cwd })),
|
|
97
|
+
);
|
|
98
|
+
for (const result of results) {
|
|
99
|
+
expect({
|
|
100
|
+
exitCode: result.exitCode,
|
|
101
|
+
stderr: result.stderr,
|
|
102
|
+
}).toEqual({
|
|
103
|
+
exitCode: 0,
|
|
104
|
+
stderr: "",
|
|
105
|
+
});
|
|
106
|
+
expect(result.stdout).toMatch(/Stable compatibility check passed/);
|
|
107
|
+
}
|
|
108
|
+
}, 60_000);
|
|
89
109
|
});
|
package/tests/check.test.ts
CHANGED
|
@@ -113,19 +113,20 @@ describe("check", () => {
|
|
|
113
113
|
expect(result.stdout).toMatch(/Stable compatibility check passed/);
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
-
test("deployed:
|
|
116
|
+
test("deployed: skips when file missing and skipIfMissing, with deprecation warning", async () => {
|
|
117
117
|
const cwd = path.join(import.meta.dirname, "check/deployed-missing-skip");
|
|
118
118
|
const result = await cli(["check"], { cwd });
|
|
119
119
|
expect(result.exitCode).toBe(0);
|
|
120
120
|
expect(result.stdout).not.toMatch(/stable/i);
|
|
121
|
+
expect(result.stderr).toMatch(/skipIfMissing.*deprecated/);
|
|
121
122
|
});
|
|
122
123
|
|
|
123
|
-
test("deployed: errors when file missing
|
|
124
|
+
test("deployed: errors when file missing", async () => {
|
|
124
125
|
const cwd = path.join(import.meta.dirname, "check/deployed-missing-error");
|
|
125
126
|
const result = await cli(["check"], { cwd });
|
|
126
127
|
expect(result.exitCode).toBe(1);
|
|
127
128
|
expect(result.stderr).toMatch(/Deployed file not found/);
|
|
128
|
-
expect(result.stderr).toMatch(/
|
|
129
|
+
expect(result.stderr).toMatch(/empty actor/);
|
|
129
130
|
});
|
|
130
131
|
|
|
131
132
|
test("--fix runs stable check after fixing", async () => {
|
package/tests/helpers.ts
CHANGED
|
@@ -37,7 +37,11 @@ export const normalizePaths = (text: string): string => {
|
|
|
37
37
|
.replace(/\/[^\s"]+\/\.cache\/mops/g, "<CACHE>")
|
|
38
38
|
.replace(/\/[^\s"]+\/Library\/Caches\/mops/g, "<CACHE>")
|
|
39
39
|
.replace(/\/[^\s"[\]]+\/moc(?:-wrapper)?(?=\s|$)/g, "moc-wrapper")
|
|
40
|
-
.replace(/\/[^\s"[\]]+\.motoko\/bin\/moc/g, "moc-wrapper")
|
|
40
|
+
.replace(/\/[^\s"[\]]+\.motoko\/bin\/moc/g, "moc-wrapper")
|
|
41
|
+
// Per-invocation scratch / staging dirs use mkdtemp; redact the random suffix
|
|
42
|
+
// (Node's exact suffix format isn't a stable contract) so snapshots stay stable.
|
|
43
|
+
.replace(/\.mops\/\.check-stable-\w+/g, ".mops/.check-stable")
|
|
44
|
+
.replace(/(\.migrations-[\w.-]+?)-\w+(?=[/\s"]|$)/g, "$1"),
|
|
41
45
|
);
|
|
42
46
|
};
|
|
43
47
|
|
package/tests/migrate.test.ts
CHANGED
|
@@ -241,8 +241,8 @@ describe("migrate", () => {
|
|
|
241
241
|
});
|
|
242
242
|
});
|
|
243
243
|
|
|
244
|
-
describe("stable check
|
|
245
|
-
test("stable check fails
|
|
244
|
+
describe("stable check", () => {
|
|
245
|
+
test("stable check fails when deployed.most is incompatible", async () => {
|
|
246
246
|
const cwd = await makeTempFixture("basic");
|
|
247
247
|
await writeFile(
|
|
248
248
|
path.join(cwd, "deployed.most"),
|