feat(server): add application:install command (#23430)
Adds `application:install` to install an application on workspaces that do not have it yet, and moves both application commands onto `WorkspaceIteratorService`. ### Behavior - Iterates provisioned workspaces through `WorkspaceIteratorService` (workspace id resolution, workspace context, per-workspace success/fail report), or the ones passed with `-w`. - Workspaces where the application is already installed are skipped, with a log line pointing at `application:upgrade`. This command never upgrades. - Installed workspaces are detected by `universalIdentifier`, the same identity `ApplicationInstallService` uses to tell a fresh install from a version upgrade, checked per workspace on the `(universalIdentifier, workspaceId)` unique index. - `--workspace-count-limit` caps both the iterator's own selection and an explicitly targeted `-w` list. - Fails fast for `LOCAL` and `OAUTH_ONLY` registrations, which have no code artifacts to install. - Per-workspace failures are collected in the iterator report and never abort the run; the command ends with an installed / skipped / failed summary. ### Options | Flag | Description | | --- | --- | | `-u, --application-registration-universal-identifier` | Application registration universal identifier (required) | | `-w, --workspace-id` | Target a specific workspace, repeatable | | `--workspace-count-limit` | Cap the number of workspaces to iterate over (max 50) | | `-d, --dry-run` | Print the workspaces that would be installed without installing | | `-y, --yes` | Skip the confirmation prompt | ### Example ``` yarn command:prod application:install -u UNIVERSAL_IDENTIFIER --dry-run ``` ### Changes to application:upgrade - `ApplicationUpgradeService.upgradeApplications` iterates through `WorkspaceIteratorService` and returns its report, replacing the hand-rolled parallel batching. - `--batch-size` dropped from the command, and `batchSize` dropped from the service and from `UpgradeApplicationsJobData`, since `iterate()` is sequential. - `parseBoundedPositiveInteger` moved to `src/database/commands/utils/` and is shared by both commands. ### Files - `application-install/commands/install-application.command.ts` (new) - `src/database/commands/utils/parse-bounded-positive-integer.util.ts` (new) - `application-upgrade/application-upgrade.service.ts`, `application-upgrade/commands/upgrade-application.command.ts`, `jobs/upgrade-applications.job*`: iterator instead of batching - `application-install.module.ts` / `application-upgrade.module.ts`: register the command, wire `WorkspaceIteratorModule` - `database-command.module.ts`: import `ApplicationInstallModule` so the command is discovered by the CLI ### Testing - `npx jest src/engine/core-modules/application` (32 suites, 181 tests passing) - `npx nx typecheck twenty-server` - `oxlint --type-aware` and `oxfmt` on the changed files
This commit is contained in:
+13
-2
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
|
||||
import { CacheLockModule } from 'src/engine/core-modules/cache-lock/cache-lock.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
@@ -11,6 +12,8 @@ import { ApplicationPackageModule } from 'src/engine/core-modules/application/ap
|
||||
import { MarketplaceModule } from 'src/engine/core-modules/application/application-marketplace/marketplace.module';
|
||||
import { ApplicationInstallResolver } from 'src/engine/core-modules/application/application-install/application-install.resolver';
|
||||
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
|
||||
import { InstallApplicationCommand } from 'src/engine/core-modules/application/application-install/commands/install-application.command';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
|
||||
import { LogicFunctionModule } from 'src/engine/core-modules/logic-function/logic-function.module';
|
||||
import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module';
|
||||
@@ -20,7 +23,10 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([ApplicationRegistrationEntity]),
|
||||
TypeOrmModule.forFeature([
|
||||
ApplicationEntity,
|
||||
ApplicationRegistrationEntity,
|
||||
]),
|
||||
ApplicationModule,
|
||||
ApplicationRegistrationModule,
|
||||
ApplicationManifestModule,
|
||||
@@ -34,8 +40,13 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
PermissionsModule,
|
||||
FileStorageModule,
|
||||
WorkspaceCacheModule,
|
||||
WorkspaceIteratorModule,
|
||||
],
|
||||
providers: [
|
||||
ApplicationInstallResolver,
|
||||
ApplicationInstallService,
|
||||
InstallApplicationCommand,
|
||||
],
|
||||
providers: [ApplicationInstallResolver, ApplicationInstallService],
|
||||
exports: [ApplicationInstallService],
|
||||
})
|
||||
export class ApplicationInstallModule {}
|
||||
|
||||
+281
@@ -0,0 +1,281 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommandRunner, Option } from 'nest-commander';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceIteratorService } from 'src/database/commands/command-runners/workspace-iterator.service';
|
||||
import { CommandLogger } from 'src/database/commands/logger';
|
||||
import { askCommandConfirmation } from 'src/database/commands/utils/ask-command-confirmation.util';
|
||||
import { parseBoundedPositiveInteger } from 'src/database/commands/utils/parse-bounded-positive-integer.util';
|
||||
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
|
||||
type InstallApplicationCommandOptions = {
|
||||
applicationRegistrationUniversalIdentifier: string;
|
||||
workspaceId?: Set<string>;
|
||||
workspaceCountLimit?: number;
|
||||
dryRun?: boolean;
|
||||
yes?: boolean;
|
||||
};
|
||||
|
||||
const MAX_WORKSPACE_COUNT_LIMIT = 50;
|
||||
|
||||
@Command({
|
||||
name: 'application:install',
|
||||
description:
|
||||
'Install an application on every workspace that does not have it yet. Workspaces where it is already installed are left untouched, use application:upgrade for those',
|
||||
})
|
||||
export class InstallApplicationCommand extends CommandRunner {
|
||||
protected logger: CommandLogger;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ApplicationRegistrationEntity)
|
||||
private readonly applicationRegistrationRepository: Repository<ApplicationRegistrationEntity>,
|
||||
@InjectRepository(ApplicationEntity)
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
private readonly applicationInstallService: ApplicationInstallService,
|
||||
private readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
) {
|
||||
super();
|
||||
this.logger = new CommandLogger({
|
||||
verbose: false,
|
||||
constructorName: this.constructor.name,
|
||||
});
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags:
|
||||
'-u, --application-registration-universal-identifier <application_registration_universal_identifier>',
|
||||
description: 'Application registration universal identifier',
|
||||
required: true,
|
||||
})
|
||||
parseApplicationRegistrationUniversalIdentifier(value: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id <workspace_id>',
|
||||
description:
|
||||
'Only install on the given workspace id. Can be repeated to target several workspaces. Targets all provisioned workspaces if not provided.',
|
||||
required: false,
|
||||
})
|
||||
parseWorkspaceId(value: string, previous?: Set<string>): Set<string> {
|
||||
const accumulator = previous ?? new Set<string>();
|
||||
|
||||
accumulator.add(value);
|
||||
|
||||
return accumulator;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '--workspace-count-limit <count>',
|
||||
description: `Limit the number of workspaces to iterate over (max ${MAX_WORKSPACE_COUNT_LIMIT})`,
|
||||
required: false,
|
||||
})
|
||||
parseWorkspaceCountLimit(value: string): number {
|
||||
return parseBoundedPositiveInteger(
|
||||
value,
|
||||
'workspace count limit',
|
||||
MAX_WORKSPACE_COUNT_LIMIT,
|
||||
);
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-d, --dry-run',
|
||||
description:
|
||||
'List the workspaces that would be installed without installing',
|
||||
required: false,
|
||||
})
|
||||
parseDryRun(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-y, --yes',
|
||||
description: 'Skip the confirmation prompt (for non-interactive usage)',
|
||||
required: false,
|
||||
})
|
||||
parseYes(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
override async run(
|
||||
_passedParams: string[],
|
||||
options: InstallApplicationCommandOptions,
|
||||
): Promise<void> {
|
||||
const registration = await this.applicationRegistrationRepository.findOne({
|
||||
where: {
|
||||
universalIdentifier: options.applicationRegistrationUniversalIdentifier,
|
||||
},
|
||||
});
|
||||
|
||||
if (!isDefined(registration)) {
|
||||
throw new Error(
|
||||
`Application registration with universal identifier ${options.applicationRegistrationUniversalIdentifier} not found`,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
registration.sourceType === ApplicationRegistrationSourceType.LOCAL ||
|
||||
registration.sourceType === ApplicationRegistrationSourceType.OAUTH_ONLY
|
||||
) {
|
||||
throw new Error(
|
||||
`Cannot install application ${registration.universalIdentifier}: applications with source type ${registration.sourceType} have no code artifacts to install`,
|
||||
);
|
||||
}
|
||||
|
||||
const targetVersion = registration.latestAvailableVersion;
|
||||
const versionLabel = targetVersion ?? 'latest available';
|
||||
|
||||
const isDryRun = options.dryRun ?? false;
|
||||
|
||||
const requestedWorkspaceIds = isDefined(options.workspaceId)
|
||||
? Array.from(options.workspaceId)
|
||||
: undefined;
|
||||
|
||||
// Explicit ids bypass the iterator's own workspace selection, so the count
|
||||
// limit and the already-installed filter are applied here instead.
|
||||
const alreadyInstalledRequestedWorkspaceIds = isDefined(
|
||||
requestedWorkspaceIds,
|
||||
)
|
||||
? await this.findAlreadyInstalledWorkspaceIds({
|
||||
universalIdentifier: registration.universalIdentifier,
|
||||
workspaceIds: requestedWorkspaceIds,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const workspaceIdsToIterate = isDefined(requestedWorkspaceIds)
|
||||
? requestedWorkspaceIds
|
||||
.filter(
|
||||
(workspaceId) =>
|
||||
!alreadyInstalledRequestedWorkspaceIds?.has(workspaceId),
|
||||
)
|
||||
.slice(0, options.workspaceCountLimit)
|
||||
: undefined;
|
||||
|
||||
if (
|
||||
isDefined(workspaceIdsToIterate) &&
|
||||
workspaceIdsToIterate.length === 0
|
||||
) {
|
||||
this.logger.log(
|
||||
`No workspace to install, every targeted workspace already has "${registration.name}" installed`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDryRun && !(options.yes ?? false)) {
|
||||
const isConfirmed = await askCommandConfirmation(
|
||||
`Confirm installing application ${registration.universalIdentifier} version ${versionLabel} on ${this.describeConfirmationTarget({ workspaceIdsToIterate, workspaceCountLimit: options.workspaceCountLimit })}`,
|
||||
);
|
||||
|
||||
if (!isConfirmed) {
|
||||
this.logger.log('Aborted, no installation performed');
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let skippedWorkspaceCount = 0;
|
||||
|
||||
const prefilteredWorkspaceCount =
|
||||
alreadyInstalledRequestedWorkspaceIds?.size ?? 0;
|
||||
|
||||
const report = await this.workspaceIteratorService.iterate({
|
||||
workspaceIds: workspaceIdsToIterate,
|
||||
workspaceCountLimit: isDefined(workspaceIdsToIterate)
|
||||
? undefined
|
||||
: options.workspaceCountLimit,
|
||||
dryRun: isDryRun,
|
||||
callback: async ({ workspaceId }) => {
|
||||
if (
|
||||
await this.isApplicationInstalled({
|
||||
universalIdentifier: registration.universalIdentifier,
|
||||
workspaceId,
|
||||
})
|
||||
) {
|
||||
skippedWorkspaceCount += 1;
|
||||
|
||||
this.logger.log(
|
||||
`Skipping workspace ${workspaceId}: "${registration.name}" is already installed, run application:upgrade to update it`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDryRun) {
|
||||
this.logger.log(
|
||||
`[DRY RUN] Would install "${registration.name}" (${registration.universalIdentifier}) version ${versionLabel} on workspace ${workspaceId}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.applicationInstallService.installApplication({
|
||||
appRegistrationId: registration.id,
|
||||
version: targetVersion ?? undefined,
|
||||
workspaceId,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`${isDryRun ? '[DRY RUN] ' : ''}Installed on ${report.success.length - skippedWorkspaceCount} workspace(s), skipped ${skippedWorkspaceCount + prefilteredWorkspaceCount} already installed, ${report.fail.length} failed`,
|
||||
);
|
||||
|
||||
this.logger.log(chalk.blue('Command completed!'));
|
||||
}
|
||||
|
||||
private describeConfirmationTarget({
|
||||
workspaceIdsToIterate,
|
||||
workspaceCountLimit,
|
||||
}: {
|
||||
workspaceIdsToIterate?: string[];
|
||||
workspaceCountLimit?: number;
|
||||
}): string {
|
||||
if (isDefined(workspaceIdsToIterate)) {
|
||||
return `workspace(s) ${workspaceIdsToIterate.join(', ')}`;
|
||||
}
|
||||
|
||||
return isDefined(workspaceCountLimit)
|
||||
? `up to ${workspaceCountLimit} provisioned workspace(s) that do not have it yet`
|
||||
: 'every provisioned workspace that does not have it yet';
|
||||
}
|
||||
|
||||
private async findAlreadyInstalledWorkspaceIds({
|
||||
universalIdentifier,
|
||||
workspaceIds,
|
||||
}: {
|
||||
universalIdentifier: string;
|
||||
workspaceIds: string[];
|
||||
}): Promise<Set<string>> {
|
||||
const existingApplications = await this.applicationRepository.find({
|
||||
select: ['workspaceId'],
|
||||
where: { universalIdentifier, workspaceId: In(workspaceIds) },
|
||||
});
|
||||
|
||||
return new Set(
|
||||
existingApplications.map((application) => application.workspaceId),
|
||||
);
|
||||
}
|
||||
|
||||
// Matches on the universal identifier, the same identity
|
||||
// ApplicationInstallService uses to decide between a fresh install and a
|
||||
// version upgrade, so a row with a stale registration id is not mistaken
|
||||
// for a missing installation.
|
||||
private async isApplicationInstalled({
|
||||
universalIdentifier,
|
||||
workspaceId,
|
||||
}: {
|
||||
universalIdentifier: string;
|
||||
workspaceId: string;
|
||||
}): Promise<boolean> {
|
||||
return this.applicationRepository.exists({
|
||||
where: { universalIdentifier, workspaceId },
|
||||
});
|
||||
}
|
||||
}
|
||||
+2
@@ -1,6 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { WorkspaceIteratorModule } from 'src/database/commands/command-runners/workspace-iterator.module';
|
||||
import { ApplicationInstallModule } from 'src/engine/core-modules/application/application-install/application-install.module';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
@@ -25,6 +26,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
FeatureFlagModule,
|
||||
PermissionsModule,
|
||||
TwentyConfigModule,
|
||||
WorkspaceIteratorModule,
|
||||
],
|
||||
providers: [
|
||||
ApplicationUpgradeService,
|
||||
|
||||
+22
-36
@@ -6,6 +6,10 @@ import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
WorkspaceIteratorService,
|
||||
type WorkspaceIteratorReport,
|
||||
} from 'src/database/commands/command-runners/workspace-iterator.service';
|
||||
import { ApplicationInstallService } from 'src/engine/core-modules/application/application-install/application-install.service';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
@@ -21,8 +25,6 @@ const npmPackageMetadataSchema = z.object({
|
||||
version: z.string(),
|
||||
});
|
||||
|
||||
const UPGRADE_APPLICATIONS_DEFAULT_BATCH_SIZE = 5;
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationUpgradeService {
|
||||
private readonly logger = new Logger(ApplicationUpgradeService.name);
|
||||
@@ -35,6 +37,7 @@ export class ApplicationUpgradeService {
|
||||
private readonly applicationInstallService: ApplicationInstallService,
|
||||
private readonly applicationRegistrationService: ApplicationRegistrationService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly workspaceIteratorService: WorkspaceIteratorService,
|
||||
) {}
|
||||
|
||||
async checkForUpdates(
|
||||
@@ -168,54 +171,38 @@ export class ApplicationUpgradeService {
|
||||
appRegistration,
|
||||
targetVersion,
|
||||
applications,
|
||||
batchSize = UPGRADE_APPLICATIONS_DEFAULT_BATCH_SIZE,
|
||||
}: {
|
||||
appRegistration: ApplicationRegistrationEntity;
|
||||
targetVersion: string;
|
||||
applications: ApplicationEntity[];
|
||||
batchSize?: number;
|
||||
}): Promise<void> {
|
||||
const sanitizedBatchSize = Math.max(1, Math.floor(batchSize));
|
||||
|
||||
for (
|
||||
let batchStart = 0;
|
||||
batchStart < applications.length;
|
||||
batchStart += sanitizedBatchSize
|
||||
) {
|
||||
const batch = applications.slice(
|
||||
batchStart,
|
||||
batchStart + sanitizedBatchSize,
|
||||
);
|
||||
|
||||
await Promise.all(
|
||||
batch.map(async (application) => {
|
||||
try {
|
||||
await this.upgradeApplicationToVersion({
|
||||
appRegistration,
|
||||
targetVersion,
|
||||
workspaceId: application.workspaceId,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to upgrade application ${application.id} to version ${targetVersion} in workspace ${application.workspaceId}`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}): Promise<WorkspaceIteratorReport> {
|
||||
// An empty workspace id list makes the iterator fall back to every
|
||||
// provisioned workspace, which would upgrade workspaces that were
|
||||
// filtered out.
|
||||
if (!isNonEmptyArray(applications)) {
|
||||
return { success: [], fail: [] };
|
||||
}
|
||||
|
||||
return this.workspaceIteratorService.iterate({
|
||||
workspaceIds: applications.map((application) => application.workspaceId),
|
||||
callback: async ({ workspaceId }) => {
|
||||
await this.upgradeApplicationToVersion({
|
||||
appRegistration,
|
||||
targetVersion,
|
||||
workspaceId,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async upgradeAllApplications({
|
||||
applicationRegistrationId,
|
||||
onlyAutoUpgrade = false,
|
||||
batchSize = UPGRADE_APPLICATIONS_DEFAULT_BATCH_SIZE,
|
||||
workspaceIds,
|
||||
workspaceCountLimit,
|
||||
}: {
|
||||
applicationRegistrationId: string;
|
||||
onlyAutoUpgrade?: boolean;
|
||||
batchSize?: number;
|
||||
workspaceIds?: string[];
|
||||
workspaceCountLimit?: number;
|
||||
}): Promise<void> {
|
||||
@@ -235,7 +222,6 @@ export class ApplicationUpgradeService {
|
||||
appRegistration,
|
||||
targetVersion,
|
||||
applications: applicationsToUpgrade,
|
||||
batchSize,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+6
-33
@@ -7,41 +7,20 @@ import { Repository } from 'typeorm';
|
||||
|
||||
import { CommandLogger } from 'src/database/commands/logger';
|
||||
import { askCommandConfirmation } from 'src/database/commands/utils/ask-command-confirmation.util';
|
||||
import { parseBoundedPositiveInteger } from 'src/database/commands/utils/parse-bounded-positive-integer.util';
|
||||
import { ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
|
||||
import { ApplicationUpgradeService } from 'src/engine/core-modules/application/application-upgrade/application-upgrade.service';
|
||||
|
||||
type UpgradeApplicationCommandOptions = {
|
||||
applicationRegistrationUniversalIdentifier: string;
|
||||
batchSize?: number;
|
||||
workspaceId?: Set<string>;
|
||||
workspaceCountLimit?: number;
|
||||
dryRun?: boolean;
|
||||
yes?: boolean;
|
||||
};
|
||||
|
||||
const MAX_BATCH_SIZE = 50;
|
||||
const MAX_WORKSPACE_COUNT_LIMIT = 50;
|
||||
|
||||
const parseBoundedPositiveInteger = (
|
||||
value: string,
|
||||
optionName: string,
|
||||
maximum: number,
|
||||
): number => {
|
||||
const parsedValue = Number(value);
|
||||
|
||||
if (!Number.isInteger(parsedValue) || parsedValue < 1) {
|
||||
throw new Error(
|
||||
`Invalid ${optionName} "${value}". Expected a positive integer`,
|
||||
);
|
||||
}
|
||||
|
||||
if (parsedValue > maximum) {
|
||||
throw new Error(`Invalid ${optionName} "${value}". Maximum is ${maximum}`);
|
||||
}
|
||||
|
||||
return parsedValue;
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'application:upgrade',
|
||||
description:
|
||||
@@ -72,15 +51,6 @@ export class UpgradeApplicationCommand extends CommandRunner {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-b, --batch-size <batch_size>',
|
||||
description: `Number of workspaces upgraded in parallel (defaults to 5, max ${MAX_BATCH_SIZE})`,
|
||||
required: false,
|
||||
})
|
||||
parseBatchSize(value: string): number {
|
||||
return parseBoundedPositiveInteger(value, 'batch size', MAX_BATCH_SIZE);
|
||||
}
|
||||
|
||||
@Option({
|
||||
flags: '-w, --workspace-id <workspace_id>',
|
||||
description:
|
||||
@@ -208,13 +178,16 @@ export class UpgradeApplicationCommand extends CommandRunner {
|
||||
|
||||
// Runs on the exact set shown at confirmation time, so installations
|
||||
// created or versions published while the operator answered are excluded.
|
||||
await this.applicationUpgradeService.upgradeApplications({
|
||||
const report = await this.applicationUpgradeService.upgradeApplications({
|
||||
appRegistration,
|
||||
targetVersion,
|
||||
applications: applicationsToUpgrade,
|
||||
batchSize: options.batchSize,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`Upgraded ${report.success.length} workspace(s), ${report.fail.length} failed`,
|
||||
);
|
||||
|
||||
this.logger.log(chalk.blue('Command completed!'));
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -3,5 +3,4 @@ export const UPGRADE_APPLICATIONS_JOB_NAME = 'UpgradeApplicationsJob';
|
||||
export type UpgradeApplicationsJobData = {
|
||||
applicationRegistrationId: string;
|
||||
onlyAutoUpgrade: boolean;
|
||||
batchSize?: number;
|
||||
};
|
||||
|
||||
-1
@@ -18,7 +18,6 @@ export class UpgradeApplicationsJob {
|
||||
await this.applicationUpgradeService.upgradeAllApplications({
|
||||
applicationRegistrationId: data.applicationRegistrationId,
|
||||
onlyAutoUpgrade: data.onlyAutoUpgrade,
|
||||
batchSize: data.batchSize,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user