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 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 -- resolution timestamp for skipped
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.3
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
  }));