b91c2a6457
## Problem closes https://github.com/twentyhq/twenty/issues/23210 Self-hosted instances on 2.23.x fail their workspace upgrade with: ``` column ApplicationEntity__ApplicationEntity_applicationRegistration.logoFileId does not exist at UpgradePeopleDataLabsApplicationCommand.runOnWorkspace ``` The `2-21` instance command that adds `core."applicationRegistration"."logoFileId"` was merged ~20 minutes after the 2.22 version bump (PR #22827, `94192a2164`), so it first shipped in 2.22 while registered under `@RegisteredInstanceCommand('2.21.0', ...)`. The upgrade runner resolves its start position from the last recorded command and only moves forward. Any instance that had already run a 2.21.x binary has its cursor past that slot, so the command is skipped permanently and the column is never created. `UpgradeAwareEntityMetadataAdapter` decides column visibility positionally (`index < currentCursor`), not by whether the command actually ran, so it keeps `logoFileId` in the SELECT list and the instance reports "Up to date" while the column is absent. **Affected:** instances that ran 2.21.x, then upgraded to >= 2.22. Instances that went from <= 2.20 straight to >= 2.22 replayed the full sequence and are fine. `logoFileId` is populated lazily by design (NULL is a supported state), so no backfill is added. ## Changes **1. Idempotent DDL guard in the failing workspace command** `2-23-workspace-command-...-upgrade-people-data-labs-application.command.ts` now ensures the column exists at the top of `runOnWorkspace`, before the `findOne` that crashes on affected instances. It uses the core `DataSource` (`@InjectDataSource()`) because `core."applicationRegistration"` is instance-global, guards with a per-process boolean in addition to the SQL-level `IF NOT EXISTS`, and copies the full statement list (column + unique + FK constraints) verbatim from the 2.21 command. In dry-run it probes `information_schema.columns` and returns instead of running the crashing query. **2. Fast instance command in 2.23** New `2-23-instance-command-fast-1784823473532-add-logo-file-id-to-application-registration.ts`, registered at the end of the 2.23 fast segment (highest timestamp), running the same idempotent DDL. This covers the normal 2.22 -> 2.23 path and, critically, instances with zero provisioned workspaces where the workspace command body never executes. The shared DDL lives in `2-23/utils/ensure-application-registration-logo-file-id-column.util.ts` so both paths stay byte-for-byte identical. Class name follows the `Early2_4` / `Early2_5` precedent to avoid colliding with the 2.21 command. The fix lives entirely in 2.23: instances stuck at the failing workspace command retry it every run, and 2.22 -> 2.24 jumps still replay the 2.23 segment. ## Ops note Instances failing right now can be unblocked immediately by running the same `ALTER TABLE` block by hand against their core database (byte-for-byte what the command does). Worth including in the 2.23 patch release note. ## Verification - New fast instance command re-slotted last in the 2.23 fast segment (timestamp `1784823473532` > current max `1784659343818`). - Manual repro path: boot `twentycrm/twenty:v2.21`, seed, stop, run `upgrade` from this branch, assert the column exists and `upgrade:status` reports 0 failed. The default v1.22 baseline does not reproduce it (replays from cursor 0). --- _Generated by [Claude Code](https://claude.ai/code/session_01YAuDR585cx7FyAKoiT32j3)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/23215?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. --> --------- Co-authored-by: Paul Rastoin <paul.rastoin@gmail.com>
153 lines
6.1 KiB
TypeScript
153 lines
6.1 KiB
TypeScript
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
|
|
|
import { Command } from 'nest-commander';
|
|
import * as semver from 'semver';
|
|
import { isDefined } from 'twenty-shared/utils';
|
|
import { DataSource, Repository } from 'typeorm';
|
|
|
|
import { ProvisionedWorkspaceCommandRunner } from 'src/database/commands/command-runners/provisioned-workspace.command-runner';
|
|
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
|
|
import { type RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspace.command-runner';
|
|
import { ensureApplicationRegistrationLogoFileIdColumn } from 'src/database/commands/upgrade-version-command/2-23/utils/ensure-application-registration-logo-file-id-column.util';
|
|
import { ApplicationUpgradeService } from 'src/engine/core-modules/application/application-upgrade/application-upgrade.service';
|
|
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
|
import { RegisteredWorkspaceCommand } from 'src/engine/core-modules/upgrade/decorators/registered-workspace-command.decorator';
|
|
|
|
// packages/twenty-apps/public/people-data-labs APPLICATION_UNIVERSAL_IDENTIFIER
|
|
const PEOPLE_DATA_LABS_APPLICATION_UNIVERSAL_IDENTIFIER =
|
|
'4a1178c1-3535-4a47-b592-231d3216b36f';
|
|
|
|
// First version whose views pin the re-derived name-free system relation
|
|
// field universal identifiers (introduced in 1.0.7) AND whose front components
|
|
// bundle React 19 to match the twenty-sdk runtime (fixed in 1.0.9).
|
|
const PEOPLE_DATA_LABS_TARGET_VERSION = '1.0.9';
|
|
|
|
@RegisteredWorkspaceCommand('2.23.0', 1784565137000)
|
|
@Command({
|
|
name: 'upgrade:2-23:upgrade-people-data-labs-application',
|
|
description:
|
|
'Upgrade the people-data-labs application to 1.0.9 right after the system relation field universal identifier backfill, so its views reference the re-derived name-free identifiers instead of the stale pre-2.23 ones and its front components run on React 19. Workspaces already at or above 1.0.9 are left untouched.',
|
|
})
|
|
export class UpgradePeopleDataLabsApplicationCommand extends ProvisionedWorkspaceCommandRunner {
|
|
// Instance-global DDL: run once per process, not per workspace.
|
|
private hasEnsuredLogoFileIdColumn = false;
|
|
|
|
constructor(
|
|
protected readonly workspaceIteratorService: WorkspaceIteratorService,
|
|
private readonly applicationUpgradeService: ApplicationUpgradeService,
|
|
@InjectRepository(ApplicationEntity)
|
|
private readonly applicationRepository: Repository<ApplicationEntity>,
|
|
@InjectDataSource()
|
|
private readonly coreDataSource: DataSource,
|
|
) {
|
|
super(workspaceIteratorService);
|
|
}
|
|
|
|
override async runOnWorkspace({
|
|
workspaceId,
|
|
options,
|
|
}: RunOnWorkspaceArgs): Promise<void> {
|
|
const isDryRun = options.dryRun ?? false;
|
|
|
|
// The 2.21 logoFileId command was skipped on instances that ran a 2.21
|
|
// binary, so the column is absent and the findOne below crashes on it.
|
|
// Repair it here since this is the command that breaks.
|
|
if (!this.hasEnsuredLogoFileIdColumn) {
|
|
if (isDryRun) {
|
|
const columnExists = await this.logoFileIdColumnExists();
|
|
|
|
if (!columnExists) {
|
|
this.logger.log(
|
|
'Would repair the missing core."applicationRegistration"."logoFileId" column',
|
|
);
|
|
|
|
return;
|
|
}
|
|
} else {
|
|
await ensureApplicationRegistrationLogoFileIdColumn((sql) =>
|
|
this.coreDataSource.query(sql),
|
|
);
|
|
this.hasEnsuredLogoFileIdColumn = true;
|
|
}
|
|
}
|
|
|
|
const application = await this.applicationRepository.findOne({
|
|
where: {
|
|
workspaceId,
|
|
universalIdentifier: PEOPLE_DATA_LABS_APPLICATION_UNIVERSAL_IDENTIFIER,
|
|
},
|
|
relations: ['applicationRegistration'],
|
|
});
|
|
|
|
if (!isDefined(application)) {
|
|
return;
|
|
}
|
|
|
|
const applicationRegistration = application.applicationRegistration;
|
|
|
|
if (!isDefined(applicationRegistration)) {
|
|
this.logger.warn(
|
|
`people-data-labs is installed but has no application registration, skipping upgrade for workspace ${workspaceId}`,
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
const installedVersion = semver.valid(application.version);
|
|
|
|
if (
|
|
isDefined(installedVersion) &&
|
|
semver.gte(installedVersion, PEOPLE_DATA_LABS_TARGET_VERSION)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
if (isDryRun) {
|
|
this.logger.log(
|
|
`[DRY RUN] Would upgrade people-data-labs from ${application.version} to ${PEOPLE_DATA_LABS_TARGET_VERSION} for workspace ${workspaceId}`,
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
try {
|
|
await this.applicationUpgradeService.upgradeApplication({
|
|
appRegistrationId: applicationRegistration.id,
|
|
targetVersion: PEOPLE_DATA_LABS_TARGET_VERSION,
|
|
workspaceId,
|
|
// 1.0.9 pins engines.twenty >=2.23.0, but this command runs as part of
|
|
// the 2.23 workspace upgrade itself, so the workspace has not yet been
|
|
// marked as completing 2.23 and the compatibility check would reject
|
|
// the install. The server is already on 2.23, so skip the check here.
|
|
skipWorkspaceCompatibilityCheck: true,
|
|
});
|
|
|
|
this.logger.log(
|
|
`Upgraded people-data-labs from ${application.version} to ${PEOPLE_DATA_LABS_TARGET_VERSION} for workspace ${workspaceId}`,
|
|
);
|
|
} catch (error) {
|
|
// Non-fatal: the app stays functional at runtime (view fields reference
|
|
// fields by database id); only a manifest re-sync of the stale version
|
|
// would fail, and the upgrade can be retried from the UI.
|
|
this.logger.error(
|
|
`Failed to upgrade people-data-labs for workspace ${workspaceId}: ${error}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
private async logoFileIdColumnExists(): Promise<boolean> {
|
|
// pg_attribute scoped to the single table, not the instance-wide
|
|
// information_schema.columns view, which is slow on many-tenant instances.
|
|
const rows = await this.coreDataSource.query(
|
|
`SELECT EXISTS (
|
|
SELECT 1 FROM pg_attribute
|
|
WHERE attrelid = to_regclass('core."applicationRegistration"')
|
|
AND attname = 'logoFileId'
|
|
AND NOT attisdropped
|
|
) AS "exists"`,
|
|
);
|
|
|
|
return rows[0]?.exists === true;
|
|
}
|
|
}
|