Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code afebb034fc fix: generate SDK client archives for pre-existing applications on upgrade
https://sonarly.com/issue/18934?type=bug

Workflow code steps fail with ARCHIVE_NOT_FOUND when the Lambda driver tries to build an SDK layer for applications that were installed before the SDK client generation feature was introduced in PR #18544.

Fix: ## What changed

Modified `SdkClientArchiveService` to auto-generate the SDK client archive when it's missing from file storage, instead of immediately throwing `ARCHIVE_NOT_FOUND`.

### The problem
PR #18544 refactored SDK client provisioning so that `LambdaDriver.ensureSdkLayer()` and `LocalDriver.ensureSdkLayer()` both require a pre-generated `twenty-client-sdk.zip` archive in S3. This archive is generated during workspace creation and app installation, but **not** for applications that were installed before the refactor. A 1.20 upgrade command (`1-20-generate-application-sdk-clients`) exists to backfill, but hasn't run on v1.19.8.

### The fix
In `SdkClientArchiveService.readArchiveStream()`, when the `FILE_NOT_FOUND` error is caught from file storage, instead of throwing `ARCHIVE_NOT_FOUND`, the service now:

1. Looks up the application by `workspaceId` + `universalIdentifier`
2. If the application exists, calls `SdkClientGenerationService.generateSdkClientForApplication()` to generate the archive on-the-fly
3. Retries reading the archive from storage
4. If the application doesn't exist, throws the original `ARCHIVE_NOT_FOUND` error

This is a self-healing approach: the first workflow execution for a pre-existing application will be slower (due to on-the-fly generation), but subsequent executions use the cached archive. The fix covers both the Lambda and Local drivers since they share the same `readArchiveStream` code path.

The `SdkClientGenerationService` is injected via `forwardRef` as a defensive measure since both services live in the same NestJS module (`SdkClientModule`).
2026-03-27 08:08:04 +00:00
@@ -1,4 +1,4 @@
import { Injectable } from '@nestjs/common';
import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { createWriteStream } from 'fs';
@@ -22,6 +22,7 @@ import {
SdkClientException,
SdkClientExceptionCode,
} from 'src/engine/core-modules/sdk-client/exceptions/sdk-client.exception';
import { SdkClientGenerationService } from 'src/engine/core-modules/sdk-client/sdk-client-generation.service';
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
@@ -29,11 +30,15 @@ const SDK_CLIENT_ARCHIVE_NAME = 'twenty-client-sdk.zip';
@Injectable()
export class SdkClientArchiveService {
private readonly logger = new Logger(SdkClientArchiveService.name);
constructor(
private readonly fileStorageService: FileStorageService,
@InjectRepository(ApplicationEntity)
private readonly applicationRepository: Repository<ApplicationEntity>,
private readonly workspaceCacheService: WorkspaceCacheService,
@Inject(forwardRef(() => SdkClientGenerationService))
private readonly sdkClientGenerationService: SdkClientGenerationService,
) {}
async downloadAndExtractToPackage({
@@ -154,13 +159,49 @@ export class SdkClientArchiveService {
error instanceof FileStorageException &&
error.code === FileStorageExceptionCode.FILE_NOT_FOUND
) {
throw new SdkClientException(
`SDK client archive "${SDK_CLIENT_ARCHIVE_NAME}" not found for application "${applicationUniversalIdentifier}" in workspace "${workspaceId}".`,
SdkClientExceptionCode.ARCHIVE_NOT_FOUND,
);
return this.generateAndRetryReadArchiveStream({
workspaceId,
applicationUniversalIdentifier,
});
}
throw error;
}
}
private async generateAndRetryReadArchiveStream({
workspaceId,
applicationUniversalIdentifier,
}: {
workspaceId: string;
applicationUniversalIdentifier: string;
}): Promise<Readable> {
const application = await this.applicationRepository.findOne({
where: { workspaceId, universalIdentifier: applicationUniversalIdentifier },
});
if (!application) {
throw new SdkClientException(
`SDK client archive "${SDK_CLIENT_ARCHIVE_NAME}" not found for application "${applicationUniversalIdentifier}" in workspace "${workspaceId}".`,
SdkClientExceptionCode.ARCHIVE_NOT_FOUND,
);
}
this.logger.log(
`SDK client archive missing for application "${applicationUniversalIdentifier}", generating on-the-fly`,
);
await this.sdkClientGenerationService.generateSdkClientForApplication({
workspaceId,
applicationId: application.id,
applicationUniversalIdentifier,
});
return this.fileStorageService.readFile({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.GeneratedSdkClient,
resourcePath: SDK_CLIENT_ARCHIVE_NAME,
});
}
}