[Upgrade] Fix workspace creation cursor (#19701)

The upgrade migration system required new workspaces to always start
from a workspace command, which was too rigid. When the system was
mid-upgrade within an instance command (IC) segment, workspace creation
would fail or produce inconsistent state.

Instance commands now write upgrade migration rows for **all
active/suspended workspaces** alongside the global row. This means every
workspace has a complete migration history, including instance command
records.

- `InstanceCommandRunnerService` reloads `activeOrSuspendedWorkspaceIds`
immediately before writing records (both success and failure paths) to
mitigate race conditions with concurrent workspace creation.
- `recordUpgradeMigration` in `UpgradeMigrationService` accepts a
discriminated union over `status`, handles `error: unknown` formatting
internally, and writes global + workspace rows in batch.

`getInitialCursorForNewWorkspace` now accepts the last **attempted**
(not just completed) instance command with its status:

- If the IC is `completed` and the next step is a workspace segment →
cursor is set to the last WC of that segment (existing behavior).
- If the IC is `failed` or not the last of its segment → cursor is set
to that IC itself, preserving its status.

This allows workspaces to be created at any point during the upgrade
lifecycle, including mid-IC-segment and after IC failure.

`validateWorkspaceCursorsAreInWorkspaceSegment` accepts workspaces whose
cursor is:
1. Within the current workspace segment, OR
2. At the immediately preceding instance command with `completed` status
(handles the `-w` single-workspace upgrade scenario).

Workspaces with cursors in a previous segment, ahead of the current
segment, or at a preceding IC with `failed` status are rejected.

created empty workspaces to allow testing upgrade with several active
workspaces
This commit is contained in:
Paul Rastoin
2026-04-15 17:51:35 +02:00
committed by prastoin
parent 438d502988
commit e13c9ef46c
18 changed files with 1538 additions and 203 deletions
@@ -62,6 +62,9 @@ export class RunInstanceCommandsCommand extends CommandRunner {
await this.checkWorkspaceVersionSafety(options);
await this.runLegacyPendingTypeOrmMigrations();
const activeOrSuspendedWorkspaceIds =
await this.workspaceVersionService.getActiveOrSuspendedWorkspaceIds();
for (const {
command,
name,
@@ -79,9 +82,6 @@ export class RunInstanceCommandsCommand extends CommandRunner {
}
if (options.includeSlow) {
const hasWorkspaces =
await this.workspaceVersionService.hasActiveOrSuspendedWorkspaces();
for (const {
command,
name,
@@ -90,7 +90,7 @@ export class RunInstanceCommandsCommand extends CommandRunner {
await this.instanceUpgradeService.runSlowInstanceCommand({
command,
name,
skipDataMigration: !hasWorkspaces,
skipDataMigration: activeOrSuspendedWorkspaceIds.length === 0,
});
if (result.status === 'failed') {
@@ -119,10 +119,10 @@ export class RunInstanceCommandsCommand extends CommandRunner {
return;
}
const activeWorkspaceIds =
const activeOrSuspendedWorkspaceIds =
await this.workspaceVersionService.getActiveOrSuspendedWorkspaceIds();
if (activeWorkspaceIds.length === 0) {
if (activeOrSuspendedWorkspaceIds.length === 0) {
return;
}
@@ -141,7 +141,7 @@ export class RunInstanceCommandsCommand extends CommandRunner {
const allAtPreviousVersion =
await this.upgradeMigrationService.areAllWorkspacesAtCommand({
commandName: lastWorkspaceCommand.name,
workspaceIds: activeWorkspaceIds,
workspaceIds: activeOrSuspendedWorkspaceIds,
});
if (!allAtPreviousVersion) {