Filtered upgrade logs when stopping before starting next instance segment (#20078)

# Introduction
In a nutshell, added a more readable logs that relates that when
performing a workspace fitlered workspace you can only browser the
workspace commands sequence

This PR is not a fix, but a log improvement
As before when cross-upgrading a single workspace you would be facing a
`instance` sync barrier error as not all your workspaces would have been
updated

Now logging and early returning instead of letting the guard hard throw

## Tests
Created a dedicated test for instance prevention on filtered upgrade
Standardized `migrationRecordToKey` usage across all upgrade integration
tests suites
This commit is contained in:
Paul Rastoin
2026-04-27 14:12:04 +02:00
committed by GitHub
parent a90895e167
commit f8c1ad5b3c
5 changed files with 217 additions and 219 deletions
@@ -68,6 +68,19 @@ export class UpgradeSequenceRunnerService {
const step = sequence[cursor];
if (step.kind === 'fast-instance' || step.kind === 'slow-instance') {
if (
isDefined(options.workspaceIds) &&
options.workspaceIds.length > 0
) {
this.logger.log(
`Stopping before instance step "${step.name}": ` +
'upgrade was run with workspace filter (-w). ' +
'Instance commands require all workspaces to be aligned.',
);
break;
}
const previousStep = cursor > 0 ? sequence[cursor - 1] : undefined;
if (previousStep?.kind === 'workspace') {
@@ -10,11 +10,12 @@ import {
makeSlowInstance,
makeStep,
makeWorkspace,
migrationRecordToKey,
resetSeedSequenceCounter,
seedInstanceMigration,
seedWorkspaceMigration,
setMockActiveWorkspaceIds,
testGetLatestMigrationForCommand,
testGetExecutedMigrationsInOrder,
WS_1,
WS_2,
} from 'test/integration/upgrade/utils/upgrade-sequence-runner-integration-test.util';
@@ -198,13 +199,15 @@ describe('UpgradeSequenceRunnerService — failing sequence (integration)', () =
}),
).rejects.toThrow('fast command exploded');
const ic2 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Ic2',
});
const executed = await testGetExecutedMigrationsInOrder(context.dataSource);
expect(ic2).toEqual(
expect.objectContaining({ name: 'Ic2', status: 'failed' }),
);
expect(executed.map(migrationRecordToKey)).toStrictEqual([
// Seeds
'Ic1:instance:completed:1',
// Ic2 attempted and failed
'Ic2:instance:failed:1',
]);
});
it('should record failure in DB when a slow instance command fails', async () => {
@@ -238,13 +241,17 @@ describe('UpgradeSequenceRunnerService — failing sequence (integration)', () =
}),
).rejects.toThrow('slow data migration exploded');
const ic2 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Ic2',
});
const executed = await testGetExecutedMigrationsInOrder(context.dataSource);
expect(ic2).toEqual(
expect.objectContaining({ name: 'Ic2', status: 'failed' }),
);
expect(executed.map(migrationRecordToKey)).toStrictEqual([
// Seeds
'Ic1:instance:completed:1',
`Ic1:${WS_1}:completed:1`,
// Ic2 attempted and failed
'Ic2:instance:failed:1',
`Ic2:${WS_1}:failed:1`,
]);
});
it('should abort and report failures when workspace commands fail, without running subsequent instance steps', async () => {
@@ -270,20 +277,15 @@ describe('UpgradeSequenceRunnerService — failing sequence (integration)', () =
expect(report.totalFailures).toBe(1);
expect(report.totalSuccesses).toBe(0);
const ic1 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Ic1',
});
const executed = await testGetExecutedMigrationsInOrder(context.dataSource);
expect(ic1).toBeNull();
expect(executed.map(migrationRecordToKey)).toStrictEqual([
// Seeds
`Wc1:${WS_1}:failed:1`,
const wc1 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Wc1',
workspaceId: WS_1,
});
expect(wc1).toEqual(
expect.objectContaining({ status: 'failed', attempt: 2 }),
);
// Runner retries Wc1, still fails — Ic1 never reached
`Wc1:${WS_1}:failed:2`,
]);
});
it('should abort at workspace failure in a multi-segment sequence with two workspaces starting aligned', async () => {
@@ -337,39 +339,23 @@ describe('UpgradeSequenceRunnerService — failing sequence (integration)', () =
expect(report.totalSuccesses).toBe(1);
expect(report.totalFailures).toBe(1);
// WS_1 succeeded Wc2
const ws1Wc2 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Wc2',
workspaceId: WS_1,
});
const executed = await testGetExecutedMigrationsInOrder(context.dataSource);
expect(ws1Wc2).toEqual(
expect.objectContaining({ name: 'Wc2', status: 'completed' }),
);
expect(executed.map(migrationRecordToKey)).toStrictEqual([
// Seeds
`Wc0:${WS_1}:completed:1`,
`Wc0:${WS_2}:completed:1`,
'Ic1:instance:completed:1',
`Ic1:${WS_1}:completed:1`,
`Ic1:${WS_2}:completed:1`,
`Wc1:${WS_1}:completed:1`,
`Wc1:${WS_2}:completed:1`,
// WS_2 failed Wc2
const ws2Wc2 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Wc2',
workspaceId: WS_2,
});
// WS_1 succeeds Wc2, WS_2 fails Wc2
`Wc2:${WS_1}:completed:1`,
`Wc2:${WS_2}:failed:1`,
expect(ws2Wc2).toEqual(
expect.objectContaining({ name: 'Wc2', status: 'failed' }),
);
// Ic2 never ran — runner aborted at the workspace segment failure
const ic2 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Ic2',
});
expect(ic2).toBeNull();
// Wc3 never ran either
const ws1Wc3 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Wc3',
workspaceId: WS_1,
});
expect(ws1Wc3).toBeNull();
// Ic2 and Wc3 never reached — runner aborted at workspace failure
]);
});
});
@@ -7,11 +7,12 @@ import {
makeFastInstance,
makeSlowInstance,
makeWorkspace,
migrationRecordToKey,
resetSeedSequenceCounter,
seedInstanceMigration,
seedWorkspaceMigration,
setMockActiveWorkspaceIds,
testGetLatestMigrationForCommand,
testGetExecutedMigrationsInOrder,
WS_1,
WS_2,
} from 'test/integration/upgrade/utils/upgrade-sequence-runner-integration-test.util';
@@ -66,23 +67,16 @@ describe('UpgradeSequenceRunnerService — execution (integration)', () => {
options: DEFAULT_OPTIONS,
});
const ic1 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Ic1',
});
const ic2 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Ic2',
});
const ic3 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Ic3',
});
const executed = await testGetExecutedMigrationsInOrder(context.dataSource);
expect(ic1).toEqual(
expect.objectContaining({ status: 'completed', attempt: 1 }),
);
expect(ic2).toEqual(
expect.objectContaining({ status: 'completed', attempt: 1 }),
);
expect(ic3).toEqual(expect.objectContaining({ status: 'completed' }));
expect(executed.map(migrationRecordToKey)).toStrictEqual([
// Seeds
'Ic1:instance:completed:1',
'Ic2:instance:completed:1',
// Runner resumes after Ic2, runs Ic3
'Ic3:instance:completed:1',
]);
});
it('should retry a failed instance command', async () => {
@@ -102,13 +96,16 @@ describe('UpgradeSequenceRunnerService — execution (integration)', () => {
options: DEFAULT_OPTIONS,
});
const ic2 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Ic2',
});
const executed = await testGetExecutedMigrationsInOrder(context.dataSource);
expect(ic2).toEqual(
expect.objectContaining({ name: 'Ic2', status: 'completed', attempt: 2 }),
);
expect(executed.map(migrationRecordToKey)).toStrictEqual([
// Seeds
'Ic1:instance:completed:1',
'Ic2:instance:failed:1',
// Runner retries Ic2
'Ic2:instance:completed:2',
]);
});
it('should resume workspace commands from per-workspace cursors', async () => {
@@ -136,14 +133,17 @@ describe('UpgradeSequenceRunnerService — execution (integration)', () => {
options: DEFAULT_OPTIONS,
});
const wc2 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Wc2',
workspaceId: WS_1,
});
const executed = await testGetExecutedMigrationsInOrder(context.dataSource);
expect(wc2).toEqual(
expect.objectContaining({ name: 'Wc2', status: 'completed' }),
);
expect(executed.map(migrationRecordToKey)).toStrictEqual([
// Seeds
'Ic1:instance:completed:1',
`Ic1:${WS_1}:completed:1`,
`Wc1:${WS_1}:completed:1`,
// Runner resumes after Wc1, runs Wc2
`Wc2:${WS_1}:completed:1`,
]);
});
it('should enforce workspace sync barrier before instance step', async () => {
@@ -162,13 +162,16 @@ describe('UpgradeSequenceRunnerService — execution (integration)', () => {
options: DEFAULT_OPTIONS,
});
const ic1 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Ic1',
});
const executed = await testGetExecutedMigrationsInOrder(context.dataSource);
expect(ic1).toEqual(
expect.objectContaining({ name: 'Ic1', status: 'completed' }),
);
expect(executed.map(migrationRecordToKey)).toStrictEqual([
// Seeds
`Wc1:${WS_1}:completed:1`,
// Barrier passes, runner executes Ic1
'Ic1:instance:completed:1',
`Ic1:${WS_1}:completed:1`,
]);
});
it('should skip data migration for slow instance commands when no workspaces exist', async () => {
@@ -196,13 +199,15 @@ describe('UpgradeSequenceRunnerService — execution (integration)', () => {
expect.objectContaining({ skipDataMigration: true }),
);
const ic2 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Ic2',
});
const executed = await testGetExecutedMigrationsInOrder(context.dataSource);
expect(ic2).toEqual(
expect.objectContaining({ name: 'Ic2', status: 'completed' }),
);
expect(executed.map(migrationRecordToKey)).toStrictEqual([
// Seeds
'Ic1:instance:completed:1',
// Runner runs Ic2 (slow, no workspaces → skip data migration)
'Ic2:instance:completed:1',
]);
});
it('should run data migration for slow instance commands when workspaces exist', async () => {
@@ -233,13 +238,17 @@ describe('UpgradeSequenceRunnerService — execution (integration)', () => {
expect.objectContaining({ skipDataMigration: false }),
);
const ic1 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Ic1',
});
const executed = await testGetExecutedMigrationsInOrder(context.dataSource);
expect(ic1).toEqual(
expect.objectContaining({ name: 'Ic1', status: 'completed' }),
);
expect(executed.map(migrationRecordToKey)).toStrictEqual([
// Seeds
'Ic0:instance:completed:1',
`Ic0:${WS_1}:completed:1`,
// Runner runs Ic1 (slow, workspaces exist → run data migration)
'Ic1:instance:completed:1',
`Ic1:${WS_1}:completed:1`,
]);
});
it('should run workspace commands for multiple workspaces successfully', async () => {
@@ -277,21 +286,20 @@ describe('UpgradeSequenceRunnerService — execution (integration)', () => {
expect(report).toEqual({ totalSuccesses: 2, totalFailures: 0 });
const ws1Wc2 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Wc2',
workspaceId: WS_1,
});
const ws2Wc2 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Wc2',
workspaceId: WS_2,
});
const executed = await testGetExecutedMigrationsInOrder(context.dataSource);
expect(ws1Wc2).toEqual(
expect.objectContaining({ name: 'Wc2', status: 'completed' }),
);
expect(ws2Wc2).toEqual(
expect.objectContaining({ name: 'Wc2', status: 'completed' }),
);
expect(executed.map(migrationRecordToKey)).toStrictEqual([
// Seeds
'Ic1:instance:completed:1',
`Ic1:${WS_1}:completed:1`,
`Ic1:${WS_2}:completed:1`,
`Wc1:${WS_1}:completed:1`,
`Wc1:${WS_2}:completed:1`,
// Both workspaces run Wc2
`Wc2:${WS_1}:completed:1`,
`Wc2:${WS_2}:completed:1`,
]);
});
it('should execute the full sequence from the initial cursor on a fresh run', async () => {
@@ -320,20 +328,19 @@ describe('UpgradeSequenceRunnerService — execution (integration)', () => {
options: DEFAULT_OPTIONS,
});
const ic2 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Ic2',
});
const wc1 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Wc1',
workspaceId: WS_1,
});
const executed = await testGetExecutedMigrationsInOrder(context.dataSource);
expect(ic2).toEqual(
expect.objectContaining({ name: 'Ic2', status: 'completed' }),
);
expect(wc1).toEqual(
expect.objectContaining({ name: 'Wc1', status: 'completed' }),
);
expect(executed.map(migrationRecordToKey)).toStrictEqual([
// Seeds
`Wc0:${WS_1}:completed:1`,
'Ic1:instance:completed:1',
`Ic1:${WS_1}:completed:1`,
// Runner resumes after Ic1, runs Ic2 then Wc1
'Ic2:instance:completed:1',
`Ic2:${WS_1}:completed:1`,
`Wc1:${WS_1}:completed:1`,
]);
});
it('should retry a failed workspace command', async () => {
@@ -363,25 +370,18 @@ describe('UpgradeSequenceRunnerService — execution (integration)', () => {
expect(report.totalFailures).toBe(0);
const wc1 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Wc1',
workspaceId: WS_1,
});
const wc2 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Wc2',
workspaceId: WS_1,
});
const executed = await testGetExecutedMigrationsInOrder(context.dataSource);
expect(wc1).toEqual(
expect.objectContaining({
name: 'Wc1',
status: 'completed',
attempt: 2,
}),
);
expect(wc2).toEqual(
expect.objectContaining({ name: 'Wc2', status: 'completed' }),
);
expect(executed.map(migrationRecordToKey)).toStrictEqual([
// Seeds
'Ic1:instance:completed:1',
`Ic1:${WS_1}:completed:1`,
`Wc1:${WS_1}:failed:1`,
// Runner retries Wc1, then runs Wc2
`Wc1:${WS_1}:completed:2`,
`Wc2:${WS_1}:completed:1`,
]);
});
it('should traverse a multi-segment sequence with sync barriers', async () => {
@@ -412,20 +412,63 @@ describe('UpgradeSequenceRunnerService — execution (integration)', () => {
expect(report.totalFailures).toBe(0);
const ic2 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Ic2',
});
const wc2 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Wc2',
workspaceId: WS_1,
const executed = await testGetExecutedMigrationsInOrder(context.dataSource);
expect(executed.map(migrationRecordToKey)).toStrictEqual([
// Seeds
'Ic1:instance:completed:1',
`Ic1:${WS_1}:completed:1`,
`Wc1:${WS_1}:completed:1`,
// Sync barrier → Ic2, then Wc2
'Ic2:instance:completed:1',
`Ic2:${WS_1}:completed:1`,
`Wc2:${WS_1}:completed:1`,
]);
});
it('should stop before the next instance step when running with workspace filter (-w)', async () => {
const sequence = [
makeFastInstance('Ic1'),
makeWorkspace('Wc1'),
makeWorkspace('Wc2'),
makeFastInstance('Ic2'),
makeWorkspace('Wc3'),
];
setMockActiveWorkspaceIds([WS_1, WS_2]);
await seedInstanceMigration(context.dataSource, {
name: 'Ic1',
status: 'completed',
workspaceIds: [WS_1, WS_2],
});
expect(ic2).toEqual(
expect.objectContaining({ name: 'Ic2', status: 'completed' }),
);
expect(wc2).toEqual(
expect.objectContaining({ name: 'Wc2', status: 'completed' }),
);
const report = await context.runner.run({
sequence,
options: {
...DEFAULT_OPTIONS,
workspaceIds: [WS_1],
},
});
expect(report.totalFailures).toBe(0);
expect(report.totalSuccesses).toBe(1);
const executed = await testGetExecutedMigrationsInOrder(context.dataSource);
expect(executed.map(migrationRecordToKey)).toStrictEqual([
// Seeds
'Ic1:instance:completed:1',
`Ic1:${WS_1}:completed:1`,
`Ic1:${WS_2}:completed:1`,
// Workspace segment: only WS_1 runs (filtered with -w)
`Wc1:${WS_1}:completed:1`,
`Wc2:${WS_1}:completed:1`,
// Ic2 and Wc3 never reached — runner stopped at instance step boundary
]);
});
it('should ignore migration records from inactive workspaces when resolving the global cursor', async () => {
@@ -462,13 +505,17 @@ describe('UpgradeSequenceRunnerService — execution (integration)', () => {
expect(report.totalFailures).toBe(0);
const wc2 = await testGetLatestMigrationForCommand(context.dataSource, {
name: 'Wc2',
workspaceId: WS_1,
});
const executed = await testGetExecutedMigrationsInOrder(context.dataSource);
expect(wc2).toEqual(
expect.objectContaining({ name: 'Wc2', status: 'completed' }),
);
expect(executed.map(migrationRecordToKey)).toStrictEqual([
// Seeds
'Ic1:instance:completed:1',
`Ic1:${WS_1}:completed:1`,
`Wc1:${WS_1}:completed:1`,
`Wc2:${WS_2}:completed:1`,
// WS_2 is inactive, runner only processes WS_1
`Wc2:${WS_1}:completed:1`,
]);
});
});
@@ -95,10 +95,7 @@ describe('UpgradeSequenceRunnerService — workspace segment alignment (integrat
const report = await context.runner.run({
sequence,
options: {
...DEFAULT_OPTIONS,
workspaceIds: [WS_1, WS_2, WS_3],
},
options: DEFAULT_OPTIONS,
});
expect(report.totalFailures).toBe(0);
@@ -504,10 +501,7 @@ describe('UpgradeSequenceRunnerService — workspace segment alignment (integrat
await expect(
context.runner.run({
sequence,
options: {
...DEFAULT_OPTIONS,
workspaceIds: [WS_1, WS_2],
},
options: DEFAULT_OPTIONS,
}),
).rejects.toThrow('Ic1 temporary failure');
@@ -542,10 +536,7 @@ describe('UpgradeSequenceRunnerService — workspace segment alignment (integrat
// Second run — Ic1 retries and succeeds, then WC segment runs for all 3
const report = await context.runner.run({
sequence,
options: {
...DEFAULT_OPTIONS,
workspaceIds: [WS_1, WS_2, WS_3],
},
options: DEFAULT_OPTIONS,
});
expect(report.totalFailures).toBe(0);
@@ -296,45 +296,6 @@ export const seedWorkspaceMigration = async (
}
};
export const testCountMigrationsForCommand = async (
dataSource: DataSource,
{
name,
workspaceId = null,
}: {
name: string;
workspaceId?: string | null;
},
): Promise<number> => {
const rows = await dataSource.query(
`SELECT COUNT(*)::int AS count FROM core."upgradeMigration"
WHERE name = $1 AND ($2::uuid IS NULL AND "workspaceId" IS NULL OR "workspaceId" = $2)`,
[name, workspaceId],
);
return rows[0].count;
};
export const testGetLatestMigrationForCommand = async (
dataSource: DataSource,
{
name,
workspaceId = null,
}: {
name: string;
workspaceId?: string | null;
},
): Promise<{ name: string; status: string; attempt: number } | null> => {
const rows = await dataSource.query(
`SELECT name, status, attempt FROM core."upgradeMigration"
WHERE name = $1 AND ($2::uuid IS NULL AND "workspaceId" IS NULL OR "workspaceId" = $2)
ORDER BY attempt DESC LIMIT 1`,
[name, workspaceId],
);
return rows.length > 0 ? rows[0] : null;
};
export type ExecutedMigrationRecord = {
name: string;
status: string;