Skip to content

Commit b1c4aee

Browse files
AKIBUZZAMAN AKIBakib
andauthored
fix(run-scripts): load WorkflowLoader during db:migrate so workflow hooks are registered (#15260)
## What Workflow hooks defined in `src/workflows/` (and plugin workflow directories) are silently skipped during `medusa db:migrate` execution. Closes #14125 ## Root Cause `loadResources()` in `packages/medusa/src/commands/db/run-scripts.ts` calls `LinkLoader` and `MedusaAppLoader` but never calls `WorkflowLoader`. This means any hook registered on a built-in workflow (e.g. `createProductsWorkflow.hooks.productsCreated`) does not fire when that workflow is invoked from a migration script — even though the workflow itself runs successfully and no error is thrown. Compare to `packages/medusa/src/loaders/index.ts` (lines 205–207), where `WorkflowLoader` is already called with all plugin paths. That call was simply missing from `run-scripts.ts`. ## Fix Add a `WorkflowLoader` call in `loadResources()` after `onApplicationStart()`, mirroring the exact pattern already used in `loaders/index.ts`: ```ts const workflowsSourcePaths = plugins.map((plugin) => join(plugin.resolve, "workflows") ) await new WorkflowLoader(workflowsSourcePaths, container).load() ``` `getResolvedPlugins()` already includes the application root directory as a plugin entry (via the third `true` argument), so no additional path handling is needed. ## Changes | File | Change | |---|---| | `packages/medusa/src/commands/db/run-scripts.ts` | Import `WorkflowLoader`; call it after `onApplicationStart()` in `loadResources()` | | `packages/medusa/src/commands/__tests__/run-scripts.spec.ts` | Add `WorkflowLoader` mock; add 4 new tests covering path derivation, `load()` invocation, call ordering, and zero-plugin edge case | ## Testing Four new unit tests added in the existing `run-scripts.spec.ts` test suite: - **path derivation** — verifies `WorkflowLoader` is constructed with `<plugin.resolve>/workflows` paths and the caller's container - **load() called** — verifies `WorkflowLoader.load()` is invoked exactly once - **call ordering** — verifies `WorkflowLoader` runs *after* `onApplicationStart()` so all module registrations are available - **zero-plugin edge case** — verifies behaviour when `getResolvedPlugins` returns an empty array ## Impact / Risk - ✅ **Purely additive** — no existing behaviour is changed - ✅ **Consistent** with `loaders/index.ts` which already uses this exact pattern - ✅ **No breaking changes** — `WorkflowLoader` is a no-op when the source paths contain no workflow files Co-authored-by: Akib <2947572+akib@users.noreply.github.com>
1 parent 3d23fa7 commit b1c4aee

3 files changed

Lines changed: 118 additions & 0 deletions

File tree

.changeset/big-teachers-cheat.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@medusajs/medusa": patch
3+
---
4+
5+
fix(medusa): load WorkflowLoader during db:migrate so workflow hooks are registered

packages/medusa/src/commands/__tests__/run-scripts.spec.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ContainerRegistrationKeys,
77
getResolvedPlugins,
88
} from "@medusajs/framework/utils"
9+
import { WorkflowLoader } from "@medusajs/framework/workflows"
910
import { MedusaContainer } from "@medusajs/types"
1011
import { runMigrationScripts } from "../db/run-scripts"
1112

@@ -19,6 +20,10 @@ jest.mock("@medusajs/framework/links", () => ({
1920
LinkLoader: jest.fn(),
2021
}))
2122

23+
jest.mock("@medusajs/framework/workflows", () => ({
24+
WorkflowLoader: jest.fn(),
25+
}))
26+
2227
jest.mock("@medusajs/framework/migrations", () => ({
2328
MigrationScriptsMigrator: jest.fn(),
2429
}))
@@ -100,6 +105,11 @@ describe("runMigrationScripts", () => {
100105
getPendingMigrations: jest.fn().mockResolvedValue([]),
101106
run: jest.fn().mockResolvedValue(undefined),
102107
}))
108+
109+
// WorkflowLoader mock
110+
;(WorkflowLoader as jest.Mock).mockImplementation(() => ({
111+
load: jest.fn().mockResolvedValue(undefined),
112+
}))
103113
})
104114

105115
describe("container forwarding to MedusaAppLoader", () => {
@@ -228,4 +238,96 @@ describe("runMigrationScripts", () => {
228238
expect(MedusaModule.clearInstances).toHaveBeenCalledTimes(1)
229239
})
230240
})
241+
242+
describe("WorkflowLoader", () => {
243+
it("instantiates WorkflowLoader with the resolved plugin workflow paths", async () => {
244+
const plugins = [
245+
{ resolve: "/app" },
246+
{ resolve: "/plugins/my-plugin" },
247+
]
248+
;(getResolvedPlugins as jest.Mock).mockResolvedValue(plugins)
249+
250+
const container = buildContainer()
251+
252+
await runMigrationScripts({
253+
directory: "/app",
254+
container,
255+
logger: mockLogger as any,
256+
})
257+
258+
expect(WorkflowLoader).toHaveBeenCalledTimes(1)
259+
const [paths, resolvedContainer] = (WorkflowLoader as jest.Mock).mock
260+
.calls[0]
261+
expect(paths).toEqual(["/app/workflows", "/plugins/my-plugin/workflows"])
262+
expect(resolvedContainer).toBe(container)
263+
})
264+
265+
it("calls WorkflowLoader.load() to register workflow hooks", async () => {
266+
const container = buildContainer()
267+
const mockLoad = jest.fn().mockResolvedValue(undefined)
268+
;(WorkflowLoader as jest.Mock).mockImplementation(() => ({
269+
load: mockLoad,
270+
}))
271+
272+
await runMigrationScripts({
273+
directory: "/app",
274+
container,
275+
logger: mockLogger as any,
276+
})
277+
278+
expect(mockLoad).toHaveBeenCalledTimes(1)
279+
})
280+
281+
it("loads WorkflowLoader after MedusaAppLoader so module registrations are available", async () => {
282+
const callOrder: string[] = []
283+
284+
;(MedusaAppLoader as jest.Mock).mockImplementation(() => ({
285+
load: jest.fn().mockImplementation(async () => {
286+
callOrder.push("MedusaAppLoader.load")
287+
return {
288+
onApplicationPrepareShutdown: jest.fn().mockResolvedValue(undefined),
289+
onApplicationShutdown: jest.fn().mockResolvedValue(undefined),
290+
onApplicationStart: jest.fn().mockImplementation(async () => {
291+
callOrder.push("onApplicationStart")
292+
}),
293+
}
294+
}),
295+
}))
296+
297+
;(WorkflowLoader as jest.Mock).mockImplementation(() => ({
298+
load: jest.fn().mockImplementation(async () => {
299+
callOrder.push("WorkflowLoader.load")
300+
}),
301+
}))
302+
303+
const container = buildContainer()
304+
305+
await runMigrationScripts({
306+
directory: "/app",
307+
container,
308+
logger: mockLogger as any,
309+
})
310+
311+
const appStartIdx = callOrder.indexOf("onApplicationStart")
312+
const wfLoaderIdx = callOrder.indexOf("WorkflowLoader.load")
313+
expect(appStartIdx).toBeGreaterThanOrEqual(0)
314+
expect(wfLoaderIdx).toBeGreaterThan(appStartIdx)
315+
})
316+
317+
it("produces no workflow paths when there are no plugins", async () => {
318+
;(getResolvedPlugins as jest.Mock).mockResolvedValue([])
319+
320+
const container = buildContainer()
321+
322+
await runMigrationScripts({
323+
directory: "/app",
324+
container,
325+
logger: mockLogger as any,
326+
})
327+
328+
expect(WorkflowLoader).toHaveBeenCalledTimes(1)
329+
const [paths] = (WorkflowLoader as jest.Mock).mock.calls[0]
330+
expect(paths).toEqual([])
331+
})
332+
})
231333
})

packages/medusa/src/commands/db/run-scripts.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { MedusaAppLoader } from "@medusajs/framework"
22
import { LinkLoader } from "@medusajs/framework/links"
33
import { MigrationScriptsMigrator } from "@medusajs/framework/migrations"
4+
import { WorkflowLoader } from "@medusajs/framework/workflows"
45
import {
56
ContainerRegistrationKeys,
67
getResolvedPlugins,
@@ -115,6 +116,16 @@ async function loadResources(
115116
const onApplicationShutdown = medusaAppResources.onApplicationShutdown
116117
await medusaAppResources.onApplicationStart()
117118

119+
// Load workflow hooks from all plugins (including the app directory,
120+
// which getResolvedPlugins already includes as a plugin entry).
121+
// Without this, workflow hooks defined in src/workflows/ are never
122+
// registered during `medusa db:migrate`, causing them to silently
123+
// skip execution even though the workflows themselves run successfully.
124+
const workflowsSourcePaths = plugins.map((plugin) =>
125+
join(plugin.resolve, "workflows")
126+
)
127+
await new WorkflowLoader(workflowsSourcePaths, container).load()
128+
118129
return {
119130
onApplicationPrepareShutdown,
120131
onApplicationShutdown,

0 commit comments

Comments
 (0)