Skip to content

Commit efd628c

Browse files
dcramercodex
andcommitted
fix(migrations): Embed durable migration implementations
Remove the migration task registry and keep historical data migration code in each journal entry. This prevents future runtime refactors or deletions from breaking pending upgrades. Expose versioned migration capabilities, move scheduler and core backfills into their migration files, and update migration tests and documentation. Co-Authored-By: GPT-5.6 Codex <noreply@openai.com>
1 parent 2dd177d commit efd628c

30 files changed

Lines changed: 10295 additions & 1476 deletions

packages/junior-migrations/README.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,9 @@ migrations. Every journal entry resolves to exactly one `<tag>.sql` or
66
the Junior migration runner owns execution and exact per-entry tracking.
77

88
TypeScript migrations target a versioned capability API and must not import
9-
Junior runtime internals. New parsers and transforms belong in the migration
10-
file so application refactors cannot break pending upgrades. A journal can
11-
also invoke a versioned host task when adopting legacy migration code that
12-
already shipped before the mixed journal existed; the task name then becomes
13-
part of the permanent migration ABI.
9+
Junior runtime internals. Every parser, transform, and migration-specific
10+
helper belongs permanently in the migration file so application refactors or
11+
deletions cannot break pending upgrades.
1412

1513
## Authoring
1614

@@ -34,6 +32,8 @@ Drizzle Kit remains the supported authoring tool. Drizzle ORM's stock
3432
requires every entry to have a SQL file. Call `runMigrationJournal` instead and
3533
provide the SQL, state, loader, and locking capabilities owned by the host.
3634

37-
The runner validates that each TypeScript migration has no runtime imports.
38-
Only a type-only import from `@sentry/junior-migrations` is accepted. Add a new
39-
API version rather than changing an existing migration capability contract.
35+
The runner rejects runtime imports of application source, relative modules,
36+
and `@sentry/junior`. External package imports are allowed when the migration
37+
needs a stable library dependency; migration-specific implementation must
38+
still remain in the migration file. Add a new API version rather than changing
39+
an existing migration capability contract.

packages/junior-migrations/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ export type {
77
MigrationContextV1,
88
MigrationJsonValue,
99
MigrationJournalEntry,
10+
MigrationLockV1,
1011
MigrationProgressV1,
12+
MigrationRedisV1,
1113
MigrationRunResult,
1214
MigrationSqlExecutor,
1315
MigrationStateV1,
14-
MigrationTasksV1,
1516
MigrationV1,
1617
ResolvedMigration,
1718
TypeScriptMigrationLoader,

packages/junior-migrations/src/journal.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,18 +57,25 @@ function validateTypeScriptSource(tag: string, source: string): void {
5757
for (const match of imports) {
5858
const clause = match[1]?.trim();
5959
const specifier = match[2];
60+
if (clause?.startsWith("type ")) {
61+
continue;
62+
}
6063
if (
61-
!clause?.startsWith("type ") ||
62-
specifier !== "@sentry/junior-migrations"
64+
!specifier ||
65+
specifier.startsWith(".") ||
66+
specifier.startsWith("/") ||
67+
specifier.startsWith("@/") ||
68+
specifier === "@sentry/junior" ||
69+
specifier.startsWith("@sentry/junior/")
6370
) {
6471
throw new Error(
65-
`TypeScript migration ${tag} may only import migration types`,
72+
`TypeScript migration ${tag} cannot import application runtime code`,
6673
);
6774
}
6875
}
6976
if (
7077
/^\s*import\s*["']/m.test(source) ||
71-
/^\s*export\s+.+\s+from\s+["']/m.test(source) ||
78+
/^\s*export\s+.+\s+from\s+["'][./]/m.test(source) ||
7279
/\b(?:import\s*\(|require\s*\()/.test(source)
7380
) {
7481
throw new Error(`TypeScript migration ${tag} cannot load runtime modules`);

packages/junior-migrations/src/types.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,33 @@ export interface MigrationSqlExecutor {
1212
): Promise<T>;
1313
}
1414

15+
/** Stable state-store capabilities exposed to v1 TypeScript migrations. */
16+
export interface MigrationLockV1 {
17+
expiresAt: number;
18+
threadId: string;
19+
token: string;
20+
}
21+
1522
/** Stable state-store capabilities exposed to v1 TypeScript migrations. */
1623
export interface MigrationStateV1 {
24+
acquireLock(threadId: string, ttlMs: number): Promise<MigrationLockV1 | null>;
1725
appendToList(
1826
key: string,
1927
value: unknown,
2028
options?: { maxLength?: number; ttlMs?: number },
2129
): Promise<void>;
30+
connect(): Promise<void>;
2231
delete(key: string): Promise<void>;
2332
get(key: string): Promise<unknown | undefined>;
2433
getList(key: string): Promise<unknown[]>;
34+
releaseLock(lock: MigrationLockV1): Promise<void>;
2535
set(key: string, value: unknown, ttlMs?: number): Promise<void>;
36+
setIfNotExists(key: string, value: unknown, ttlMs?: number): Promise<boolean>;
37+
}
38+
39+
/** Optional raw Redis capability for migrations preserving Redis indexes. */
40+
export interface MigrationRedisV1 {
41+
sendCommand<T = unknown>(args: readonly string[]): Promise<T>;
2642
}
2743

2844
/** Resumable progress storage scoped to one TypeScript migration. */
@@ -31,11 +47,6 @@ export interface MigrationProgressV1 {
3147
save(value: MigrationJsonValue): Promise<void>;
3248
}
3349

34-
/** Versioned host tasks available to migrations that predate the public ABI. */
35-
export interface MigrationTasksV1 {
36-
run(name: string): Promise<MigrationJsonValue | undefined>;
37-
}
38-
3950
/** JSON-compatible value persisted in migration progress and result columns. */
4051
export type MigrationJsonValue =
4152
| boolean
@@ -49,9 +60,11 @@ export type MigrationJsonValue =
4960
export interface MigrationContextV1 {
5061
log(message: string): void;
5162
progress: MigrationProgressV1;
52-
sql: Pick<MigrationSqlExecutor, "execute" | "query" | "transaction">;
63+
redis?: MigrationRedisV1;
64+
sql: Pick<MigrationSqlExecutor, "execute" | "query" | "transaction"> & {
65+
db(): unknown;
66+
};
5367
state: MigrationStateV1;
54-
tasks: MigrationTasksV1;
5568
}
5669

5770
/** Isolated TypeScript data migration targeting the v1 ABI. */

packages/junior-migrations/tests/journal.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ describe("resolveMigrations", () => {
5858
);
5959

6060
await expect(resolveMigrations(folder)).rejects.toThrow(
61-
"may only import migration types",
61+
"cannot import application runtime code",
6262
);
6363
});
6464

packages/junior-migrations/tests/runner.test.ts

Lines changed: 21 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ class FakeExecutor implements MigrationSqlExecutor {
2020
readonly rows = new Map<number, StoredRow>();
2121
readonly statements: string[] = [];
2222

23+
db(): undefined {
24+
return undefined;
25+
}
26+
2327
async execute(statement: string, parameters: readonly unknown[] = []) {
2428
const normalized = statement.trim();
2529
if (
@@ -87,6 +91,20 @@ class FakeExecutor implements MigrationSqlExecutor {
8791
}
8892
}
8993

94+
function fakeMigrationState(): MigrationContextV1["state"] {
95+
return {
96+
acquireLock: async () => null,
97+
appendToList: async () => {},
98+
connect: async () => {},
99+
delete: async () => {},
100+
get: async () => undefined,
101+
getList: async () => [],
102+
releaseLock: async () => {},
103+
set: async () => {},
104+
setIfNotExists: async () => true,
105+
};
106+
}
107+
90108
const temporaryDirectories: string[] = [];
91109

92110
async function mixedFolder(): Promise<string> {
@@ -149,16 +167,7 @@ describe("runMigrationJournal", () => {
149167
log: () => {},
150168
progress,
151169
sql: executor,
152-
state: {
153-
appendToList: async () => {},
154-
delete: async () => {},
155-
get: async () => undefined,
156-
getList: async () => [],
157-
set: async () => {},
158-
},
159-
tasks: {
160-
run: async () => undefined,
161-
},
170+
state: fakeMigrationState(),
162171
}),
163172
}),
164173
).resolves.toEqual({ existing: 0, migrated: 3, scanned: 3, skipped: 0 });
@@ -178,16 +187,7 @@ describe("runMigrationJournal", () => {
178187
log: () => {},
179188
progress,
180189
sql: executor,
181-
state: {
182-
appendToList: async () => {},
183-
delete: async () => {},
184-
get: async () => undefined,
185-
getList: async () => [],
186-
set: async () => {},
187-
},
188-
tasks: {
189-
run: async () => undefined,
190-
},
190+
state: fakeMigrationState(),
191191
}),
192192
}),
193193
).resolves.toEqual({ existing: 3, migrated: 0, scanned: 3, skipped: 0 });
@@ -237,16 +237,7 @@ describe("runMigrationJournal", () => {
237237
log: () => {},
238238
progress,
239239
sql: executor,
240-
state: {
241-
appendToList: async () => {},
242-
delete: async () => {},
243-
get: async () => undefined,
244-
getList: async () => [],
245-
set: async () => {},
246-
},
247-
tasks: {
248-
run: async () => undefined,
249-
},
240+
state: fakeMigrationState(),
250241
}),
251242
};
252243

packages/junior-plugin-api/README.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,13 @@ exported TypeScript types and runtime validators are authoritative.
55

66
## Registration
77

8-
Use `defineJuniorPlugin({ manifest, hooks, tasks, migrationTasks, cli, model })`.
8+
Use `defineJuniorPlugin({ manifest, hooks, tasks, cli, model })`.
99
A plugin name is a lowercase identifier and is unique within the enabled app
1010
plugin set.
1111

1212
A plugin may instead be a declarative `plugin.yaml` package when it has no
13-
host-executed hooks, tasks, migration tasks, CLI, or model implementation. Do
14-
not combine an inline manifest with a second YAML definition for the same
15-
plugin.
13+
host-executed hooks, tasks, CLI, or model implementation. Do not combine an
14+
inline manifest with a second YAML definition for the same plugin.
1615

1716
## Manifest
1817

@@ -60,8 +59,8 @@ reports, and other typed hook surfaces exported by this package.
6059

6160
- Packaged migrations create plugin-owned tables through the host migration
6261
runner.
63-
- TypeScript journal entries may invoke explicitly versioned `migrationTasks`;
64-
tasks run only when their owning journal entry is pending.
62+
- TypeScript journal entries contain their complete durable implementation and
63+
execute through the versioned migration capability API.
6564
- Generate migration artifacts from the package schema; do not hand-maintain a
6665
second schema contract.
6766
- Runtime hooks and CLI actions use host-provided `ctx.db`.

packages/junior-plugin-api/src/operations.ts

Lines changed: 1 addition & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { z } from "zod";
2-
import type { PluginContext, PluginLogger } from "./context";
2+
import type { PluginContext } from "./context";
33
import type { Dispatch, DispatchOptions, DispatchResult } from "./dispatch";
44
import { nonBlankStringSchema } from "./schemas";
55
import type { PluginReadState, PluginState } from "./state";
@@ -47,32 +47,6 @@ export interface HeartbeatResult {
4747
dispatchCount?: number;
4848
}
4949

50-
/** Per-record outcome reported by one versioned plugin migration task. */
51-
export type PluginMigrationResult = {
52-
existing: number;
53-
migrated: number;
54-
missing: number;
55-
scanned: number;
56-
skipped?: number;
57-
};
58-
59-
/** Stable host capabilities available to a plugin migration task. */
60-
export interface PluginMigrationContext {
61-
db: unknown;
62-
log: PluginLogger;
63-
state: PluginState;
64-
}
65-
66-
/** Idempotent task invoked only by its owning pending journal entry. */
67-
export interface PluginMigrationTask {
68-
(
69-
context: PluginMigrationContext,
70-
):
71-
| Promise<PluginMigrationResult | undefined>
72-
| PluginMigrationResult
73-
| undefined;
74-
}
75-
7650
export type PluginOperationalTone = "danger" | "good" | "neutral" | "warning";
7751

7852
export interface PluginOperationalMetric {

packages/junior-plugin-api/src/registration.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { PluginCliDefinition } from "./cli";
22
import type { PluginHooks } from "./hooks";
33
import type { PluginManifest } from "./manifest";
4-
import type { PluginMigrationTask } from "./operations";
54
import type { PluginTasks } from "./tasks";
65

76
export interface PluginModelConfig {
@@ -15,8 +14,6 @@ export type PluginRegistrationInput = {
1514
cli?: PluginCliDefinition;
1615
hooks?: PluginHooks;
1716
manifest: PluginManifest;
18-
/** Permanently versioned data tasks referenced by this plugin's migration journal. */
19-
migrationTasks?: Record<string, PluginMigrationTask>;
2017
model?: PluginModelConfig;
2118
packageName?: string;
2219
tasks?: PluginTasks;

packages/junior-scheduler/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,8 @@ operational reporting. Generate schema changes with
4848
`pnpm --filter @sentry/junior-scheduler db:generate`.
4949
Use `pnpm --filter @sentry/junior-scheduler db:generate:data --name <name>`
5050
for a TypeScript data migration in the same journal. New migrations should use
51-
the stable migration capabilities directly; the existing scheduler backfill
52-
uses a versioned task to preserve its pre-journal implementation.
51+
the stable migration capabilities directly and keep their complete
52+
implementation in the migration file.
5353

5454
Follow `../../policies/serverless-background-work.md`,
5555
`../../policies/context-bound-systems.md`, and

0 commit comments

Comments
 (0)