@growth-labs/cms 0.3.0 → 0.3.1
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/README.md +43 -0
- package/dist/cli/migrations.d.ts +3 -0
- package/dist/cli/migrations.d.ts.map +1 -0
- package/dist/cli/migrations.js +73 -0
- package/dist/cli/migrations.js.map +1 -0
- package/dist/migration-vendor-files.d.ts +17 -0
- package/dist/migration-vendor-files.d.ts.map +1 -0
- package/dist/migration-vendor-files.js +67 -0
- package/dist/migration-vendor-files.js.map +1 -0
- package/dist/migration-vendor.d.ts +26 -0
- package/dist/migration-vendor.d.ts.map +1 -0
- package/dist/migration-vendor.js +321 -0
- package/dist/migration-vendor.js.map +1 -0
- package/package.json +8 -1
- package/src/cli/migrations.ts +85 -0
- package/src/migration-vendor-files.ts +100 -0
- package/src/migration-vendor.ts +450 -0
package/README.md
CHANGED
|
@@ -168,6 +168,49 @@ manual publish with a 409 and leaves the content status unchanged; throwing from
|
|
|
168
168
|
the hook fails closed with a 503. This keeps media-specific readiness checks in
|
|
169
169
|
the host site while preventing a configured site from bypassing them.
|
|
170
170
|
|
|
171
|
+
## Consumer D1 migrations
|
|
172
|
+
|
|
173
|
+
Wrangler accepts one `migrations_dir`, while a site commonly already owns
|
|
174
|
+
numbered analytics, SEO, or product migrations. Vendor the exact SQL from the
|
|
175
|
+
installed CMS package into that existing directory, review it, and commit it:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
pnpm exec growth-labs-cms-migrations sync --dir migrations --start 10
|
|
179
|
+
pnpm exec growth-labs-cms-migrations verify --dir migrations
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`--start` is the first unused site sequence and is mandatory. `sync` reserves a
|
|
183
|
+
contiguous range, copies every package migration without rewriting it, and writes
|
|
184
|
+
`migrations/.growth-labs-cms-migrations.json` with the package version and SHA-256
|
|
185
|
+
receipt. It fails before writing if any site SQL already owns a reserved sequence.
|
|
186
|
+
Existing managed SQL is never overwritten: consumer drift, upstream mutation of
|
|
187
|
+
an already-vendored migration, a changed starting sequence, or a symlink target
|
|
188
|
+
fails closed.
|
|
189
|
+
|
|
190
|
+
The default receipt path is the only manifest allowed inside `migrations/`.
|
|
191
|
+
When `--manifest` is supplied, it must name a `.json` file outside the migrations
|
|
192
|
+
directory whose parent is an existing non-symlink directory; it can never
|
|
193
|
+
collide with or become Wrangler SQL. Sync stages the full SQL set and receipt
|
|
194
|
+
before linking targets. A later link, verification, or receipt failure removes
|
|
195
|
+
every SQL file created by that attempt. An interrupted process may leave only
|
|
196
|
+
exact-hash linked SQL and/or `*.tmp-<uuid>` staging files, but no valid completed
|
|
197
|
+
receipt; `verify` therefore fails closed until a repeated `sync` reconciles the
|
|
198
|
+
batch. Wrangler ignores the temp files, which are safe to delete after the
|
|
199
|
+
repeated sync and verify succeed. Never run the managed D1 workflow without the
|
|
200
|
+
required verify step.
|
|
201
|
+
|
|
202
|
+
Both `sync` and `verify` acquire an exclusive adjacent `<receipt>.lock` file,
|
|
203
|
+
so concurrent invocations fail closed instead of racing the manifest update.
|
|
204
|
+
An abruptly terminated process can leave a stale lock; first confirm no vendor
|
|
205
|
+
or verify process is running, then remove only that lock and repeat `sync`
|
|
206
|
+
followed by `verify`.
|
|
207
|
+
|
|
208
|
+
Run `sync` deliberately after a package upgrade, inspect and commit the new SQL
|
|
209
|
+
and receipt, then require `verify` in pull-request CI before the managed D1
|
|
210
|
+
migration workflow. The command only reads and writes local files; it never opens
|
|
211
|
+
a network connection or applies a database migration. Do not call schema setup
|
|
212
|
+
from a request handler.
|
|
213
|
+
|
|
171
214
|
## Bounded publication kernel
|
|
172
215
|
|
|
173
216
|
`publishOne` is the build-independent publication path for a single content item.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../../src/cli/migrations.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { syncCmsMigrations, verifyCmsMigrations } from '../migration-vendor.js';
|
|
3
|
+
function usage() {
|
|
4
|
+
return [
|
|
5
|
+
'Usage:',
|
|
6
|
+
' growth-labs-cms-migrations sync --dir <migrations-dir> --start <positive-integer>',
|
|
7
|
+
' growth-labs-cms-migrations verify --dir <migrations-dir>',
|
|
8
|
+
'',
|
|
9
|
+
'Optional for both commands: --manifest <receipt-path>',
|
|
10
|
+
].join('\n');
|
|
11
|
+
}
|
|
12
|
+
function requireValue(argv, index, flag) {
|
|
13
|
+
const value = argv[index + 1];
|
|
14
|
+
if (!value || value.startsWith('--'))
|
|
15
|
+
throw new Error(`${flag} requires a value`);
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
function parseArgs(argv) {
|
|
19
|
+
const command = argv[0];
|
|
20
|
+
if (command !== 'sync' && command !== 'verify')
|
|
21
|
+
throw new Error(usage());
|
|
22
|
+
let migrationsDir;
|
|
23
|
+
let startSequence;
|
|
24
|
+
let manifestPath;
|
|
25
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
26
|
+
const flag = argv[index];
|
|
27
|
+
if (flag === '--dir') {
|
|
28
|
+
migrationsDir = requireValue(argv, index, flag);
|
|
29
|
+
index += 1;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (flag === '--start') {
|
|
33
|
+
startSequence = Number(requireValue(argv, index, flag));
|
|
34
|
+
index += 1;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (flag === '--manifest') {
|
|
38
|
+
manifestPath = requireValue(argv, index, flag);
|
|
39
|
+
index += 1;
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
throw new Error(`unknown argument: ${flag}\n\n${usage()}`);
|
|
43
|
+
}
|
|
44
|
+
if (!migrationsDir)
|
|
45
|
+
throw new Error(`--dir is required\n\n${usage()}`);
|
|
46
|
+
if (command === 'sync' && startSequence === undefined) {
|
|
47
|
+
throw new Error(`--start is required for sync\n\n${usage()}`);
|
|
48
|
+
}
|
|
49
|
+
if (command === 'verify' && startSequence !== undefined) {
|
|
50
|
+
throw new Error('--start is read from the committed manifest during verify');
|
|
51
|
+
}
|
|
52
|
+
return { command, migrationsDir, startSequence, manifestPath };
|
|
53
|
+
}
|
|
54
|
+
async function main() {
|
|
55
|
+
const args = parseArgs(process.argv.slice(2));
|
|
56
|
+
const result = args.command === 'sync'
|
|
57
|
+
? await syncCmsMigrations({
|
|
58
|
+
migrationsDir: args.migrationsDir,
|
|
59
|
+
startSequence: args.startSequence,
|
|
60
|
+
manifestPath: args.manifestPath,
|
|
61
|
+
})
|
|
62
|
+
: await verifyCmsMigrations({
|
|
63
|
+
migrationsDir: args.migrationsDir,
|
|
64
|
+
manifestPath: args.manifestPath,
|
|
65
|
+
});
|
|
66
|
+
process.stdout.write(`${JSON.stringify({ ok: true, command: args.command, ...result })}\n`);
|
|
67
|
+
}
|
|
68
|
+
main().catch((error) => {
|
|
69
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
+
process.stderr.write(`growth-labs-cms-migrations: ${message}\n`);
|
|
71
|
+
process.exitCode = 1;
|
|
72
|
+
});
|
|
73
|
+
//# sourceMappingURL=migrations.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/cli/migrations.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface StagedMigrationVendorFile {
|
|
2
|
+
tempPath: string;
|
|
3
|
+
targetPath: string;
|
|
4
|
+
}
|
|
5
|
+
export interface MigrationVendorFileOperations {
|
|
6
|
+
link(source: string, target: string): Promise<void>;
|
|
7
|
+
unlink(path: string): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
export declare function rollbackNewFiles(paths: string[], operations?: MigrationVendorFileOperations): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Link every pre-staged file without replacing an existing directory entry.
|
|
12
|
+
* A later failure removes all targets linked by this attempt and every temp.
|
|
13
|
+
*/
|
|
14
|
+
export declare function commitNewFilesAtomically(files: StagedMigrationVendorFile[], operations?: MigrationVendorFileOperations): Promise<string[]>;
|
|
15
|
+
/** Commit staged SQL, then run the receipt/final verification boundary. */
|
|
16
|
+
export declare function commitStagedMigrationBatch(files: StagedMigrationVendorFile[], finalize: () => Promise<void>, operations?: MigrationVendorFileOperations): Promise<string[]>;
|
|
17
|
+
//# sourceMappingURL=migration-vendor-files.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-vendor-files.d.ts","sourceRoot":"","sources":["../src/migration-vendor-files.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,yBAAyB;IACzC,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;CAClB;AAED,MAAM,WAAW,6BAA6B;IAC7C,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACnD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACnC;AAmBD,wBAAsB,gBAAgB,CACrC,KAAK,EAAE,MAAM,EAAE,EACf,UAAU,GAAE,6BAAkD,GAC5D,OAAO,CAAC,IAAI,CAAC,CAKf;AAED;;;GAGG;AACH,wBAAsB,wBAAwB,CAC7C,KAAK,EAAE,yBAAyB,EAAE,EAClC,UAAU,GAAE,6BAAkD,GAC5D,OAAO,CAAC,MAAM,EAAE,CAAC,CA8BnB;AAED,2EAA2E;AAC3E,wBAAsB,0BAA0B,CAC/C,KAAK,EAAE,yBAAyB,EAAE,EAClC,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,EAC7B,UAAU,GAAE,6BAAkD,GAC5D,OAAO,CAAC,MAAM,EAAE,CAAC,CAgBnB"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { link, unlink } from 'node:fs/promises';
|
|
2
|
+
const DEFAULT_OPERATIONS = { link, unlink };
|
|
3
|
+
async function removePaths(paths, operations) {
|
|
4
|
+
const errors = [];
|
|
5
|
+
for (const path of [...paths].reverse()) {
|
|
6
|
+
try {
|
|
7
|
+
await operations.unlink(path);
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (error.code !== 'ENOENT')
|
|
11
|
+
errors.push(error);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return errors;
|
|
15
|
+
}
|
|
16
|
+
export async function rollbackNewFiles(paths, operations = DEFAULT_OPERATIONS) {
|
|
17
|
+
const errors = await removePaths(paths, operations);
|
|
18
|
+
if (errors.length > 0) {
|
|
19
|
+
throw new AggregateError(errors, 'failed to roll back newly linked CMS migration files');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Link every pre-staged file without replacing an existing directory entry.
|
|
24
|
+
* A later failure removes all targets linked by this attempt and every temp.
|
|
25
|
+
*/
|
|
26
|
+
export async function commitNewFilesAtomically(files, operations = DEFAULT_OPERATIONS) {
|
|
27
|
+
const committed = [];
|
|
28
|
+
let failure;
|
|
29
|
+
try {
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
await operations.link(file.tempPath, file.targetPath);
|
|
32
|
+
committed.push(file.targetPath);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
failure = error;
|
|
37
|
+
}
|
|
38
|
+
const cleanupErrors = await removePaths(files.map((file) => file.tempPath), operations);
|
|
39
|
+
if (failure) {
|
|
40
|
+
const rollbackErrors = await removePaths(committed, operations);
|
|
41
|
+
const errors = [failure, ...cleanupErrors, ...rollbackErrors].filter((error) => error !== undefined);
|
|
42
|
+
const primary = failure instanceof Error ? `: ${failure.message}` : '';
|
|
43
|
+
throw new AggregateError(errors, `CMS migration file commit failed${primary}`);
|
|
44
|
+
}
|
|
45
|
+
// A hard-linked target remains valid when unlinking only its staged temp
|
|
46
|
+
// fails. The caller's outer finally retries temp cleanup; rolling targets
|
|
47
|
+
// back here would turn a harmless leftover temp into data loss.
|
|
48
|
+
return committed;
|
|
49
|
+
}
|
|
50
|
+
/** Commit staged SQL, then run the receipt/final verification boundary. */
|
|
51
|
+
export async function commitStagedMigrationBatch(files, finalize, operations = DEFAULT_OPERATIONS) {
|
|
52
|
+
const committed = await commitNewFilesAtomically(files, operations);
|
|
53
|
+
try {
|
|
54
|
+
await finalize();
|
|
55
|
+
return committed;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
try {
|
|
59
|
+
await rollbackNewFiles(committed, operations);
|
|
60
|
+
}
|
|
61
|
+
catch (rollbackError) {
|
|
62
|
+
throw new AggregateError([error, rollbackError], 'CMS migration batch finalization failed and rollback was incomplete');
|
|
63
|
+
}
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=migration-vendor-files.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-vendor-files.js","sourceRoot":"","sources":["../src/migration-vendor-files.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
declare const PACKAGE_NAME = "@growth-labs/cms";
|
|
2
|
+
export interface SyncCmsMigrationsOptions {
|
|
3
|
+
/** Existing consumer-owned Wrangler migrations directory. */
|
|
4
|
+
migrationsDir: string;
|
|
5
|
+
/** First unused sequence number reserved for the package migrations. */
|
|
6
|
+
startSequence: number;
|
|
7
|
+
/** Override only when a consumer keeps the receipt outside migrationsDir. */
|
|
8
|
+
manifestPath?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface VerifyCmsMigrationsOptions {
|
|
11
|
+
migrationsDir: string;
|
|
12
|
+
manifestPath?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface CmsMigrationVendorResult {
|
|
15
|
+
packageName: typeof PACKAGE_NAME;
|
|
16
|
+
packageVersion: string;
|
|
17
|
+
startSequence: number;
|
|
18
|
+
managedFiles: string[];
|
|
19
|
+
written: number;
|
|
20
|
+
verified: number;
|
|
21
|
+
manifestPath: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function syncCmsMigrations(options: SyncCmsMigrationsOptions): Promise<CmsMigrationVendorResult>;
|
|
24
|
+
export declare function verifyCmsMigrations(options: VerifyCmsMigrationsOptions): Promise<CmsMigrationVendorResult>;
|
|
25
|
+
export {};
|
|
26
|
+
//# sourceMappingURL=migration-vendor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-vendor.d.ts","sourceRoot":"","sources":["../src/migration-vendor.ts"],"names":[],"mappings":"AAiBA,QAAA,MAAM,YAAY,qBAAqB,CAAA;AAgCvC,MAAM,WAAW,wBAAwB;IACxC,6DAA6D;IAC7D,aAAa,EAAE,MAAM,CAAA;IACrB,wEAAwE;IACxE,aAAa,EAAE,MAAM,CAAA;IACrB,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,0BAA0B;IAC1C,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,wBAAwB;IACxC,WAAW,EAAE,OAAO,YAAY,CAAA;IAChC,cAAc,EAAE,MAAM,CAAA;IACtB,aAAa,EAAE,MAAM,CAAA;IACrB,YAAY,EAAE,MAAM,EAAE,CAAA;IACtB,OAAO,EAAE,MAAM,CAAA;IACf,QAAQ,EAAE,MAAM,CAAA;IAChB,YAAY,EAAE,MAAM,CAAA;CACpB;AAgSD,wBAAsB,iBAAiB,CACtC,OAAO,EAAE,wBAAwB,GAC/B,OAAO,CAAC,wBAAwB,CAAC,CAiDnC;AAED,wBAAsB,mBAAmB,CACxC,OAAO,EAAE,0BAA0B,GACjC,OAAO,CAAC,wBAAwB,CAAC,CAmCnC"}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { link, lstat, readdir, readFile, realpath, rename, unlink, writeFile, } from 'node:fs/promises';
|
|
2
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { commitStagedMigrationBatch, } from './migration-vendor-files.js';
|
|
5
|
+
const PACKAGE_NAME = '@growth-labs/cms';
|
|
6
|
+
const MANIFEST_NAME = '.growth-labs-cms-migrations.json';
|
|
7
|
+
const MANIFEST_SCHEMA_VERSION = 1;
|
|
8
|
+
const MIGRATION_FILE = /^\d{4}_[a-z0-9][a-z0-9_-]*\.sql$/;
|
|
9
|
+
async function packageContract() {
|
|
10
|
+
const packageRoot = fileURLToPath(new URL('..', import.meta.url));
|
|
11
|
+
const packageJsonPath = join(packageRoot, 'package.json');
|
|
12
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8'));
|
|
13
|
+
if (packageJson.name !== PACKAGE_NAME || !packageJson.version) {
|
|
14
|
+
throw new Error('installed CMS package metadata is invalid');
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
packageVersion: packageJson.version,
|
|
18
|
+
sourceDir: join(packageRoot, 'migrations'),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async function sha256(bytes) {
|
|
22
|
+
const input = new Uint8Array(bytes.byteLength);
|
|
23
|
+
input.set(bytes);
|
|
24
|
+
const digest = await crypto.subtle.digest('SHA-256', input.buffer);
|
|
25
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
26
|
+
}
|
|
27
|
+
async function requireExistingDirectory(path, label = 'migrationsDir') {
|
|
28
|
+
let stat;
|
|
29
|
+
try {
|
|
30
|
+
stat = await lstat(path);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new Error(`${label} must be an existing directory: ${path}`);
|
|
34
|
+
}
|
|
35
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
36
|
+
throw new Error(`${label} must be an existing non-symlink directory: ${path}`);
|
|
37
|
+
}
|
|
38
|
+
return realpath(path);
|
|
39
|
+
}
|
|
40
|
+
function assertStartSequence(value) {
|
|
41
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
42
|
+
throw new Error('startSequence must be a positive integer');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function sequencePrefix(sequence) {
|
|
46
|
+
return String(sequence).padStart(4, '0');
|
|
47
|
+
}
|
|
48
|
+
async function planMigrations(sourceDir, startSequence) {
|
|
49
|
+
assertStartSequence(startSequence);
|
|
50
|
+
const sourceNames = (await readdir(sourceDir)).filter((name) => MIGRATION_FILE.test(name)).sort();
|
|
51
|
+
if (sourceNames.length === 0)
|
|
52
|
+
throw new Error('installed CMS package has no migration SQL');
|
|
53
|
+
if (startSequence + sourceNames.length - 1 > 9999) {
|
|
54
|
+
throw new Error('reserved CMS migration sequence exceeds 9999');
|
|
55
|
+
}
|
|
56
|
+
return Promise.all(sourceNames.map(async (source, index) => {
|
|
57
|
+
const sequence = startSequence + index;
|
|
58
|
+
const suffix = source.replace(/^\d{4}_/, '');
|
|
59
|
+
const target = `${sequencePrefix(sequence)}_growth_labs_cms_${suffix}`;
|
|
60
|
+
const bytes = new Uint8Array(await readFile(join(sourceDir, source)));
|
|
61
|
+
return { source, target, sequence, sha256: await sha256(bytes), bytes };
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
function manifestFor(packageVersion, startSequence, plan) {
|
|
65
|
+
return {
|
|
66
|
+
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
67
|
+
packageName: PACKAGE_NAME,
|
|
68
|
+
packageVersion,
|
|
69
|
+
startSequence,
|
|
70
|
+
files: plan.map(({ source, target, sha256: hash }) => ({ source, target, sha256: hash })),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
async function readManifest(path) {
|
|
74
|
+
try {
|
|
75
|
+
const stat = await lstat(path);
|
|
76
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
77
|
+
throw new Error(`CMS migration manifest must be a regular file: ${path}`);
|
|
78
|
+
}
|
|
79
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
80
|
+
if (parsed.schemaVersion !== MANIFEST_SCHEMA_VERSION ||
|
|
81
|
+
parsed.packageName !== PACKAGE_NAME ||
|
|
82
|
+
!parsed.packageVersion ||
|
|
83
|
+
!Number.isSafeInteger(parsed.startSequence) ||
|
|
84
|
+
parsed.startSequence < 1 ||
|
|
85
|
+
!Array.isArray(parsed.files)) {
|
|
86
|
+
throw new Error(`CMS migration manifest is invalid: ${path}`);
|
|
87
|
+
}
|
|
88
|
+
for (const entry of parsed.files) {
|
|
89
|
+
if (!entry ||
|
|
90
|
+
typeof entry.source !== 'string' ||
|
|
91
|
+
typeof entry.target !== 'string' ||
|
|
92
|
+
!MIGRATION_FILE.test(entry.source) ||
|
|
93
|
+
!MIGRATION_FILE.test(entry.target) ||
|
|
94
|
+
!/^([a-f0-9]{64})$/.test(entry.sha256)) {
|
|
95
|
+
throw new Error(`CMS migration manifest contains an invalid file entry: ${path}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return parsed;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error.code === 'ENOENT')
|
|
102
|
+
return null;
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function entriesEqual(left, right) {
|
|
107
|
+
return (left.source === right.source && left.target === right.target && left.sha256 === right.sha256);
|
|
108
|
+
}
|
|
109
|
+
function assertManifestCanAdvance(current, next) {
|
|
110
|
+
if (!current)
|
|
111
|
+
return;
|
|
112
|
+
if (current.startSequence !== next.startSequence) {
|
|
113
|
+
throw new Error(`CMS migration manifest reserves startSequence ${current.startSequence}; received ${next.startSequence}`);
|
|
114
|
+
}
|
|
115
|
+
if (current.files.length > next.files.length) {
|
|
116
|
+
throw new Error('installed CMS package removed migrations recorded by the consumer manifest');
|
|
117
|
+
}
|
|
118
|
+
for (const [index, entry] of current.files.entries()) {
|
|
119
|
+
const nextEntry = next.files[index];
|
|
120
|
+
if (!nextEntry || !entriesEqual(entry, nextEntry)) {
|
|
121
|
+
throw new Error(`installed CMS migration changed after vendoring: ${entry.source}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function assertNoSequenceCollisions(migrationsDir, plan) {
|
|
126
|
+
const existing = await readdir(migrationsDir);
|
|
127
|
+
for (const migration of plan) {
|
|
128
|
+
const prefix = `${sequencePrefix(migration.sequence)}_`;
|
|
129
|
+
const collisions = existing.filter((name) => name.endsWith('.sql') && name.startsWith(prefix) && name !== migration.target);
|
|
130
|
+
if (collisions.length > 0) {
|
|
131
|
+
throw new Error(`reserved sequence ${sequencePrefix(migration.sequence)} collides with ${collisions.join(', ')}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function verifyTargets(migrationsDir, plan, allowMissing) {
|
|
136
|
+
let verified = 0;
|
|
137
|
+
const missing = [];
|
|
138
|
+
for (const migration of plan) {
|
|
139
|
+
const targetPath = join(migrationsDir, migration.target);
|
|
140
|
+
try {
|
|
141
|
+
const stat = await lstat(targetPath);
|
|
142
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
143
|
+
throw new Error(`managed CMS migration must be a regular file: ${migration.target}`);
|
|
144
|
+
}
|
|
145
|
+
const targetHash = await sha256(new Uint8Array(await readFile(targetPath)));
|
|
146
|
+
if (targetHash !== migration.sha256) {
|
|
147
|
+
throw new Error(`CMS migration hash mismatch: ${migration.target}`);
|
|
148
|
+
}
|
|
149
|
+
verified += 1;
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (error.code === 'ENOENT' && allowMissing) {
|
|
153
|
+
missing.push(migration);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { verified, missing };
|
|
160
|
+
}
|
|
161
|
+
async function stageNewFile(path, bytes) {
|
|
162
|
+
const temp = `${path}.tmp-${crypto.randomUUID()}`;
|
|
163
|
+
await writeFile(temp, bytes, { flag: 'wx' });
|
|
164
|
+
return temp;
|
|
165
|
+
}
|
|
166
|
+
async function stageManifest(path, manifest) {
|
|
167
|
+
const temp = `${path}.tmp-${crypto.randomUUID()}`;
|
|
168
|
+
await writeFile(temp, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' });
|
|
169
|
+
return temp;
|
|
170
|
+
}
|
|
171
|
+
async function commitManifest(tempPath, manifestPath, currentManifest) {
|
|
172
|
+
if (currentManifest) {
|
|
173
|
+
const latest = await readManifest(manifestPath);
|
|
174
|
+
if (!latest || JSON.stringify(latest) !== JSON.stringify(currentManifest)) {
|
|
175
|
+
throw new Error('CMS migration manifest changed during sync');
|
|
176
|
+
}
|
|
177
|
+
await rename(tempPath, manifestPath);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
await link(tempPath, manifestPath);
|
|
181
|
+
}
|
|
182
|
+
function isInsideDirectory(directory, path) {
|
|
183
|
+
const fromDirectory = relative(directory, path);
|
|
184
|
+
return (fromDirectory === '' ||
|
|
185
|
+
(!fromDirectory.startsWith(`..${sep}`) && fromDirectory !== '..' && !isAbsolute(fromDirectory)));
|
|
186
|
+
}
|
|
187
|
+
async function resolveManifestPath(migrationsDir, override) {
|
|
188
|
+
if (!override)
|
|
189
|
+
return join(migrationsDir, MANIFEST_NAME);
|
|
190
|
+
const requested = resolve(override);
|
|
191
|
+
if (extname(requested).toLowerCase() !== '.json') {
|
|
192
|
+
throw new Error('CMS migration manifest override must be a .json file');
|
|
193
|
+
}
|
|
194
|
+
if (isInsideDirectory(migrationsDir, requested)) {
|
|
195
|
+
throw new Error('CMS migration manifest override must be outside migrationsDir');
|
|
196
|
+
}
|
|
197
|
+
const parent = await requireExistingDirectory(dirname(requested), 'manifest parent');
|
|
198
|
+
const resolvedPath = join(parent, basename(requested));
|
|
199
|
+
if (isInsideDirectory(migrationsDir, resolvedPath)) {
|
|
200
|
+
throw new Error('CMS migration manifest override must be outside migrationsDir');
|
|
201
|
+
}
|
|
202
|
+
return resolvedPath;
|
|
203
|
+
}
|
|
204
|
+
async function withManifestLock(manifestPath, action) {
|
|
205
|
+
const lockPath = `${manifestPath}.lock`;
|
|
206
|
+
try {
|
|
207
|
+
await writeFile(lockPath, `${JSON.stringify({ pid: process.pid, startedAt: Date.now() })}\n`, {
|
|
208
|
+
flag: 'wx',
|
|
209
|
+
mode: 0o600,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
if (error.code === 'EEXIST') {
|
|
214
|
+
throw new Error(`another CMS migration operation is already active: ${lockPath}`);
|
|
215
|
+
}
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
let result;
|
|
219
|
+
let operationError;
|
|
220
|
+
try {
|
|
221
|
+
result = await action();
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
operationError = error;
|
|
225
|
+
}
|
|
226
|
+
let releaseError;
|
|
227
|
+
try {
|
|
228
|
+
await unlink(lockPath);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
releaseError = error;
|
|
232
|
+
}
|
|
233
|
+
if (operationError && releaseError) {
|
|
234
|
+
throw new AggregateError([operationError, releaseError], 'CMS migration operation failed and its receipt lock could not be released');
|
|
235
|
+
}
|
|
236
|
+
if (operationError)
|
|
237
|
+
throw operationError;
|
|
238
|
+
if (releaseError)
|
|
239
|
+
throw releaseError;
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
export async function syncCmsMigrations(options) {
|
|
243
|
+
assertStartSequence(options.startSequence);
|
|
244
|
+
const migrationsDir = await requireExistingDirectory(resolve(options.migrationsDir));
|
|
245
|
+
const manifestPath = await resolveManifestPath(migrationsDir, options.manifestPath);
|
|
246
|
+
return withManifestLock(manifestPath, async () => {
|
|
247
|
+
const contract = await packageContract();
|
|
248
|
+
const plan = await planMigrations(contract.sourceDir, options.startSequence);
|
|
249
|
+
const nextManifest = manifestFor(contract.packageVersion, options.startSequence, plan);
|
|
250
|
+
const currentManifest = await readManifest(manifestPath);
|
|
251
|
+
assertManifestCanAdvance(currentManifest, nextManifest);
|
|
252
|
+
await assertNoSequenceCollisions(migrationsDir, plan);
|
|
253
|
+
const preflight = await verifyTargets(migrationsDir, plan, true);
|
|
254
|
+
const staged = [];
|
|
255
|
+
let manifestTemp = null;
|
|
256
|
+
let verified = 0;
|
|
257
|
+
try {
|
|
258
|
+
for (const migration of preflight.missing) {
|
|
259
|
+
const targetPath = join(migrationsDir, migration.target);
|
|
260
|
+
staged.push({
|
|
261
|
+
targetPath,
|
|
262
|
+
tempPath: await stageNewFile(targetPath, migration.bytes),
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const stagedManifest = await stageManifest(manifestPath, nextManifest);
|
|
266
|
+
manifestTemp = stagedManifest;
|
|
267
|
+
await commitStagedMigrationBatch(staged, async () => {
|
|
268
|
+
await assertNoSequenceCollisions(migrationsDir, plan);
|
|
269
|
+
const final = await verifyTargets(migrationsDir, plan, false);
|
|
270
|
+
verified = final.verified;
|
|
271
|
+
await commitManifest(stagedManifest, manifestPath, currentManifest);
|
|
272
|
+
});
|
|
273
|
+
return {
|
|
274
|
+
packageName: PACKAGE_NAME,
|
|
275
|
+
packageVersion: contract.packageVersion,
|
|
276
|
+
startSequence: options.startSequence,
|
|
277
|
+
managedFiles: plan.map(({ target }) => target),
|
|
278
|
+
written: preflight.missing.length,
|
|
279
|
+
verified,
|
|
280
|
+
manifestPath,
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
finally {
|
|
284
|
+
await Promise.all([
|
|
285
|
+
...staged.map(({ tempPath }) => unlink(tempPath).catch(() => { })),
|
|
286
|
+
...(manifestTemp ? [unlink(manifestTemp).catch(() => { })] : []),
|
|
287
|
+
]);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
export async function verifyCmsMigrations(options) {
|
|
292
|
+
const migrationsDir = await requireExistingDirectory(resolve(options.migrationsDir));
|
|
293
|
+
const manifestPath = await resolveManifestPath(migrationsDir, options.manifestPath);
|
|
294
|
+
return withManifestLock(manifestPath, async () => {
|
|
295
|
+
const manifest = await readManifest(manifestPath);
|
|
296
|
+
if (!manifest)
|
|
297
|
+
throw new Error(`CMS migration manifest is missing: ${manifestPath}`);
|
|
298
|
+
const contract = await packageContract();
|
|
299
|
+
if (manifest.packageVersion !== contract.packageVersion) {
|
|
300
|
+
throw new Error(`CMS migration manifest version ${manifest.packageVersion} does not match installed ${contract.packageVersion}`);
|
|
301
|
+
}
|
|
302
|
+
const plan = await planMigrations(contract.sourceDir, manifest.startSequence);
|
|
303
|
+
const expected = manifestFor(contract.packageVersion, manifest.startSequence, plan);
|
|
304
|
+
if (expected.files.length !== manifest.files.length ||
|
|
305
|
+
expected.files.some((entry, index) => !entriesEqual(entry, manifest.files[index]))) {
|
|
306
|
+
throw new Error('CMS migration manifest does not match the installed package contract');
|
|
307
|
+
}
|
|
308
|
+
await assertNoSequenceCollisions(migrationsDir, plan);
|
|
309
|
+
const result = await verifyTargets(migrationsDir, plan, false);
|
|
310
|
+
return {
|
|
311
|
+
packageName: PACKAGE_NAME,
|
|
312
|
+
packageVersion: contract.packageVersion,
|
|
313
|
+
startSequence: manifest.startSequence,
|
|
314
|
+
managedFiles: plan.map(({ target }) => target),
|
|
315
|
+
written: 0,
|
|
316
|
+
verified: result.verified,
|
|
317
|
+
manifestPath,
|
|
318
|
+
};
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
//# sourceMappingURL=migration-vendor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"migration-vendor.js","sourceRoot":"","sources":["../src/migration-vendor.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@growth-labs/cms",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"growth-labs-cms-migrations": "./dist/cli/migrations.js"
|
|
7
|
+
},
|
|
5
8
|
"types": "./dist/index.d.ts",
|
|
6
9
|
"exports": {
|
|
7
10
|
".": {
|
|
@@ -24,6 +27,10 @@
|
|
|
24
27
|
"types": "./dist/integration/index.d.ts",
|
|
25
28
|
"import": "./dist/integration/index.js"
|
|
26
29
|
},
|
|
30
|
+
"./migration-vendor": {
|
|
31
|
+
"types": "./dist/migration-vendor.d.ts",
|
|
32
|
+
"import": "./dist/migration-vendor.js"
|
|
33
|
+
},
|
|
27
34
|
"./ui": {
|
|
28
35
|
"types": "./dist/ui/components/CmsApp.d.ts",
|
|
29
36
|
"import": "./dist/ui/components/CmsApp.js"
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { syncCmsMigrations, verifyCmsMigrations } from '../migration-vendor.js'
|
|
4
|
+
|
|
5
|
+
interface ParsedArgs {
|
|
6
|
+
command: 'sync' | 'verify'
|
|
7
|
+
migrationsDir: string
|
|
8
|
+
startSequence?: number
|
|
9
|
+
manifestPath?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function usage(): string {
|
|
13
|
+
return [
|
|
14
|
+
'Usage:',
|
|
15
|
+
' growth-labs-cms-migrations sync --dir <migrations-dir> --start <positive-integer>',
|
|
16
|
+
' growth-labs-cms-migrations verify --dir <migrations-dir>',
|
|
17
|
+
'',
|
|
18
|
+
'Optional for both commands: --manifest <receipt-path>',
|
|
19
|
+
].join('\n')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function requireValue(argv: string[], index: number, flag: string): string {
|
|
23
|
+
const value = argv[index + 1]
|
|
24
|
+
if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`)
|
|
25
|
+
return value
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parseArgs(argv: string[]): ParsedArgs {
|
|
29
|
+
const command = argv[0]
|
|
30
|
+
if (command !== 'sync' && command !== 'verify') throw new Error(usage())
|
|
31
|
+
let migrationsDir: string | undefined
|
|
32
|
+
let startSequence: number | undefined
|
|
33
|
+
let manifestPath: string | undefined
|
|
34
|
+
|
|
35
|
+
for (let index = 1; index < argv.length; index += 1) {
|
|
36
|
+
const flag = argv[index]
|
|
37
|
+
if (flag === '--dir') {
|
|
38
|
+
migrationsDir = requireValue(argv, index, flag)
|
|
39
|
+
index += 1
|
|
40
|
+
continue
|
|
41
|
+
}
|
|
42
|
+
if (flag === '--start') {
|
|
43
|
+
startSequence = Number(requireValue(argv, index, flag))
|
|
44
|
+
index += 1
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
if (flag === '--manifest') {
|
|
48
|
+
manifestPath = requireValue(argv, index, flag)
|
|
49
|
+
index += 1
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`unknown argument: ${flag}\n\n${usage()}`)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!migrationsDir) throw new Error(`--dir is required\n\n${usage()}`)
|
|
56
|
+
if (command === 'sync' && startSequence === undefined) {
|
|
57
|
+
throw new Error(`--start is required for sync\n\n${usage()}`)
|
|
58
|
+
}
|
|
59
|
+
if (command === 'verify' && startSequence !== undefined) {
|
|
60
|
+
throw new Error('--start is read from the committed manifest during verify')
|
|
61
|
+
}
|
|
62
|
+
return { command, migrationsDir, startSequence, manifestPath }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function main(): Promise<void> {
|
|
66
|
+
const args = parseArgs(process.argv.slice(2))
|
|
67
|
+
const result =
|
|
68
|
+
args.command === 'sync'
|
|
69
|
+
? await syncCmsMigrations({
|
|
70
|
+
migrationsDir: args.migrationsDir,
|
|
71
|
+
startSequence: args.startSequence as number,
|
|
72
|
+
manifestPath: args.manifestPath,
|
|
73
|
+
})
|
|
74
|
+
: await verifyCmsMigrations({
|
|
75
|
+
migrationsDir: args.migrationsDir,
|
|
76
|
+
manifestPath: args.manifestPath,
|
|
77
|
+
})
|
|
78
|
+
process.stdout.write(`${JSON.stringify({ ok: true, command: args.command, ...result })}\n`)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
main().catch((error: unknown) => {
|
|
82
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
83
|
+
process.stderr.write(`growth-labs-cms-migrations: ${message}\n`)
|
|
84
|
+
process.exitCode = 1
|
|
85
|
+
})
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { link, unlink } from 'node:fs/promises'
|
|
2
|
+
|
|
3
|
+
export interface StagedMigrationVendorFile {
|
|
4
|
+
tempPath: string
|
|
5
|
+
targetPath: string
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface MigrationVendorFileOperations {
|
|
9
|
+
link(source: string, target: string): Promise<void>
|
|
10
|
+
unlink(path: string): Promise<void>
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const DEFAULT_OPERATIONS: MigrationVendorFileOperations = { link, unlink }
|
|
14
|
+
|
|
15
|
+
async function removePaths(
|
|
16
|
+
paths: string[],
|
|
17
|
+
operations: MigrationVendorFileOperations,
|
|
18
|
+
): Promise<unknown[]> {
|
|
19
|
+
const errors: unknown[] = []
|
|
20
|
+
for (const path of [...paths].reverse()) {
|
|
21
|
+
try {
|
|
22
|
+
await operations.unlink(path)
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') errors.push(error)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return errors
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function rollbackNewFiles(
|
|
31
|
+
paths: string[],
|
|
32
|
+
operations: MigrationVendorFileOperations = DEFAULT_OPERATIONS,
|
|
33
|
+
): Promise<void> {
|
|
34
|
+
const errors = await removePaths(paths, operations)
|
|
35
|
+
if (errors.length > 0) {
|
|
36
|
+
throw new AggregateError(errors, 'failed to roll back newly linked CMS migration files')
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Link every pre-staged file without replacing an existing directory entry.
|
|
42
|
+
* A later failure removes all targets linked by this attempt and every temp.
|
|
43
|
+
*/
|
|
44
|
+
export async function commitNewFilesAtomically(
|
|
45
|
+
files: StagedMigrationVendorFile[],
|
|
46
|
+
operations: MigrationVendorFileOperations = DEFAULT_OPERATIONS,
|
|
47
|
+
): Promise<string[]> {
|
|
48
|
+
const committed: string[] = []
|
|
49
|
+
let failure: unknown
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
for (const file of files) {
|
|
53
|
+
await operations.link(file.tempPath, file.targetPath)
|
|
54
|
+
committed.push(file.targetPath)
|
|
55
|
+
}
|
|
56
|
+
} catch (error) {
|
|
57
|
+
failure = error
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const cleanupErrors = await removePaths(
|
|
61
|
+
files.map((file) => file.tempPath),
|
|
62
|
+
operations,
|
|
63
|
+
)
|
|
64
|
+
if (failure) {
|
|
65
|
+
const rollbackErrors = await removePaths(committed, operations)
|
|
66
|
+
const errors = [failure, ...cleanupErrors, ...rollbackErrors].filter(
|
|
67
|
+
(error): error is NonNullable<unknown> => error !== undefined,
|
|
68
|
+
)
|
|
69
|
+
const primary = failure instanceof Error ? `: ${failure.message}` : ''
|
|
70
|
+
throw new AggregateError(errors, `CMS migration file commit failed${primary}`)
|
|
71
|
+
}
|
|
72
|
+
// A hard-linked target remains valid when unlinking only its staged temp
|
|
73
|
+
// fails. The caller's outer finally retries temp cleanup; rolling targets
|
|
74
|
+
// back here would turn a harmless leftover temp into data loss.
|
|
75
|
+
|
|
76
|
+
return committed
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Commit staged SQL, then run the receipt/final verification boundary. */
|
|
80
|
+
export async function commitStagedMigrationBatch(
|
|
81
|
+
files: StagedMigrationVendorFile[],
|
|
82
|
+
finalize: () => Promise<void>,
|
|
83
|
+
operations: MigrationVendorFileOperations = DEFAULT_OPERATIONS,
|
|
84
|
+
): Promise<string[]> {
|
|
85
|
+
const committed = await commitNewFilesAtomically(files, operations)
|
|
86
|
+
try {
|
|
87
|
+
await finalize()
|
|
88
|
+
return committed
|
|
89
|
+
} catch (error) {
|
|
90
|
+
try {
|
|
91
|
+
await rollbackNewFiles(committed, operations)
|
|
92
|
+
} catch (rollbackError) {
|
|
93
|
+
throw new AggregateError(
|
|
94
|
+
[error, rollbackError],
|
|
95
|
+
'CMS migration batch finalization failed and rollback was incomplete',
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
throw error
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import {
|
|
2
|
+
link,
|
|
3
|
+
lstat,
|
|
4
|
+
readdir,
|
|
5
|
+
readFile,
|
|
6
|
+
realpath,
|
|
7
|
+
rename,
|
|
8
|
+
unlink,
|
|
9
|
+
writeFile,
|
|
10
|
+
} from 'node:fs/promises'
|
|
11
|
+
import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
import {
|
|
14
|
+
commitStagedMigrationBatch,
|
|
15
|
+
type StagedMigrationVendorFile,
|
|
16
|
+
} from './migration-vendor-files.js'
|
|
17
|
+
|
|
18
|
+
const PACKAGE_NAME = '@growth-labs/cms'
|
|
19
|
+
const MANIFEST_NAME = '.growth-labs-cms-migrations.json'
|
|
20
|
+
const MANIFEST_SCHEMA_VERSION = 1
|
|
21
|
+
const MIGRATION_FILE = /^\d{4}_[a-z0-9][a-z0-9_-]*\.sql$/
|
|
22
|
+
|
|
23
|
+
interface PackageJson {
|
|
24
|
+
name?: string
|
|
25
|
+
version?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface PlannedMigration {
|
|
29
|
+
source: string
|
|
30
|
+
target: string
|
|
31
|
+
sequence: number
|
|
32
|
+
sha256: string
|
|
33
|
+
bytes: Uint8Array
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface CmsMigrationManifestEntry {
|
|
37
|
+
source: string
|
|
38
|
+
target: string
|
|
39
|
+
sha256: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface CmsMigrationManifest {
|
|
43
|
+
schemaVersion: 1
|
|
44
|
+
packageName: typeof PACKAGE_NAME
|
|
45
|
+
packageVersion: string
|
|
46
|
+
startSequence: number
|
|
47
|
+
files: CmsMigrationManifestEntry[]
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface SyncCmsMigrationsOptions {
|
|
51
|
+
/** Existing consumer-owned Wrangler migrations directory. */
|
|
52
|
+
migrationsDir: string
|
|
53
|
+
/** First unused sequence number reserved for the package migrations. */
|
|
54
|
+
startSequence: number
|
|
55
|
+
/** Override only when a consumer keeps the receipt outside migrationsDir. */
|
|
56
|
+
manifestPath?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface VerifyCmsMigrationsOptions {
|
|
60
|
+
migrationsDir: string
|
|
61
|
+
manifestPath?: string
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface CmsMigrationVendorResult {
|
|
65
|
+
packageName: typeof PACKAGE_NAME
|
|
66
|
+
packageVersion: string
|
|
67
|
+
startSequence: number
|
|
68
|
+
managedFiles: string[]
|
|
69
|
+
written: number
|
|
70
|
+
verified: number
|
|
71
|
+
manifestPath: string
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface PackageContract {
|
|
75
|
+
packageVersion: string
|
|
76
|
+
sourceDir: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function packageContract(): Promise<PackageContract> {
|
|
80
|
+
const packageRoot = fileURLToPath(new URL('..', import.meta.url))
|
|
81
|
+
const packageJsonPath = join(packageRoot, 'package.json')
|
|
82
|
+
const packageJson = JSON.parse(await readFile(packageJsonPath, 'utf8')) as PackageJson
|
|
83
|
+
if (packageJson.name !== PACKAGE_NAME || !packageJson.version) {
|
|
84
|
+
throw new Error('installed CMS package metadata is invalid')
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
packageVersion: packageJson.version,
|
|
88
|
+
sourceDir: join(packageRoot, 'migrations'),
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function sha256(bytes: Uint8Array): Promise<string> {
|
|
93
|
+
const input = new Uint8Array(bytes.byteLength)
|
|
94
|
+
input.set(bytes)
|
|
95
|
+
const digest = await crypto.subtle.digest('SHA-256', input.buffer)
|
|
96
|
+
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function requireExistingDirectory(path: string, label = 'migrationsDir'): Promise<string> {
|
|
100
|
+
let stat: Awaited<ReturnType<typeof lstat>>
|
|
101
|
+
try {
|
|
102
|
+
stat = await lstat(path)
|
|
103
|
+
} catch {
|
|
104
|
+
throw new Error(`${label} must be an existing directory: ${path}`)
|
|
105
|
+
}
|
|
106
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
107
|
+
throw new Error(`${label} must be an existing non-symlink directory: ${path}`)
|
|
108
|
+
}
|
|
109
|
+
return realpath(path)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function assertStartSequence(value: number): void {
|
|
113
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
114
|
+
throw new Error('startSequence must be a positive integer')
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function sequencePrefix(sequence: number): string {
|
|
119
|
+
return String(sequence).padStart(4, '0')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function planMigrations(
|
|
123
|
+
sourceDir: string,
|
|
124
|
+
startSequence: number,
|
|
125
|
+
): Promise<PlannedMigration[]> {
|
|
126
|
+
assertStartSequence(startSequence)
|
|
127
|
+
const sourceNames = (await readdir(sourceDir)).filter((name) => MIGRATION_FILE.test(name)).sort()
|
|
128
|
+
if (sourceNames.length === 0) throw new Error('installed CMS package has no migration SQL')
|
|
129
|
+
if (startSequence + sourceNames.length - 1 > 9999) {
|
|
130
|
+
throw new Error('reserved CMS migration sequence exceeds 9999')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return Promise.all(
|
|
134
|
+
sourceNames.map(async (source, index) => {
|
|
135
|
+
const sequence = startSequence + index
|
|
136
|
+
const suffix = source.replace(/^\d{4}_/, '')
|
|
137
|
+
const target = `${sequencePrefix(sequence)}_growth_labs_cms_${suffix}`
|
|
138
|
+
const bytes = new Uint8Array(await readFile(join(sourceDir, source)))
|
|
139
|
+
return { source, target, sequence, sha256: await sha256(bytes), bytes }
|
|
140
|
+
}),
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function manifestFor(
|
|
145
|
+
packageVersion: string,
|
|
146
|
+
startSequence: number,
|
|
147
|
+
plan: PlannedMigration[],
|
|
148
|
+
): CmsMigrationManifest {
|
|
149
|
+
return {
|
|
150
|
+
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
151
|
+
packageName: PACKAGE_NAME,
|
|
152
|
+
packageVersion,
|
|
153
|
+
startSequence,
|
|
154
|
+
files: plan.map(({ source, target, sha256: hash }) => ({ source, target, sha256: hash })),
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function readManifest(path: string): Promise<CmsMigrationManifest | null> {
|
|
159
|
+
try {
|
|
160
|
+
const stat = await lstat(path)
|
|
161
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
162
|
+
throw new Error(`CMS migration manifest must be a regular file: ${path}`)
|
|
163
|
+
}
|
|
164
|
+
const parsed = JSON.parse(await readFile(path, 'utf8')) as Partial<CmsMigrationManifest>
|
|
165
|
+
if (
|
|
166
|
+
parsed.schemaVersion !== MANIFEST_SCHEMA_VERSION ||
|
|
167
|
+
parsed.packageName !== PACKAGE_NAME ||
|
|
168
|
+
!parsed.packageVersion ||
|
|
169
|
+
!Number.isSafeInteger(parsed.startSequence) ||
|
|
170
|
+
(parsed.startSequence as number) < 1 ||
|
|
171
|
+
!Array.isArray(parsed.files)
|
|
172
|
+
) {
|
|
173
|
+
throw new Error(`CMS migration manifest is invalid: ${path}`)
|
|
174
|
+
}
|
|
175
|
+
for (const entry of parsed.files) {
|
|
176
|
+
if (
|
|
177
|
+
!entry ||
|
|
178
|
+
typeof entry.source !== 'string' ||
|
|
179
|
+
typeof entry.target !== 'string' ||
|
|
180
|
+
!MIGRATION_FILE.test(entry.source) ||
|
|
181
|
+
!MIGRATION_FILE.test(entry.target) ||
|
|
182
|
+
!/^([a-f0-9]{64})$/.test(entry.sha256)
|
|
183
|
+
) {
|
|
184
|
+
throw new Error(`CMS migration manifest contains an invalid file entry: ${path}`)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return parsed as CmsMigrationManifest
|
|
188
|
+
} catch (error) {
|
|
189
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null
|
|
190
|
+
throw error
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function entriesEqual(left: CmsMigrationManifestEntry, right: CmsMigrationManifestEntry): boolean {
|
|
195
|
+
return (
|
|
196
|
+
left.source === right.source && left.target === right.target && left.sha256 === right.sha256
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function assertManifestCanAdvance(
|
|
201
|
+
current: CmsMigrationManifest | null,
|
|
202
|
+
next: CmsMigrationManifest,
|
|
203
|
+
): void {
|
|
204
|
+
if (!current) return
|
|
205
|
+
if (current.startSequence !== next.startSequence) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`CMS migration manifest reserves startSequence ${current.startSequence}; received ${next.startSequence}`,
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
if (current.files.length > next.files.length) {
|
|
211
|
+
throw new Error('installed CMS package removed migrations recorded by the consumer manifest')
|
|
212
|
+
}
|
|
213
|
+
for (const [index, entry] of current.files.entries()) {
|
|
214
|
+
const nextEntry = next.files[index]
|
|
215
|
+
if (!nextEntry || !entriesEqual(entry, nextEntry)) {
|
|
216
|
+
throw new Error(`installed CMS migration changed after vendoring: ${entry.source}`)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function assertNoSequenceCollisions(
|
|
222
|
+
migrationsDir: string,
|
|
223
|
+
plan: PlannedMigration[],
|
|
224
|
+
): Promise<void> {
|
|
225
|
+
const existing = await readdir(migrationsDir)
|
|
226
|
+
for (const migration of plan) {
|
|
227
|
+
const prefix = `${sequencePrefix(migration.sequence)}_`
|
|
228
|
+
const collisions = existing.filter(
|
|
229
|
+
(name) => name.endsWith('.sql') && name.startsWith(prefix) && name !== migration.target,
|
|
230
|
+
)
|
|
231
|
+
if (collisions.length > 0) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`reserved sequence ${sequencePrefix(migration.sequence)} collides with ${collisions.join(', ')}`,
|
|
234
|
+
)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function verifyTargets(
|
|
240
|
+
migrationsDir: string,
|
|
241
|
+
plan: PlannedMigration[],
|
|
242
|
+
allowMissing: boolean,
|
|
243
|
+
): Promise<{ verified: number; missing: PlannedMigration[] }> {
|
|
244
|
+
let verified = 0
|
|
245
|
+
const missing: PlannedMigration[] = []
|
|
246
|
+
for (const migration of plan) {
|
|
247
|
+
const targetPath = join(migrationsDir, migration.target)
|
|
248
|
+
try {
|
|
249
|
+
const stat = await lstat(targetPath)
|
|
250
|
+
if (!stat.isFile() || stat.isSymbolicLink()) {
|
|
251
|
+
throw new Error(`managed CMS migration must be a regular file: ${migration.target}`)
|
|
252
|
+
}
|
|
253
|
+
const targetHash = await sha256(new Uint8Array(await readFile(targetPath)))
|
|
254
|
+
if (targetHash !== migration.sha256) {
|
|
255
|
+
throw new Error(`CMS migration hash mismatch: ${migration.target}`)
|
|
256
|
+
}
|
|
257
|
+
verified += 1
|
|
258
|
+
} catch (error) {
|
|
259
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT' && allowMissing) {
|
|
260
|
+
missing.push(migration)
|
|
261
|
+
continue
|
|
262
|
+
}
|
|
263
|
+
throw error
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return { verified, missing }
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function stageNewFile(path: string, bytes: Uint8Array): Promise<string> {
|
|
270
|
+
const temp = `${path}.tmp-${crypto.randomUUID()}`
|
|
271
|
+
await writeFile(temp, bytes, { flag: 'wx' })
|
|
272
|
+
return temp
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function stageManifest(path: string, manifest: CmsMigrationManifest): Promise<string> {
|
|
276
|
+
const temp = `${path}.tmp-${crypto.randomUUID()}`
|
|
277
|
+
await writeFile(temp, `${JSON.stringify(manifest, null, 2)}\n`, { flag: 'wx' })
|
|
278
|
+
return temp
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function commitManifest(
|
|
282
|
+
tempPath: string,
|
|
283
|
+
manifestPath: string,
|
|
284
|
+
currentManifest: CmsMigrationManifest | null,
|
|
285
|
+
): Promise<void> {
|
|
286
|
+
if (currentManifest) {
|
|
287
|
+
const latest = await readManifest(manifestPath)
|
|
288
|
+
if (!latest || JSON.stringify(latest) !== JSON.stringify(currentManifest)) {
|
|
289
|
+
throw new Error('CMS migration manifest changed during sync')
|
|
290
|
+
}
|
|
291
|
+
await rename(tempPath, manifestPath)
|
|
292
|
+
return
|
|
293
|
+
}
|
|
294
|
+
await link(tempPath, manifestPath)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function isInsideDirectory(directory: string, path: string): boolean {
|
|
298
|
+
const fromDirectory = relative(directory, path)
|
|
299
|
+
return (
|
|
300
|
+
fromDirectory === '' ||
|
|
301
|
+
(!fromDirectory.startsWith(`..${sep}`) && fromDirectory !== '..' && !isAbsolute(fromDirectory))
|
|
302
|
+
)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function resolveManifestPath(migrationsDir: string, override?: string): Promise<string> {
|
|
306
|
+
if (!override) return join(migrationsDir, MANIFEST_NAME)
|
|
307
|
+
const requested = resolve(override)
|
|
308
|
+
if (extname(requested).toLowerCase() !== '.json') {
|
|
309
|
+
throw new Error('CMS migration manifest override must be a .json file')
|
|
310
|
+
}
|
|
311
|
+
if (isInsideDirectory(migrationsDir, requested)) {
|
|
312
|
+
throw new Error('CMS migration manifest override must be outside migrationsDir')
|
|
313
|
+
}
|
|
314
|
+
const parent = await requireExistingDirectory(dirname(requested), 'manifest parent')
|
|
315
|
+
const resolvedPath = join(parent, basename(requested))
|
|
316
|
+
if (isInsideDirectory(migrationsDir, resolvedPath)) {
|
|
317
|
+
throw new Error('CMS migration manifest override must be outside migrationsDir')
|
|
318
|
+
}
|
|
319
|
+
return resolvedPath
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function withManifestLock<T>(manifestPath: string, action: () => Promise<T>): Promise<T> {
|
|
323
|
+
const lockPath = `${manifestPath}.lock`
|
|
324
|
+
try {
|
|
325
|
+
await writeFile(lockPath, `${JSON.stringify({ pid: process.pid, startedAt: Date.now() })}\n`, {
|
|
326
|
+
flag: 'wx',
|
|
327
|
+
mode: 0o600,
|
|
328
|
+
})
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
|
|
331
|
+
throw new Error(`another CMS migration operation is already active: ${lockPath}`)
|
|
332
|
+
}
|
|
333
|
+
throw error
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let result: T | undefined
|
|
337
|
+
let operationError: unknown
|
|
338
|
+
try {
|
|
339
|
+
result = await action()
|
|
340
|
+
} catch (error) {
|
|
341
|
+
operationError = error
|
|
342
|
+
}
|
|
343
|
+
let releaseError: unknown
|
|
344
|
+
try {
|
|
345
|
+
await unlink(lockPath)
|
|
346
|
+
} catch (error) {
|
|
347
|
+
releaseError = error
|
|
348
|
+
}
|
|
349
|
+
if (operationError && releaseError) {
|
|
350
|
+
throw new AggregateError(
|
|
351
|
+
[operationError, releaseError],
|
|
352
|
+
'CMS migration operation failed and its receipt lock could not be released',
|
|
353
|
+
)
|
|
354
|
+
}
|
|
355
|
+
if (operationError) throw operationError
|
|
356
|
+
if (releaseError) throw releaseError
|
|
357
|
+
return result as T
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export async function syncCmsMigrations(
|
|
361
|
+
options: SyncCmsMigrationsOptions,
|
|
362
|
+
): Promise<CmsMigrationVendorResult> {
|
|
363
|
+
assertStartSequence(options.startSequence)
|
|
364
|
+
const migrationsDir = await requireExistingDirectory(resolve(options.migrationsDir))
|
|
365
|
+
const manifestPath = await resolveManifestPath(migrationsDir, options.manifestPath)
|
|
366
|
+
return withManifestLock(manifestPath, async () => {
|
|
367
|
+
const contract = await packageContract()
|
|
368
|
+
const plan = await planMigrations(contract.sourceDir, options.startSequence)
|
|
369
|
+
const nextManifest = manifestFor(contract.packageVersion, options.startSequence, plan)
|
|
370
|
+
const currentManifest = await readManifest(manifestPath)
|
|
371
|
+
assertManifestCanAdvance(currentManifest, nextManifest)
|
|
372
|
+
await assertNoSequenceCollisions(migrationsDir, plan)
|
|
373
|
+
const preflight = await verifyTargets(migrationsDir, plan, true)
|
|
374
|
+
const staged: StagedMigrationVendorFile[] = []
|
|
375
|
+
let manifestTemp: string | null = null
|
|
376
|
+
let verified = 0
|
|
377
|
+
|
|
378
|
+
try {
|
|
379
|
+
for (const migration of preflight.missing) {
|
|
380
|
+
const targetPath = join(migrationsDir, migration.target)
|
|
381
|
+
staged.push({
|
|
382
|
+
targetPath,
|
|
383
|
+
tempPath: await stageNewFile(targetPath, migration.bytes),
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
const stagedManifest = await stageManifest(manifestPath, nextManifest)
|
|
387
|
+
manifestTemp = stagedManifest
|
|
388
|
+
await commitStagedMigrationBatch(staged, async () => {
|
|
389
|
+
await assertNoSequenceCollisions(migrationsDir, plan)
|
|
390
|
+
const final = await verifyTargets(migrationsDir, plan, false)
|
|
391
|
+
verified = final.verified
|
|
392
|
+
await commitManifest(stagedManifest, manifestPath, currentManifest)
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
return {
|
|
396
|
+
packageName: PACKAGE_NAME,
|
|
397
|
+
packageVersion: contract.packageVersion,
|
|
398
|
+
startSequence: options.startSequence,
|
|
399
|
+
managedFiles: plan.map(({ target }) => target),
|
|
400
|
+
written: preflight.missing.length,
|
|
401
|
+
verified,
|
|
402
|
+
manifestPath,
|
|
403
|
+
}
|
|
404
|
+
} finally {
|
|
405
|
+
await Promise.all([
|
|
406
|
+
...staged.map(({ tempPath }) => unlink(tempPath).catch(() => {})),
|
|
407
|
+
...(manifestTemp ? [unlink(manifestTemp).catch(() => {})] : []),
|
|
408
|
+
])
|
|
409
|
+
}
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export async function verifyCmsMigrations(
|
|
414
|
+
options: VerifyCmsMigrationsOptions,
|
|
415
|
+
): Promise<CmsMigrationVendorResult> {
|
|
416
|
+
const migrationsDir = await requireExistingDirectory(resolve(options.migrationsDir))
|
|
417
|
+
const manifestPath = await resolveManifestPath(migrationsDir, options.manifestPath)
|
|
418
|
+
return withManifestLock(manifestPath, async () => {
|
|
419
|
+
const manifest = await readManifest(manifestPath)
|
|
420
|
+
if (!manifest) throw new Error(`CMS migration manifest is missing: ${manifestPath}`)
|
|
421
|
+
const contract = await packageContract()
|
|
422
|
+
if (manifest.packageVersion !== contract.packageVersion) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
`CMS migration manifest version ${manifest.packageVersion} does not match installed ${contract.packageVersion}`,
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
const plan = await planMigrations(contract.sourceDir, manifest.startSequence)
|
|
428
|
+
const expected = manifestFor(contract.packageVersion, manifest.startSequence, plan)
|
|
429
|
+
if (
|
|
430
|
+
expected.files.length !== manifest.files.length ||
|
|
431
|
+
expected.files.some(
|
|
432
|
+
(entry, index) => !entriesEqual(entry, manifest.files[index] as CmsMigrationManifestEntry),
|
|
433
|
+
)
|
|
434
|
+
) {
|
|
435
|
+
throw new Error('CMS migration manifest does not match the installed package contract')
|
|
436
|
+
}
|
|
437
|
+
await assertNoSequenceCollisions(migrationsDir, plan)
|
|
438
|
+
const result = await verifyTargets(migrationsDir, plan, false)
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
packageName: PACKAGE_NAME,
|
|
442
|
+
packageVersion: contract.packageVersion,
|
|
443
|
+
startSequence: manifest.startSequence,
|
|
444
|
+
managedFiles: plan.map(({ target }) => target),
|
|
445
|
+
written: 0,
|
|
446
|
+
verified: result.verified,
|
|
447
|
+
manifestPath,
|
|
448
|
+
}
|
|
449
|
+
})
|
|
450
|
+
}
|