Fix upgrade --start-from-workspace-id (#20116)
# Introduction Prevent using both `--start-from-workspace-id` and `--workspace` When any of the two are being passed we prevent passing to the next instance segment, it would require an upgrade re run even if legit When `--start-from-workspace-id` is passed we filter from all the fetched active or suspended workspace ids and apply equivalent filter as before
This commit is contained in:
+9
@@ -113,6 +113,15 @@ export class UpgradeCommand extends CommandRunner {
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
isDefined(options.workspaceId) &&
|
||||
isDefined(options.startFromWorkspaceId)
|
||||
) {
|
||||
throw new Error(
|
||||
'Cannot use --start-from-workspace-id together with -w/--workspace-id',
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const sequence = this.upgradeSequenceReaderService.getUpgradeSequence();
|
||||
|
||||
|
||||
+35
-9
@@ -69,12 +69,14 @@ export class UpgradeSequenceRunnerService {
|
||||
|
||||
if (step.kind === 'fast-instance' || step.kind === 'slow-instance') {
|
||||
if (
|
||||
isDefined(options.workspaceIds) &&
|
||||
options.workspaceIds.length > 0
|
||||
(isDefined(options.workspaceIds) &&
|
||||
options.workspaceIds.length > 0) ||
|
||||
isDefined(options.startFromWorkspaceId) ||
|
||||
isDefined(options.workspaceCountLimit)
|
||||
) {
|
||||
this.logger.log(
|
||||
`Stopping before instance step "${step.name}": ` +
|
||||
'upgrade was run with workspace filter (-w). ' +
|
||||
'upgrade was run with a workspace filter (-w, --start-from-workspace-id, or --workspace-count-limit). ' +
|
||||
'Instance commands require all workspaces to be aligned.',
|
||||
);
|
||||
|
||||
@@ -303,15 +305,13 @@ export class UpgradeSequenceRunnerService {
|
||||
allActiveOrSuspendedWorkspaceIds: string[];
|
||||
options: ParsedUpgradeCommandOptions;
|
||||
}): Promise<WorkspaceIteratorReport> {
|
||||
const workspaceIds =
|
||||
isDefined(options.workspaceIds) && options.workspaceIds.length > 0
|
||||
? options.workspaceIds
|
||||
: allActiveOrSuspendedWorkspaceIds;
|
||||
const workspaceIds = this.deriveWorkspaceIdsToProcess({
|
||||
allActiveOrSuspendedWorkspaceIds,
|
||||
options,
|
||||
});
|
||||
|
||||
return this.workspaceIteratorService.iterate({
|
||||
workspaceIds,
|
||||
startFromWorkspaceId: options.startFromWorkspaceId,
|
||||
workspaceCountLimit: options.workspaceCountLimit,
|
||||
dryRun: options.dryRun,
|
||||
callback: async (context) => {
|
||||
const workspaceCursor = workspaceCursors.get(context.workspaceId);
|
||||
@@ -337,6 +337,32 @@ export class UpgradeSequenceRunnerService {
|
||||
});
|
||||
}
|
||||
|
||||
private deriveWorkspaceIdsToProcess({
|
||||
allActiveOrSuspendedWorkspaceIds,
|
||||
options,
|
||||
}: {
|
||||
allActiveOrSuspendedWorkspaceIds: string[];
|
||||
options: ParsedUpgradeCommandOptions;
|
||||
}): string[] {
|
||||
if (isDefined(options.workspaceIds) && options.workspaceIds.length > 0) {
|
||||
return options.workspaceIds;
|
||||
}
|
||||
|
||||
let workspaceIds = allActiveOrSuspendedWorkspaceIds;
|
||||
|
||||
if (isDefined(options.startFromWorkspaceId)) {
|
||||
workspaceIds = workspaceIds.filter(
|
||||
(id) => id >= options.startFromWorkspaceId!,
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(options.workspaceCountLimit)) {
|
||||
workspaceIds = workspaceIds.slice(0, options.workspaceCountLimit);
|
||||
}
|
||||
|
||||
return workspaceIds;
|
||||
}
|
||||
|
||||
private enforceWorkspacesCompletedPreviousWorkspaceSegment({
|
||||
sequence,
|
||||
previousWorkspaceStep,
|
||||
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
import {
|
||||
type IntegrationTestContext,
|
||||
createUpgradeSequenceRunnerIntegrationTestModule,
|
||||
DEFAULT_OPTIONS,
|
||||
makeFastInstance,
|
||||
makeWorkspace,
|
||||
migrationRecordToKey,
|
||||
resetSeedSequenceCounter,
|
||||
seedInstanceMigration,
|
||||
setMockActiveWorkspaceIds,
|
||||
testGetExecutedMigrationsInOrder,
|
||||
WS_1,
|
||||
WS_2,
|
||||
WS_3,
|
||||
WS_4,
|
||||
} from 'test/integration/upgrade/utils/upgrade-sequence-runner-integration-test.util';
|
||||
|
||||
// Sorted ASC: WS_1 < WS_2 < WS_3 < WS_4
|
||||
|
||||
describe('UpgradeSequenceRunnerService — startFromWorkspaceId (integration)', () => {
|
||||
let context: IntegrationTestContext;
|
||||
|
||||
beforeAll(async () => {
|
||||
context = await createUpgradeSequenceRunnerIntegrationTestModule();
|
||||
}, 30000);
|
||||
|
||||
afterAll(async () => {
|
||||
await context.dataSource.query('DELETE FROM core."upgradeMigration"');
|
||||
await context.module?.close();
|
||||
await context.dataSource?.destroy();
|
||||
}, 15000);
|
||||
|
||||
beforeEach(async () => {
|
||||
await context.dataSource.query('DELETE FROM core."upgradeMigration"');
|
||||
resetSeedSequenceCounter();
|
||||
setMockActiveWorkspaceIds([]);
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should only process workspaces whose id >= startFromWorkspaceId', async () => {
|
||||
const sequence = [
|
||||
makeFastInstance('Ic1'),
|
||||
makeWorkspace('Wc1'),
|
||||
makeWorkspace('Wc2'),
|
||||
makeFastInstance('Ic2'),
|
||||
];
|
||||
|
||||
setMockActiveWorkspaceIds([WS_1, WS_2, WS_3]);
|
||||
|
||||
await seedInstanceMigration(context.dataSource, {
|
||||
name: 'Ic1',
|
||||
status: 'completed',
|
||||
workspaceIds: [WS_1, WS_2, WS_3],
|
||||
});
|
||||
|
||||
const report = await context.runner.run({
|
||||
sequence,
|
||||
options: {
|
||||
...DEFAULT_OPTIONS,
|
||||
startFromWorkspaceId: WS_2,
|
||||
},
|
||||
});
|
||||
|
||||
expect(report.totalFailures).toBe(0);
|
||||
expect(report.totalSuccesses).toBe(2);
|
||||
|
||||
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`,
|
||||
`Ic1:${WS_3}:completed:1`,
|
||||
|
||||
// Only WS_2 and WS_3 are processed (WS_1 skipped)
|
||||
`Wc1:${WS_2}:completed:1`,
|
||||
`Wc2:${WS_2}:completed:1`,
|
||||
`Wc1:${WS_3}:completed:1`,
|
||||
`Wc2:${WS_3}:completed:1`,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should respect workspaceCountLimit together with startFromWorkspaceId', async () => {
|
||||
const sequence = [makeFastInstance('Ic1'), makeWorkspace('Wc1')];
|
||||
|
||||
setMockActiveWorkspaceIds([WS_1, WS_2, WS_3, WS_4]);
|
||||
|
||||
await seedInstanceMigration(context.dataSource, {
|
||||
name: 'Ic1',
|
||||
status: 'completed',
|
||||
workspaceIds: [WS_1, WS_2, WS_3, WS_4],
|
||||
});
|
||||
|
||||
const report = await context.runner.run({
|
||||
sequence,
|
||||
options: {
|
||||
...DEFAULT_OPTIONS,
|
||||
startFromWorkspaceId: WS_2,
|
||||
workspaceCountLimit: 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`,
|
||||
`Ic1:${WS_3}:completed:1`,
|
||||
`Ic1:${WS_4}:completed:1`,
|
||||
|
||||
// Only WS_2 processed (first after startFrom, limited to 1)
|
||||
`Wc1:${WS_2}:completed:1`,
|
||||
]);
|
||||
});
|
||||
|
||||
it('should stop before instance step when startFromWorkspaceId is set', async () => {
|
||||
const sequence = [
|
||||
makeFastInstance('Ic1'),
|
||||
makeWorkspace('Wc1'),
|
||||
makeWorkspace('Wc2'),
|
||||
makeFastInstance('Ic2'),
|
||||
makeWorkspace('Wc3'),
|
||||
];
|
||||
|
||||
setMockActiveWorkspaceIds([WS_1, WS_2, WS_3]);
|
||||
|
||||
await seedInstanceMigration(context.dataSource, {
|
||||
name: 'Ic1',
|
||||
status: 'completed',
|
||||
workspaceIds: [WS_1, WS_2, WS_3],
|
||||
});
|
||||
|
||||
const report = await context.runner.run({
|
||||
sequence,
|
||||
options: {
|
||||
...DEFAULT_OPTIONS,
|
||||
startFromWorkspaceId: WS_2,
|
||||
},
|
||||
});
|
||||
|
||||
expect(report.totalFailures).toBe(0);
|
||||
expect(report.totalSuccesses).toBe(2);
|
||||
|
||||
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`,
|
||||
`Ic1:${WS_3}:completed:1`,
|
||||
|
||||
// Workspace segment: only WS_2 and WS_3 run
|
||||
`Wc1:${WS_2}:completed:1`,
|
||||
`Wc2:${WS_2}:completed:1`,
|
||||
`Wc1:${WS_3}:completed:1`,
|
||||
`Wc2:${WS_3}:completed:1`,
|
||||
|
||||
// Ic2 and Wc3 never reached — runner stopped at instance step boundary
|
||||
]);
|
||||
});
|
||||
|
||||
it('should process no workspaces when startFromWorkspaceId is greater than all ids', async () => {
|
||||
const sequence = [makeFastInstance('Ic1'), makeWorkspace('Wc1')];
|
||||
|
||||
setMockActiveWorkspaceIds([WS_1, WS_2]);
|
||||
|
||||
await seedInstanceMigration(context.dataSource, {
|
||||
name: 'Ic1',
|
||||
status: 'completed',
|
||||
workspaceIds: [WS_1, WS_2],
|
||||
});
|
||||
|
||||
const report = await context.runner.run({
|
||||
sequence,
|
||||
options: {
|
||||
...DEFAULT_OPTIONS,
|
||||
startFromWorkspaceId: 'ffffffff-ffff-ffff-ffff-ffffffffffff',
|
||||
},
|
||||
});
|
||||
|
||||
expect(report.totalFailures).toBe(0);
|
||||
expect(report.totalSuccesses).toBe(0);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user