github →

New request

#20666: Restart loop after first workspace creation on a fresh self-host install 2.4.0

We'll provision a sandbox, run an agent against the issue, and open a draft PR. You can pull the branch and iterate from there.

Issue
sonarly:high

Short Description / Error Message:

Restart loop after first workspace creation on a fresh self-host install (Some workspace(s) have not completed the last workspace command for 2.3.0)


Hello from the Cloudron Team 👋🏻

While packaging Twenty 2.4.0 for Cloudron we ran into a problem that I am fairly sure also affects any plain self-host setup. The first restart after the first workspace is created puts the server into a restart loop. The migrate step on startup throws:

Instance commands failed: Unable to run instance commands. Some workspace(s) have not completed the last workspace command for 2.3.0 ("2.3.0_DeleteGaugeWidgetsCommand_1798000000000").
Please ensure all workspaces are upgraded to at least the previous version before running migrations.
Use --force to bypass this check (not recommended).

The workspace was just created on 2.4.0 from an empty database, so there is nothing to migrate at all. The check is firing on a healthy install.

Steps to reproduce

  1. Empty database, no core."workspace" rows, no core."upgradeMigration" rows.
  2. yarn database:init:prod. Completes fine.
  3. Start the server, sign up, first workspace gets created.
  4. Restart the server, which re-runs yarn database:migrate:prod on startup.
  5. The migrate step throws the error above and the process exits. If the container has auto-restart you now have a restart loop.

Adding --force to database:migrate:prod makes it go away, but the check is wrong, not catching something real.

What is going on

The safety check is here:

https://github.com/twentyhq/twenty/blob/6b3064e2bae39f41265104a648f5b0f73ff18eb4/packages/twenty-server/src/database/commands/run-instance-commands.command.ts#L108-L152

It takes the last entry of TWENTY_PREVIOUS_VERSIONS:

https://github.com/twentyhq/twenty/blob/6b3064e2bae39f41265104a648f5b0f73ff18eb4/packages/twenty-server/src/engine/core-modules/upgrade/constants/twenty-previous-versions.constant.ts#L10-L19

asks the registry for the last workspace command of that version (2.3.0_DeleteGaugeWidgetsCommand_1798000000000 in our case), and then asks areAllWorkspacesAtCommand({ commandName, workspaceIds }) whether every active workspace has a core."upgradeMigration" row with exactly that name and status='completed'. If not, throw.

On the workspace creation side:

https://github.com/twentyhq/twenty/blob/6b3064e2bae39f41265104a648f5b0f73ff18eb4/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts#L396-L437

activateAndInitializeUpgradeState() writes only one row via markAsWorkspaceInitial. The cursor it uses comes from getInitialCursorForNewWorkspace:

https://github.com/twentyhq/twenty/blob/6b3064e2bae39f41265104a648f5b0f73ff18eb4/packages/twenty-server/src/engine/core-modules/upgrade/services/upgrade-sequence-reader.service.ts#L165-L194

For a 2.4.0 fresh install this returns the last workspace command of the 2.4.0 segment (2.4.0_MigrateToBillingV2Command_1797000001000). The 2.3.0 segment's last workspace command is never written. The cursor based runner (getPendingWorkspaceCommands) is fine with that, it treats anything before the cursor as done. But areAllWorkspacesAtCommand is just a row existence query and does not know about cursors.

So:

  • the workspace's only init row is the last 2.4.0 workspace command
  • the safety check looks for the last 2.3.0 workspace command
  • the row does not exist
  • check throws, startup fails, restart loop

What the DB actually looks like

core."upgradeMigration" for the affected workspace, ordered by createdAt desc:

                              name                              |  status   | is_instance | isInitial
----------------------------------------------------------------+-----------+-------------+-----------
 2.4.0_MigrateToBillingV2Command_1797000001000                  | completed |      f      |     t
 2.4.0_AddApplicationIdToPublicDomainFastInstanceCommand_...    | completed |      t      |     f
 ...                                                            |           |             |
 2.3.0_RemoveUserDefaultAvatarUrlFastInstanceCommand_...        | completed |      t      |     f
 ...

All instance commands are there. The only workspace-scoped row is the single isInitial=true cursor at 2.4.0_MigrateToBillingV2Command. No row for 2.3.0_DeleteGaugeWidgetsCommand_1798000000000, which is exactly what the check wants.

This is still present in 2.4.1, 2.4.2, 2.5.0, 2.5.1, 2.5.2

I checked the relevant files in every 2.4.x and 2.5.x tag, hoping a newer release would fix it. It does not.

  • run-instance-commands.command.ts has the same content in all six tags. The safety check is unchanged.
  • upgrade-sequence-reader.service.ts has the same content in all six tags. getInitialCursorForNewWorkspace still seeds exactly one row.
  • workspace.service.ts was touched in 2.5.0, but the change is around enqueueSdkClientGenerationForWorkspace. activateAndInitializeUpgradeState is the same.
  • upgrade-migration.service.ts was touched in 2.5.0 and 2.5.1, but the changes are a new getInferredVersion helper, batching saves in chunks of 1000, and a raw SQL rewrite of getWorkspaceLastAttemptedCommandName. markAsWorkspaceInitial and areAllWorkspacesAtCommand behave the same.

Also, TWENTY_PREVIOUS_VERSIONS simply grows each release, so the same trap re-appears one version later. A workspace created fresh on 2.5.0 will fail on first restart against the missing 2.4.0_<lastWorkspaceCommand> row. A 2.5.1 fresh workspace fails against 2.5.0_<lastWorkspaceCommand>. And so on.

Suggested fix

Two options, either is fine:

  1. In activateAndInitializeUpgradeState, also write an initial row for the previous version's last workspace command, when one exists. That makes the existing check accurate.
  2. In checkWorkspaceVersionSafety, stop looking for an exact row. Use the workspace's latest cursor (you already have getWorkspaceLastAttemptedCommandName) and accept anything at or after the previous version's last workspace command in the upgrade sequence. That matches how getPendingWorkspaceCommands already reasons.

Option 2 is more general.

Workaround

For anyone hitting this in the meantime: add --force to database:migrate:prod. The database:init:prod script in package.json already does this (yarn database:migrate:prod --force --include-slow), so it is not a new flag and not unsafe in this scenario, but it should not be needed on every restart of a healthy server.

Thanks for the great work on Twenty.

Assessmentadvisory
bug●● medium85% confidence

Startup instance command check incorrectly requires all workspaces to complete a 2.3 command even on fresh installs with no prior migration state.

Likely files
  • packages/twenty-server/src/database/typeorm/core/migrations/common
  • packages/twenty-server/src/engine/workspace-manager/workspace-migration
  • packages/twenty-server/src/engine/core-modules/upgrade/upgrade.service.ts
  • packages/twenty-server/src/engine/workspace-manager/commands
Create the request

This opens a fresh agent run and a draft PR for issue #20666.

Cancel