From eaeeb60d8d1d92344ea3fae2ea5d15632643eb69 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 22 Apr 2026 13:51:28 +0100 Subject: [PATCH 1/5] Revert "Remove forceActions flag" This reverts commit 360599a7dba3ed751a8c370105da9144d6f23b4e. --- src/commands/current.ts | 10 +++++++++- src/currentRunner.ts | 5 +++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/commands/current.ts b/src/commands/current.ts index 29e4241..949b2c2 100644 --- a/src/commands/current.ts +++ b/src/commands/current.ts @@ -9,13 +9,14 @@ import { _migrate } from "./migrate"; interface CurrentArgv extends CommonArgv { shadow?: boolean; + forceActions?: boolean; } export async function current( settings: Settings, options: Partial = {}, ): Promise { - const { shadow = false } = options; + const { shadow = false, forceActions = false } = options; const parsedSettings = await parseSettings(settings, shadow); await _migrate(parsedSettings, shadow); @@ -31,6 +32,7 @@ export async function current( const run = makeCurrentMigrationRunner(parsedSettings, { once: true, shadow, + forceActions, }); return run(); } @@ -49,6 +51,12 @@ export const currentCommand: CommandModule< default: false, description: "Apply migrations to the shadow DB (for development).", }, + forceActions: { + type: "boolean", + default: false, + description: + "Run beforeAllMigrations and afterAllMigrations actions even if no migration was necessary.", + }, }, handler: async (argv) => { const settings = await getSettings({ configFile: argv.config }); diff --git a/src/currentRunner.ts b/src/currentRunner.ts index 35758c1..61e92ba 100644 --- a/src/currentRunner.ts +++ b/src/currentRunner.ts @@ -13,9 +13,10 @@ export function makeCurrentMigrationRunner( options: { once?: boolean; shadow?: boolean; + forceActions?: boolean; } = {}, ): () => Promise { - const { shadow = false } = options; + const { shadow = false, forceActions = false } = options; async function run(): Promise { const currentLocation = await getCurrentMigrationLocation(parsedSettings); const body = await readCurrentMigration(parsedSettings, currentLocation); @@ -75,7 +76,7 @@ export function makeCurrentMigrationRunner( currentBodyMinified === previousBodyMinified; // 4: if different - if (!migrationsAreEquivalent) { + if (forceActions || !migrationsAreEquivalent) { await executeActions( parsedSettings, shadow, From 80224fcbcf7c5d31e3e74c41470a857cd2c6a7c0 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 22 Apr 2026 13:59:27 +0100 Subject: [PATCH 2/5] Forcing actions shouldn't force migration to run --- src/currentRunner.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/currentRunner.ts b/src/currentRunner.ts index 61e92ba..4333b52 100644 --- a/src/currentRunner.ts +++ b/src/currentRunner.ts @@ -75,14 +75,17 @@ export function makeCurrentMigrationRunner( migrationsAreEquivalent = currentBodyMinified === previousBodyMinified; - // 4: if different + // 3a: Run actions if the migrations are different OR if forced. if (forceActions || !migrationsAreEquivalent) { await executeActions( parsedSettings, shadow, parsedSettings.beforeCurrent, ); + } + // 4: if different + if (!migrationsAreEquivalent) { // 4a: invert previous current; on success delete from graphile_migrate.current; on failure rollback and abort if (previousBody) { await reverseMigration(lockingPgClient, previousBody); @@ -136,7 +139,7 @@ export function makeCurrentMigrationRunner( ); const interval = process.hrtime(start); const duration = interval[0] * 1e3 + interval[1] * 1e-6; - if (!migrationsAreEquivalent) { + if (forceActions || !migrationsAreEquivalent) { await executeActions( parsedSettings, shadow, From 5c07d0a397cb05d365e2a6623806c711edf113e5 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 22 Apr 2026 14:06:29 +0100 Subject: [PATCH 3/5] Update README --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9cb3122..ff586c6 100644 --- a/README.md +++ b/README.md @@ -302,9 +302,13 @@ Runs any un-executed committed migrations, as well as the current migration. For development. Options: - --help Show help [boolean] - -c, --config Optional path to gmrc file [string] [default: .gmrc[.js|.cjs]] - --shadow Apply migrations to the shadow DB (for development). + --help Show help [boolean] + -c, --config Optional path to gmrc file + [string] [default: .gmrc[.js|.cjs]] + --shadow Apply migrations to the shadow DB (for development). + [boolean] [default: false] + --forceActions Run beforeAllMigrations and afterAllMigrations actions + even if no migration was necessary. [boolean] [default: false] ``` From 955b13577bb667fec98bbea67acb0450999caaca Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 22 Apr 2026 14:08:37 +0100 Subject: [PATCH 4/5] Clarify docs, fix omission --- README.md | 6 +++--- src/commands/current.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ff586c6..44364e4 100644 --- a/README.md +++ b/README.md @@ -307,9 +307,9 @@ Options: [string] [default: .gmrc[.js|.cjs]] --shadow Apply migrations to the shadow DB (for development). [boolean] [default: false] - --forceActions Run beforeAllMigrations and afterAllMigrations actions - even if no migration was necessary. - [boolean] [default: false] + --forceActions Run beforeAllMigrations, afterAllMigrations, + beforeCurrent, and afterCurrent actions even if no + migration was necessary. [boolean] [default: false] ``` diff --git a/src/commands/current.ts b/src/commands/current.ts index 949b2c2..57c8eea 100644 --- a/src/commands/current.ts +++ b/src/commands/current.ts @@ -18,7 +18,7 @@ export async function current( ): Promise { const { shadow = false, forceActions = false } = options; const parsedSettings = await parseSettings(settings, shadow); - await _migrate(parsedSettings, shadow); + await _migrate(parsedSettings, shadow, forceActions); const currentLocation = await getCurrentMigrationLocation(parsedSettings); if (!currentLocation.exists) { @@ -55,7 +55,7 @@ export const currentCommand: CommandModule< type: "boolean", default: false, description: - "Run beforeAllMigrations and afterAllMigrations actions even if no migration was necessary.", + "Run beforeAllMigrations, afterAllMigrations, beforeCurrent, and afterCurrent actions even if no migration was necessary.", }, }, handler: async (argv) => { From d808bded5323a0016f6cb997c56edd792f66499f Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 22 Apr 2026 15:59:29 +0100 Subject: [PATCH 5/5] Add tests that expected actions are called --- __tests__/current.test.ts | 70 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/__tests__/current.test.ts b/__tests__/current.test.ts index aa98590..8174da9 100644 --- a/__tests__/current.test.ts +++ b/__tests__/current.test.ts @@ -1,10 +1,12 @@ +jest.mock("child_process"); import "./helpers"; // Has side-effects; must come first +import { exec } from "child_process"; import mockFs from "mock-fs"; import { current } from "../src"; import { withClient } from "../src/pg"; -import { ParsedSettings, parseSettings } from "../src/settings"; +import { ParsedSettings, parseSettings, Settings } from "../src/settings"; import { makeMigrations, resetDb, settings } from "./helpers"; beforeEach(resetDb); @@ -134,3 +136,69 @@ it("runs migrations", async () => { expect(newTables).toEqual(tables); expect(newEnums).toEqual(enums); }); + +it("runs actions when forceActions is set", async () => { + const ACTIONS = { + initial: { + forceActions: false, + currentSql: "", + expectedActions: [ + "beforeAllMigrations", + "afterAllMigrations", + "beforeCurrent", + "afterCurrent", + ], + }, + currentChange: { + forceActions: false, + currentSql: MIGRATION_NOTRX_TEXT, + expectedActions: ["beforeCurrent", "afterCurrent"], + }, + noop: { + forceActions: false, + currentSql: MIGRATION_NOTRX_TEXT, + expectedActions: [], + }, + forceActions: { + forceActions: true, + currentSql: MIGRATION_NOTRX_TEXT, + expectedActions: [ + "beforeAllMigrations", + "afterAllMigrations", + "beforeCurrent", + "afterCurrent", + ], + }, + } as const; + const settingsWithHooks: Settings = { + ...settings, + beforeAllMigrations: [ + { _: "command", command: "echo did_beforeAllMigrations" }, + ], + afterAllMigrations: [ + { _: "command", command: "echo did_afterAllMigrations" }, + ], + beforeCurrent: [{ _: "command", command: "echo did_beforeCurrent" }], + afterCurrent: [{ _: "command", command: "echo did_afterCurrent" }], + }; + for (const mode of Object.keys(ACTIONS) as Array) { + const { forceActions, currentSql, expectedActions } = ACTIONS[mode]; + + mockFs({ + [`migrations/committed/000001.sql`]: MIGRATION_1_COMMITTED, + [`migrations/committed/000002.sql`]: MIGRATION_ENUM_COMMITTED, // Creates enum with 1 value + "migrations/current.sql": currentSql, + }); + + const mockedExec: jest.Mock = exec as any; + mockedExec.mockClear(); + mockedExec.mockImplementation((_cmd, _options, callback) => + callback(null, { stdout: "", stderr: "" }), + ); + await current(settingsWithHooks, { forceActions }); + const calledActions = mockedExec.mock.calls.map((c) => + c[0].substring("echo did_".length), + ); + expect(calledActions).toEqual(expectedActions); + } +});