migraguard 0.8.3 → 0.8.4
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 +2 -1
- package/cli-contract.yaml +18 -1
- package/dist/cli.js +71 -43
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +62 -37
- package/dist/index.js.map +1 -1
- package/docs/cli-reference.md +14 -1
- package/docs/commands.md +1 -0
- package/docs/state-model.md +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -124,7 +124,8 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
|
124
124
|
checksum VARCHAR(64) NOT NULL,
|
|
125
125
|
status VARCHAR(16) NOT NULL DEFAULT 'applied', -- applied / failed / skipped
|
|
126
126
|
applied_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
127
|
-
resolved_at TIMESTAMPTZ
|
|
127
|
+
resolved_at TIMESTAMPTZ, -- resolution timestamp for skipped
|
|
128
|
+
tag VARCHAR(256) -- caller-supplied tag (e.g. commit hash, release tag)
|
|
128
129
|
);
|
|
129
130
|
```
|
|
130
131
|
|
package/cli-contract.yaml
CHANGED
|
@@ -3,7 +3,7 @@ cliContracts: 0.1.0
|
|
|
3
3
|
|
|
4
4
|
info:
|
|
5
5
|
title: migraguard CLI
|
|
6
|
-
version: 0.8.
|
|
6
|
+
version: 0.8.4
|
|
7
7
|
description: >-
|
|
8
8
|
PostgreSQL-first schema-aware deployment control — idempotent SQL migrations
|
|
9
9
|
with CI-enforced integrity checks, expand/contract migration orchestration,
|
|
@@ -112,6 +112,11 @@ commandSets:
|
|
|
112
112
|
type: boolean
|
|
113
113
|
default: false
|
|
114
114
|
|
|
115
|
+
- name: tag
|
|
116
|
+
description: Tag to record with applied migrations (e.g. commit hash, release tag).
|
|
117
|
+
schema:
|
|
118
|
+
type: string
|
|
119
|
+
|
|
115
120
|
exits:
|
|
116
121
|
'0':
|
|
117
122
|
description: All pending migrations applied successfully.
|
|
@@ -536,6 +541,12 @@ commandSets:
|
|
|
536
541
|
type: string
|
|
537
542
|
enum: [running, completed, failed]
|
|
538
543
|
|
|
544
|
+
options:
|
|
545
|
+
- name: tag
|
|
546
|
+
description: Tag to record (e.g. commit hash, release tag).
|
|
547
|
+
schema:
|
|
548
|
+
type: string
|
|
549
|
+
|
|
539
550
|
exits:
|
|
540
551
|
'0':
|
|
541
552
|
description: Phase state transition recorded.
|
|
@@ -584,6 +595,12 @@ commandSets:
|
|
|
584
595
|
type: string
|
|
585
596
|
enum: [expand, backfill, switch, contract]
|
|
586
597
|
|
|
598
|
+
options:
|
|
599
|
+
- name: tag
|
|
600
|
+
description: Tag to record (e.g. commit hash, release tag).
|
|
601
|
+
schema:
|
|
602
|
+
type: string
|
|
603
|
+
|
|
587
604
|
exits:
|
|
588
605
|
'0':
|
|
589
606
|
description: Phase applied successfully.
|
package/dist/cli.js
CHANGED
|
@@ -136,7 +136,8 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
|
136
136
|
resolved_at TIMESTAMP(6) NULL,
|
|
137
137
|
migration_class VARCHAR(16) DEFAULT 'safe',
|
|
138
138
|
phase VARCHAR(16) NULL,
|
|
139
|
-
group_name VARCHAR(256) NULL
|
|
139
|
+
group_name VARCHAR(256) NULL,
|
|
140
|
+
tag VARCHAR(256) NULL
|
|
140
141
|
) ENGINE=InnoDB;
|
|
141
142
|
`;
|
|
142
143
|
var MigraguardDbMysql = class {
|
|
@@ -172,6 +173,13 @@ var MigraguardDbMysql = class {
|
|
|
172
173
|
}
|
|
173
174
|
async ensureTable() {
|
|
174
175
|
await this.exec(CREATE_TABLE_SQL);
|
|
176
|
+
const rows = await this.queryRows(
|
|
177
|
+
`SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
|
|
178
|
+
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'schema_migrations' AND COLUMN_NAME = 'tag'`
|
|
179
|
+
);
|
|
180
|
+
if (rows.length === 0) {
|
|
181
|
+
await this.exec(`ALTER TABLE schema_migrations ADD COLUMN tag VARCHAR(256) NULL`);
|
|
182
|
+
}
|
|
175
183
|
}
|
|
176
184
|
async acquireAdvisoryLock() {
|
|
177
185
|
await this.exec(`SELECT GET_LOCK(?, -1)`, [ADVISORY_LOCK_KEY]);
|
|
@@ -182,7 +190,7 @@ var MigraguardDbMysql = class {
|
|
|
182
190
|
async getAllRecords() {
|
|
183
191
|
const rows = await this.queryRows(
|
|
184
192
|
`SELECT file_name, checksum, status, applied_at, resolved_at,
|
|
185
|
-
migration_class, phase, group_name
|
|
193
|
+
migration_class, phase, group_name, tag
|
|
186
194
|
FROM schema_migrations
|
|
187
195
|
ORDER BY applied_at ASC`
|
|
188
196
|
);
|
|
@@ -191,7 +199,7 @@ var MigraguardDbMysql = class {
|
|
|
191
199
|
async getRecordsForFile(fileName) {
|
|
192
200
|
const rows = await this.queryRows(
|
|
193
201
|
`SELECT file_name, checksum, status, applied_at, resolved_at,
|
|
194
|
-
migration_class, phase, group_name
|
|
202
|
+
migration_class, phase, group_name, tag
|
|
195
203
|
FROM schema_migrations
|
|
196
204
|
WHERE file_name = ?
|
|
197
205
|
ORDER BY applied_at ASC`,
|
|
@@ -203,17 +211,18 @@ var MigraguardDbMysql = class {
|
|
|
203
211
|
const migrationClass = options?.migrationClass ?? "safe";
|
|
204
212
|
const phase = options?.phase ?? null;
|
|
205
213
|
const groupName = options?.groupName ?? null;
|
|
214
|
+
const tag = options?.tag ?? null;
|
|
206
215
|
if (status === "skipped") {
|
|
207
216
|
await this.exec(
|
|
208
|
-
`INSERT INTO schema_migrations (file_name, checksum, status, resolved_at, migration_class, phase, group_name)
|
|
209
|
-
VALUES (?, ?, ?, CURRENT_TIMESTAMP(6), ?, ?, ?)`,
|
|
210
|
-
[fileName, checksum, status, migrationClass, phase, groupName]
|
|
217
|
+
`INSERT INTO schema_migrations (file_name, checksum, status, resolved_at, migration_class, phase, group_name, tag)
|
|
218
|
+
VALUES (?, ?, ?, CURRENT_TIMESTAMP(6), ?, ?, ?, ?)`,
|
|
219
|
+
[fileName, checksum, status, migrationClass, phase, groupName, tag]
|
|
211
220
|
);
|
|
212
221
|
} else {
|
|
213
222
|
await this.exec(
|
|
214
|
-
`INSERT INTO schema_migrations (file_name, checksum, status, migration_class, phase, group_name)
|
|
215
|
-
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
216
|
-
[fileName, checksum, status, migrationClass, phase, groupName]
|
|
223
|
+
`INSERT INTO schema_migrations (file_name, checksum, status, migration_class, phase, group_name, tag)
|
|
224
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
225
|
+
[fileName, checksum, status, migrationClass, phase, groupName, tag]
|
|
217
226
|
);
|
|
218
227
|
}
|
|
219
228
|
}
|
|
@@ -238,7 +247,8 @@ function mapRow(row) {
|
|
|
238
247
|
resolvedAt: row["resolved_at"] ? row["resolved_at"] instanceof Date ? row["resolved_at"] : new Date(row["resolved_at"]) : null,
|
|
239
248
|
migrationClass: row["migration_class"] ?? "safe",
|
|
240
249
|
phase: row["phase"] ?? null,
|
|
241
|
-
groupName: row["group_name"] ?? null
|
|
250
|
+
groupName: row["group_name"] ?? null,
|
|
251
|
+
tag: row["tag"] ?? null
|
|
242
252
|
};
|
|
243
253
|
}
|
|
244
254
|
|
|
@@ -253,7 +263,8 @@ CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
|
253
263
|
resolved_at TEXT,
|
|
254
264
|
migration_class TEXT DEFAULT 'safe',
|
|
255
265
|
phase TEXT,
|
|
256
|
-
group_name TEXT
|
|
266
|
+
group_name TEXT,
|
|
267
|
+
tag TEXT
|
|
257
268
|
);
|
|
258
269
|
`;
|
|
259
270
|
var MigraguardDbSqlite = class {
|
|
@@ -284,6 +295,11 @@ var MigraguardDbSqlite = class {
|
|
|
284
295
|
}
|
|
285
296
|
async ensureTable() {
|
|
286
297
|
this.db().exec(CREATE_TABLE_SQL2);
|
|
298
|
+
const cols = this.db().prepare(`PRAGMA table_info(schema_migrations)`).all();
|
|
299
|
+
const hasTag = cols.some((c) => c["name"] === "tag");
|
|
300
|
+
if (!hasTag) {
|
|
301
|
+
this.db().exec(`ALTER TABLE schema_migrations ADD COLUMN tag TEXT`);
|
|
302
|
+
}
|
|
287
303
|
}
|
|
288
304
|
async acquireAdvisoryLock() {
|
|
289
305
|
}
|
|
@@ -292,7 +308,7 @@ var MigraguardDbSqlite = class {
|
|
|
292
308
|
async getAllRecords() {
|
|
293
309
|
const rows = this.db().prepare(
|
|
294
310
|
`SELECT file_name, checksum, status, applied_at, resolved_at,
|
|
295
|
-
migration_class, phase, group_name
|
|
311
|
+
migration_class, phase, group_name, tag
|
|
296
312
|
FROM schema_migrations
|
|
297
313
|
ORDER BY applied_at ASC`
|
|
298
314
|
).all();
|
|
@@ -301,7 +317,7 @@ var MigraguardDbSqlite = class {
|
|
|
301
317
|
async getRecordsForFile(fileName) {
|
|
302
318
|
const rows = this.db().prepare(
|
|
303
319
|
`SELECT file_name, checksum, status, applied_at, resolved_at,
|
|
304
|
-
migration_class, phase, group_name
|
|
320
|
+
migration_class, phase, group_name, tag
|
|
305
321
|
FROM schema_migrations
|
|
306
322
|
WHERE file_name = ?
|
|
307
323
|
ORDER BY applied_at ASC`
|
|
@@ -312,16 +328,17 @@ var MigraguardDbSqlite = class {
|
|
|
312
328
|
const migrationClass = options?.migrationClass ?? "safe";
|
|
313
329
|
const phase = options?.phase ?? null;
|
|
314
330
|
const groupName = options?.groupName ?? null;
|
|
331
|
+
const tag = options?.tag ?? null;
|
|
315
332
|
if (status === "skipped") {
|
|
316
333
|
this.db().prepare(
|
|
317
|
-
`INSERT INTO schema_migrations (file_name, checksum, status, resolved_at, migration_class, phase, group_name)
|
|
318
|
-
VALUES (?, ?, ?, datetime('now'), ?, ?, ?)`
|
|
319
|
-
).run(fileName, checksum, status, migrationClass, phase, groupName);
|
|
334
|
+
`INSERT INTO schema_migrations (file_name, checksum, status, resolved_at, migration_class, phase, group_name, tag)
|
|
335
|
+
VALUES (?, ?, ?, datetime('now'), ?, ?, ?, ?)`
|
|
336
|
+
).run(fileName, checksum, status, migrationClass, phase, groupName, tag);
|
|
320
337
|
} else {
|
|
321
338
|
this.db().prepare(
|
|
322
|
-
`INSERT INTO schema_migrations (file_name, checksum, status, migration_class, phase, group_name)
|
|
323
|
-
VALUES (?, ?, ?, ?, ?, ?)`
|
|
324
|
-
).run(fileName, checksum, status, migrationClass, phase, groupName);
|
|
339
|
+
`INSERT INTO schema_migrations (file_name, checksum, status, migration_class, phase, group_name, tag)
|
|
340
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
341
|
+
).run(fileName, checksum, status, migrationClass, phase, groupName, tag);
|
|
325
342
|
}
|
|
326
343
|
}
|
|
327
344
|
db() {
|
|
@@ -338,7 +355,8 @@ function mapRow2(row) {
|
|
|
338
355
|
resolvedAt: row["resolved_at"] ? new Date(row["resolved_at"]) : null,
|
|
339
356
|
migrationClass: row["migration_class"] ?? "safe",
|
|
340
357
|
phase: row["phase"] ?? null,
|
|
341
|
-
groupName: row["group_name"] ?? null
|
|
358
|
+
groupName: row["group_name"] ?? null,
|
|
359
|
+
tag: row["tag"] ?? null
|
|
342
360
|
};
|
|
343
361
|
}
|
|
344
362
|
|
|
@@ -359,7 +377,8 @@ var ALTER_TABLE_SQL = `
|
|
|
359
377
|
ALTER TABLE schema_migrations
|
|
360
378
|
ADD COLUMN IF NOT EXISTS migration_class VARCHAR(16) DEFAULT 'safe',
|
|
361
379
|
ADD COLUMN IF NOT EXISTS phase VARCHAR(16),
|
|
362
|
-
ADD COLUMN IF NOT EXISTS group_name VARCHAR(256)
|
|
380
|
+
ADD COLUMN IF NOT EXISTS group_name VARCHAR(256),
|
|
381
|
+
ADD COLUMN IF NOT EXISTS tag VARCHAR(256);
|
|
363
382
|
`;
|
|
364
383
|
function createDb(config) {
|
|
365
384
|
switch (config.dialect) {
|
|
@@ -401,7 +420,7 @@ var MigraguardDb = class {
|
|
|
401
420
|
async getAllRecords() {
|
|
402
421
|
const result = await this.client.query(
|
|
403
422
|
`SELECT file_name, checksum, status, applied_at, resolved_at,
|
|
404
|
-
migration_class, phase, group_name
|
|
423
|
+
migration_class, phase, group_name, tag
|
|
405
424
|
FROM schema_migrations
|
|
406
425
|
ORDER BY applied_at ASC`
|
|
407
426
|
);
|
|
@@ -410,7 +429,7 @@ var MigraguardDb = class {
|
|
|
410
429
|
async getRecordsForFile(fileName) {
|
|
411
430
|
const result = await this.client.query(
|
|
412
431
|
`SELECT file_name, checksum, status, applied_at, resolved_at,
|
|
413
|
-
migration_class, phase, group_name
|
|
432
|
+
migration_class, phase, group_name, tag
|
|
414
433
|
FROM schema_migrations
|
|
415
434
|
WHERE file_name = $1
|
|
416
435
|
ORDER BY applied_at ASC`,
|
|
@@ -422,17 +441,18 @@ var MigraguardDb = class {
|
|
|
422
441
|
const migrationClass = options?.migrationClass ?? "safe";
|
|
423
442
|
const phase = options?.phase ?? null;
|
|
424
443
|
const groupName = options?.groupName ?? null;
|
|
444
|
+
const tag = options?.tag ?? null;
|
|
425
445
|
if (status === "skipped") {
|
|
426
446
|
await this.client.query(
|
|
427
|
-
`INSERT INTO schema_migrations (file_name, checksum, status, resolved_at, migration_class, phase, group_name)
|
|
428
|
-
VALUES ($1, $2, $3, CURRENT_TIMESTAMP, $4, $5, $6)`,
|
|
429
|
-
[fileName, checksum, status, migrationClass, phase, groupName]
|
|
447
|
+
`INSERT INTO schema_migrations (file_name, checksum, status, resolved_at, migration_class, phase, group_name, tag)
|
|
448
|
+
VALUES ($1, $2, $3, CURRENT_TIMESTAMP, $4, $5, $6, $7)`,
|
|
449
|
+
[fileName, checksum, status, migrationClass, phase, groupName, tag]
|
|
430
450
|
);
|
|
431
451
|
} else {
|
|
432
452
|
await this.client.query(
|
|
433
|
-
`INSERT INTO schema_migrations (file_name, checksum, status, migration_class, phase, group_name)
|
|
434
|
-
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
435
|
-
[fileName, checksum, status, migrationClass, phase, groupName]
|
|
453
|
+
`INSERT INTO schema_migrations (file_name, checksum, status, migration_class, phase, group_name, tag)
|
|
454
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)`,
|
|
455
|
+
[fileName, checksum, status, migrationClass, phase, groupName, tag]
|
|
436
456
|
);
|
|
437
457
|
}
|
|
438
458
|
}
|
|
@@ -449,7 +469,8 @@ function mapRow3(row) {
|
|
|
449
469
|
resolvedAt: row["resolved_at"] ?? null,
|
|
450
470
|
migrationClass: row["migration_class"] ?? "safe",
|
|
451
471
|
phase: row["phase"] ?? null,
|
|
452
|
-
groupName: row["group_name"] ?? null
|
|
472
|
+
groupName: row["group_name"] ?? null,
|
|
473
|
+
tag: row["tag"] ?? null
|
|
453
474
|
};
|
|
454
475
|
}
|
|
455
476
|
|
|
@@ -1787,7 +1808,7 @@ async function commandApply(config, options) {
|
|
|
1787
1808
|
await db.ensureTable();
|
|
1788
1809
|
await db.acquireAdvisoryLock();
|
|
1789
1810
|
if (options?.fromBaseline) {
|
|
1790
|
-
await applyFromBaseline(config, db, metadata, result);
|
|
1811
|
+
await applyFromBaseline(config, db, metadata, result, options?.tag);
|
|
1791
1812
|
}
|
|
1792
1813
|
const files = await scanMigrations(config);
|
|
1793
1814
|
if (files.length === 0 && !options?.fromBaseline) {
|
|
@@ -1857,7 +1878,7 @@ async function commandApply(config, options) {
|
|
|
1857
1878
|
const latestRecord = getLatestRecord(fileRecords);
|
|
1858
1879
|
const currentChecksum = await checksumFile(file.filePath);
|
|
1859
1880
|
const isEditable = dag && leafSet ? leafSet.has(file.fileName) : file.fileName === latestFileName;
|
|
1860
|
-
const insertOpts = file.migrationClass === "expand_contract" ? { migrationClass: "expand_contract", phase: file.phase, groupName: file.groupName } : void 0;
|
|
1881
|
+
const insertOpts = file.migrationClass === "expand_contract" ? { migrationClass: "expand_contract", phase: file.phase, groupName: file.groupName, tag: options?.tag } : options?.tag ? { tag: options.tag } : void 0;
|
|
1861
1882
|
const applyResult = await processFile(
|
|
1862
1883
|
config,
|
|
1863
1884
|
db,
|
|
@@ -1986,7 +2007,7 @@ async function processFile(config, db, filePath, fileName, fileRecords, latestRe
|
|
|
1986
2007
|
return "error";
|
|
1987
2008
|
}
|
|
1988
2009
|
}
|
|
1989
|
-
async function applyFromBaseline(config, db, metadata, result) {
|
|
2010
|
+
async function applyFromBaseline(config, db, metadata, result, tag) {
|
|
1990
2011
|
const schemaPath = resolveFromConfig(config, config.schemaFile);
|
|
1991
2012
|
if (!existsSync(schemaPath)) {
|
|
1992
2013
|
result.errors.push("No schema.sql found. Cannot apply from baseline.");
|
|
@@ -2002,9 +2023,10 @@ async function applyFromBaseline(config, db, metadata, result) {
|
|
|
2002
2023
|
}
|
|
2003
2024
|
console.log(chalk.green(" \u2713 baseline schema applied"));
|
|
2004
2025
|
if (metadata.baselines) {
|
|
2026
|
+
const insertOpts = tag ? { tag } : void 0;
|
|
2005
2027
|
for (const baseline of metadata.baselines) {
|
|
2006
2028
|
for (const inc of baseline.includes) {
|
|
2007
|
-
await db.insertRecord(inc.file, inc.checksum, "applied");
|
|
2029
|
+
await db.insertRecord(inc.file, inc.checksum, "applied", insertOpts);
|
|
2008
2030
|
result.applied.push(inc.file);
|
|
2009
2031
|
}
|
|
2010
2032
|
}
|
|
@@ -2090,7 +2112,8 @@ async function commandAdvance(config, options) {
|
|
|
2090
2112
|
await db.insertRecord(fileName, "", dbStatus, {
|
|
2091
2113
|
migrationClass: "expand_contract",
|
|
2092
2114
|
phase,
|
|
2093
|
-
groupName: group
|
|
2115
|
+
groupName: group,
|
|
2116
|
+
tag: options.tag
|
|
2094
2117
|
});
|
|
2095
2118
|
const updatedRecords = await db.getAllRecords();
|
|
2096
2119
|
const newState = deriveGroupState(updatedRecords, group).state;
|
|
@@ -2216,7 +2239,8 @@ async function commandApplyPhase(config, options) {
|
|
|
2216
2239
|
await db.insertRecord(file.fileName, checksum, "applied", {
|
|
2217
2240
|
migrationClass: "expand_contract",
|
|
2218
2241
|
phase,
|
|
2219
|
-
groupName: group
|
|
2242
|
+
groupName: group,
|
|
2243
|
+
tag: options.tag
|
|
2220
2244
|
});
|
|
2221
2245
|
console.log(chalk.green(`Applied phase "${phase}" for "${group}"`));
|
|
2222
2246
|
return { success: true, group, phase };
|
|
@@ -2224,7 +2248,8 @@ async function commandApplyPhase(config, options) {
|
|
|
2224
2248
|
await db.insertRecord(file.fileName, checksum, "failed", {
|
|
2225
2249
|
migrationClass: "expand_contract",
|
|
2226
2250
|
phase,
|
|
2227
|
-
groupName: group
|
|
2251
|
+
groupName: group,
|
|
2252
|
+
tag: options.tag
|
|
2228
2253
|
});
|
|
2229
2254
|
const msg = `Failed to apply phase "${phase}" for "${group}": ${psqlResult.stderr}`;
|
|
2230
2255
|
console.error(chalk.red(msg));
|
|
@@ -5405,11 +5430,12 @@ program.command("new <name>").description("Create a new migration SQL file with
|
|
|
5405
5430
|
const config = await loadConfig();
|
|
5406
5431
|
await commandNew(config, name, { expandContract: opts.expandContract });
|
|
5407
5432
|
}));
|
|
5408
|
-
program.command("apply").description("Apply pending migrations via psql").option("--with-drift-check", "Check schema drift before apply and update dump after").option("--from-baseline", "Apply schema.sql first, then remaining migrations").action((opts) => run(async () => {
|
|
5433
|
+
program.command("apply").description("Apply pending migrations via psql").option("--with-drift-check", "Check schema drift before apply and update dump after").option("--from-baseline", "Apply schema.sql first, then remaining migrations").option("--tag <text>", "Tag to record with applied migrations (e.g. commit hash, release tag)").action((opts) => run(async () => {
|
|
5409
5434
|
const config = await loadConfig();
|
|
5410
5435
|
const result = await commandApply(config, {
|
|
5411
5436
|
withDriftCheck: opts.withDriftCheck,
|
|
5412
|
-
fromBaseline: opts.fromBaseline
|
|
5437
|
+
fromBaseline: opts.fromBaseline,
|
|
5438
|
+
tag: opts.tag
|
|
5413
5439
|
});
|
|
5414
5440
|
if (result.errors.length > 0) process.exit(1);
|
|
5415
5441
|
}));
|
|
@@ -5462,7 +5488,7 @@ program.command("baseline").description("Squash applied migrations into schema.s
|
|
|
5462
5488
|
const result = await commandBaseline(config, { keepSince: opts.keepSince });
|
|
5463
5489
|
if (!result.success) process.exit(1);
|
|
5464
5490
|
}));
|
|
5465
|
-
program.command("advance <group> <phase> <status>").description("Record a phase state transition (for external executor)").action((group, phase, status) => run(async () => {
|
|
5491
|
+
program.command("advance <group> <phase> <status>").description("Record a phase state transition (for external executor)").option("--tag <text>", "Tag to record (e.g. commit hash, release tag)").action((group, phase, status, opts) => run(async () => {
|
|
5466
5492
|
const config = await loadConfig();
|
|
5467
5493
|
const validPhases = ["expand", "backfill", "switch", "contract"];
|
|
5468
5494
|
const validStatuses = ["running", "completed", "failed"];
|
|
@@ -5471,17 +5497,19 @@ program.command("advance <group> <phase> <status>").description("Record a phase
|
|
|
5471
5497
|
const result = await commandAdvance(config, {
|
|
5472
5498
|
group,
|
|
5473
5499
|
phase,
|
|
5474
|
-
status
|
|
5500
|
+
status,
|
|
5501
|
+
tag: opts.tag
|
|
5475
5502
|
});
|
|
5476
5503
|
if (!result.success) process.exit(1);
|
|
5477
5504
|
}));
|
|
5478
|
-
program.command("apply-phase <group> <phase>").description("Apply a specific phase of a migration group via psql").action((group, phase) => run(async () => {
|
|
5505
|
+
program.command("apply-phase <group> <phase>").description("Apply a specific phase of a migration group via psql").option("--tag <text>", "Tag to record (e.g. commit hash, release tag)").action((group, phase, opts) => run(async () => {
|
|
5479
5506
|
const config = await loadConfig();
|
|
5480
5507
|
const validPhases = ["expand", "backfill", "switch", "contract"];
|
|
5481
5508
|
if (!validPhases.includes(phase)) throw new Error(`Invalid phase: ${phase}`);
|
|
5482
5509
|
const result = await commandApplyPhase(config, {
|
|
5483
5510
|
group,
|
|
5484
|
-
phase
|
|
5511
|
+
phase,
|
|
5512
|
+
tag: opts.tag
|
|
5485
5513
|
});
|
|
5486
5514
|
if (!result.success) process.exit(1);
|
|
5487
5515
|
}));
|