From c4a79c50c39aace4fdd56cdb07ed3f5881519282 Mon Sep 17 00:00:00 2001 From: martmull Date: Wed, 29 Jul 2026 18:30:00 +0200 Subject: [PATCH] Install pre-installed apps in a dedicated job after the workspace upgrade cursor is written (#23517) ## Problem Application registrations flagged `isPreInstalled: true` were not installed on newly created workspaces. The call was wired in, but it ran too early. `activateWorkspace` invoked `preInstalledAppsService.installOnWorkspace` from inside `prefillCreatedWorkspaceRecords`, which runs **before** `activateAndInitializeUpgradeState`. The install path validates app/workspace version compatibility: - `ApplicationInstallService.runInstall` reads `engines.twenty` from the app's `package.json` and calls `validateWorkspaceCompatibility` - `ApplicationVersionValidationService.validateWorkspaceCompatibility` resolves the workspace version through `UpgradeStatusService.getWorkspaceCompletedVersion` - that reads the workspace's upgrade-migration cursor, which is only written by `markAsWorkspaceInitial` inside `activateAndInitializeUpgradeState` During creation the workspace has no cursor row yet, so `getWorkspaceCompletedVersion` returns `null`, the install throws `INVALID_WORKSPACE_VERSION`, and the failure is swallowed twice over: `PreInstalledAppsService` logs per-app failures without rethrowing, and `activateWorkspace` wraps the whole call in non-critical error handling. The workspace comes up silently missing its apps. This affects most real apps, since they pin `engines.twenty`: `fireflies`, `last-contact`, `people-data-labs`, `call-recorder`, `postcard`, `self-hosting`, `twenty-partners` (`>=2.23.0`) and `exa`, `real-estate` (`>=2.19.0`). Only apps with no `engines.twenty` installed successfully. The same interaction is already documented in `2-23-workspace-command-1784565137000-upgrade-people-data-labs-application.command.ts`, which works around it with `skipWorkspaceCompatibilityCheck: true`. ## Changes - Added `InstallPreInstalledAppsJob` on the workspace queue, mirroring the existing `InstallOnboardingAppsJob`. - `activateWorkspace` now enqueues that job instead of installing synchronously, so workspace creation no longer blocks on package fetching and manifest application. - The enqueue happens after `activateAndInitializeUpgradeState` writes the upgrade cursor, so the compatibility check has a workspace version to resolve by the time the worker picks the job up. ## Notes Workspaces created before this fix can be repaired with the existing `install-pre-installed-apps` backfill command, which is idempotent. --- .../pre-installed-apps.service.spec.ts | 8 ++++++ ...nstall-pre-installed-apps.job-constants.ts | 5 ++++ .../jobs/install-pre-installed-apps.job.ts | 28 +++++++++++++++++++ .../pre-installed-apps.service.ts | 17 +++++++++++ .../core-modules/message-queue/jobs.module.ts | 4 +++ .../workspace/services/workspace.service.ts | 10 +++++-- 6 files changed, 70 insertions(+), 2 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/application/pre-installed-apps/jobs/install-pre-installed-apps.job-constants.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/pre-installed-apps/jobs/install-pre-installed-apps.job.ts diff --git a/packages/twenty-server/src/engine/core-modules/application/pre-installed-apps/__tests__/pre-installed-apps.service.spec.ts b/packages/twenty-server/src/engine/core-modules/application/pre-installed-apps/__tests__/pre-installed-apps.service.spec.ts index 9f554af324..19cf443531 100644 --- a/packages/twenty-server/src/engine/core-modules/application/pre-installed-apps/__tests__/pre-installed-apps.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/application/pre-installed-apps/__tests__/pre-installed-apps.service.spec.ts @@ -9,6 +9,8 @@ import { ApplicationException, ApplicationExceptionCode, } from 'src/engine/core-modules/application/application.exception'; +import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; +import { getQueueToken } from 'src/engine/core-modules/message-queue/utils/get-queue-token.util'; describe('PreInstalledAppsService', () => { let service: PreInstalledAppsService; @@ -18,11 +20,13 @@ describe('PreInstalledAppsService', () => { findOne: jest.Mock; }; let workspaceIteratorService: { iterate: jest.Mock }; + let messageQueueService: { add: jest.Mock }; beforeEach(async () => { applicationInstallService = { installApplication: jest.fn() }; applicationRegistrationRepository = { find: jest.fn(), findOne: jest.fn() }; workspaceIteratorService = { iterate: jest.fn() }; + messageQueueService = { add: jest.fn() }; const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -39,6 +43,10 @@ describe('PreInstalledAppsService', () => { provide: WorkspaceIteratorService, useValue: workspaceIteratorService, }, + { + provide: getQueueToken(MessageQueue.workspaceQueue), + useValue: messageQueueService, + }, ], }).compile(); diff --git a/packages/twenty-server/src/engine/core-modules/application/pre-installed-apps/jobs/install-pre-installed-apps.job-constants.ts b/packages/twenty-server/src/engine/core-modules/application/pre-installed-apps/jobs/install-pre-installed-apps.job-constants.ts new file mode 100644 index 0000000000..87e2b40111 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/pre-installed-apps/jobs/install-pre-installed-apps.job-constants.ts @@ -0,0 +1,5 @@ +export const INSTALL_PRE_INSTALLED_APPS_JOB_NAME = 'InstallPreInstalledAppsJob'; + +export type InstallPreInstalledAppsJobData = { + workspaceId: string; +}; diff --git a/packages/twenty-server/src/engine/core-modules/application/pre-installed-apps/jobs/install-pre-installed-apps.job.ts b/packages/twenty-server/src/engine/core-modules/application/pre-installed-apps/jobs/install-pre-installed-apps.job.ts new file mode 100644 index 0000000000..1e3ff209c7 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/application/pre-installed-apps/jobs/install-pre-installed-apps.job.ts @@ -0,0 +1,28 @@ +import { Logger } from '@nestjs/common'; + +import { + INSTALL_PRE_INSTALLED_APPS_JOB_NAME, + type InstallPreInstalledAppsJobData, +} from 'src/engine/core-modules/application/pre-installed-apps/jobs/install-pre-installed-apps.job-constants'; +import { PreInstalledAppsService } from 'src/engine/core-modules/application/pre-installed-apps/pre-installed-apps.service'; +import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; +import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; +import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; + +@Processor(MessageQueue.workspaceQueue) +export class InstallPreInstalledAppsJob { + private readonly logger = new Logger(InstallPreInstalledAppsJob.name); + + constructor( + private readonly preInstalledAppsService: PreInstalledAppsService, + ) {} + + @Process(INSTALL_PRE_INSTALLED_APPS_JOB_NAME) + async handle({ workspaceId }: InstallPreInstalledAppsJobData): Promise { + this.logger.log( + `Installing pre-installed apps on workspace ${workspaceId}`, + ); + + await this.preInstalledAppsService.installOnWorkspace(workspaceId); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/application/pre-installed-apps/pre-installed-apps.service.ts b/packages/twenty-server/src/engine/core-modules/application/pre-installed-apps/pre-installed-apps.service.ts index fdd7b5d14e..9242a1d409 100644 --- a/packages/twenty-server/src/engine/core-modules/application/pre-installed-apps/pre-installed-apps.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/pre-installed-apps/pre-installed-apps.service.ts @@ -10,6 +10,13 @@ import { ApplicationException, ApplicationExceptionCode, } from 'src/engine/core-modules/application/application.exception'; +import { + INSTALL_PRE_INSTALLED_APPS_JOB_NAME, + type InstallPreInstalledAppsJobData, +} from 'src/engine/core-modules/application/pre-installed-apps/jobs/install-pre-installed-apps.job-constants'; +import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; +import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; +import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; @Injectable() export class PreInstalledAppsService { @@ -20,8 +27,18 @@ export class PreInstalledAppsService { @InjectRepository(ApplicationRegistrationEntity) private readonly applicationRegistrationRepository: Repository, private readonly workspaceIteratorService: WorkspaceIteratorService, + @InjectMessageQueue(MessageQueue.workspaceQueue) + private readonly messageQueueService: MessageQueueService, ) {} + async enqueueInstallOnWorkspace(workspaceId: string): Promise { + await this.messageQueueService.add( + INSTALL_PRE_INSTALLED_APPS_JOB_NAME, + { workspaceId }, + { id: `${INSTALL_PRE_INSTALLED_APPS_JOB_NAME}-${workspaceId}` }, + ); + } + // Per-app failures are logged but never block the other installs — // `ApplicationInstallService` holds a per-app cache lock so parallel // installs are safe. diff --git a/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts b/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts index 9e657a94e2..2d7e5c1085 100644 --- a/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts +++ b/packages/twenty-server/src/engine/core-modules/message-queue/jobs.module.ts @@ -16,6 +16,8 @@ import { ApplicationInstallModule } from 'src/engine/core-modules/application/ap import { ApplicationRegistrationModule } from 'src/engine/core-modules/application/application-registration/application-registration.module'; import { ApplicationUpgradeModule } from 'src/engine/core-modules/application/application-upgrade/application-upgrade.module'; import { UpgradeApplicationsJob } from 'src/engine/core-modules/application/jobs/upgrade-applications.job'; +import { InstallPreInstalledAppsJob } from 'src/engine/core-modules/application/pre-installed-apps/jobs/install-pre-installed-apps.job'; +import { PreInstalledAppsModule } from 'src/engine/core-modules/application/pre-installed-apps/pre-installed-apps.module'; import { InstallOnboardingAppsJob } from 'src/engine/core-modules/onboarding/jobs/install-onboarding-apps.job'; import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module'; import { EmailSenderJob } from 'src/engine/core-modules/email/email-sender.job'; @@ -95,6 +97,7 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module'; ApplicationInstallModule, ApplicationRegistrationModule, ApplicationUpgradeModule, + PreInstalledAppsModule, OnboardingModule, BillingReminderModule, ], @@ -113,6 +116,7 @@ import { WorkflowModule } from 'src/modules/workflow/workflow.module'; GenerateSdkClientJob, UpgradeApplicationsJob, InstallOnboardingAppsJob, + InstallPreInstalledAppsJob, ], }) export class JobsModule { diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts index fcfc7864ed..05d175db04 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts @@ -411,6 +411,8 @@ export class WorkspaceService { await this.activateAndInitializeUpgradeState({ workspaceId: workspace.id, }); + + await this.enqueuePreInstalledAppsInstallation(workspace.id); } catch (error) { await this.workspaceRepository.update(workspace.id, { activationStatus: WorkspaceActivationStatus.PENDING_CREATION, @@ -952,12 +954,16 @@ export class WorkspaceService { ); this.exceptionHandlerService.captureExceptions([error as Error]); } + } + private async enqueuePreInstalledAppsInstallation( + workspaceId: string, + ): Promise { try { - await this.preInstalledAppsService.installOnWorkspace(workspaceId); + await this.preInstalledAppsService.enqueueInstallOnWorkspace(workspaceId); } catch (error) { this.logger.error( - `Non-critical: failed to install pre-installed apps for workspace ${workspaceId}`, + `Non-critical: failed to enqueue pre-installed apps installation for workspace ${workspaceId}`, error, ); this.exceptionHandlerService.captureExceptions([error as Error]);