Fix install and deploy commands (#19481)

- disable publish with existing version
- disable installation of app version already installed

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
martmull
2026-04-09 12:10:16 +02:00
committed by GitHub
parent 5eaabe95e7
commit a78a843419
7 changed files with 133 additions and 3 deletions
@@ -29,6 +29,8 @@ export class ApplicationExceptionFilter implements ExceptionFilter {
throw new ForbiddenError(exception);
case ApplicationExceptionCode.INVALID_INPUT:
case ApplicationExceptionCode.SOURCE_CHANNEL_MISMATCH:
case ApplicationExceptionCode.APP_ALREADY_INSTALLED:
case ApplicationExceptionCode.CANNOT_DOWNGRADE_APPLICATION:
throw new UserInputError(exception);
case ApplicationExceptionCode.PACKAGE_RESOLUTION_FAILED:
case ApplicationExceptionCode.TARBALL_EXTRACTION_FAILED:
@@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { promises as fs } from 'fs';
import { resolve } from 'path';
import semver from 'semver';
import { Manifest } from 'twenty-shared/application';
import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
@@ -117,6 +118,37 @@ export class ApplicationInstallService {
sourceType: appRegistration.sourceType,
});
const incomingVersion = resolvedPackage.packageJson.version;
if (
!wasCreated &&
isDefined(application.version) &&
isDefined(incomingVersion)
) {
if (!isDefined(semver.valid(incomingVersion))) {
throw new ApplicationException(
`Invalid version "${incomingVersion}" in package.json. Must be a valid semver version.`,
ApplicationExceptionCode.INVALID_INPUT,
);
}
if (isDefined(semver.valid(application.version))) {
if (semver.eq(incomingVersion, application.version)) {
throw new ApplicationException(
`${universalIdentifier}@${incomingVersion} is already installed in this workspace.`,
ApplicationExceptionCode.APP_ALREADY_INSTALLED,
);
}
if (semver.lt(incomingVersion, application.version)) {
throw new ApplicationException(
`Cannot install ${universalIdentifier}@${incomingVersion}: version ${application.version} is already installed and downgrading is not allowed.`,
ApplicationExceptionCode.CANNOT_DOWNGRADE_APPLICATION,
);
}
}
}
await this.writeFilesToStorage(
resolvedPackage.extractedDir,
resolvedPackage.manifest,
@@ -12,6 +12,7 @@ export enum ApplicationRegistrationExceptionCode {
INVALID_INPUT = 'INVALID_INPUT',
SOURCE_CHANNEL_MISMATCH = 'SOURCE_CHANNEL_MISMATCH',
VARIABLE_NOT_FOUND = 'VARIABLE_NOT_FOUND',
VERSION_ALREADY_EXISTS = 'VERSION_ALREADY_EXISTS',
}
const getExceptionUserFriendlyMessage = (
@@ -32,6 +33,8 @@ const getExceptionUserFriendlyMessage = (
return msg`The app source channel does not match the expected type.`;
case ApplicationRegistrationExceptionCode.VARIABLE_NOT_FOUND:
return msg`Application registration variable not found.`;
case ApplicationRegistrationExceptionCode.VERSION_ALREADY_EXISTS:
return msg`This version is not higher than the currently deployed version. Please bump the version in package.json before deploying again.`;
default:
assertUnreachable(code);
}
@@ -5,6 +5,7 @@ import { promises as fs } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
import semver from 'semver';
import { FileFolder } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { Repository } from 'typeorm';
@@ -103,6 +104,33 @@ export class ApplicationTarballService {
ApplicationRegistrationExceptionCode.SOURCE_CHANNEL_MISMATCH,
);
}
if (
appRegistration.sourceType ===
ApplicationRegistrationSourceType.TARBALL &&
isDefined(appRegistration.latestAvailableVersion) &&
isDefined(packageJson?.version)
) {
const incomingVersion = packageJson.version;
const currentVersion = appRegistration.latestAvailableVersion;
if (!isDefined(semver.valid(incomingVersion))) {
throw new ApplicationRegistrationException(
`Invalid version "${incomingVersion}" in package.json. Must be a valid semver version.`,
ApplicationRegistrationExceptionCode.INVALID_INPUT,
);
}
if (
isDefined(semver.valid(currentVersion)) &&
semver.lte(incomingVersion, currentVersion)
) {
throw new ApplicationRegistrationException(
`Cannot deploy ${universalIdentifier}@${incomingVersion}: version must be higher than the currently deployed version ${currentVersion}. Please bump the version in package.json.`,
ApplicationRegistrationExceptionCode.VERSION_ALREADY_EXISTS,
);
}
}
} else {
appRegistration = this.appRegistrationRepository.create({
universalIdentifier,
@@ -18,6 +18,8 @@ export enum ApplicationExceptionCode {
PACKAGE_RESOLUTION_FAILED = 'PACKAGE_RESOLUTION_FAILED',
TARBALL_EXTRACTION_FAILED = 'TARBALL_EXTRACTION_FAILED',
UPGRADE_FAILED = 'UPGRADE_FAILED',
APP_ALREADY_INSTALLED = 'APP_ALREADY_INSTALLED',
CANNOT_DOWNGRADE_APPLICATION = 'CANNOT_DOWNGRADE_APPLICATION',
}
const getApplicationExceptionUserFriendlyMessage = (
@@ -50,6 +52,10 @@ const getApplicationExceptionUserFriendlyMessage = (
return msg`Failed to extract tarball.`;
case ApplicationExceptionCode.UPGRADE_FAILED:
return msg`Application upgrade failed.`;
case ApplicationExceptionCode.APP_ALREADY_INSTALLED:
return msg`This version of the application is already installed in this workspace.`;
case ApplicationExceptionCode.CANNOT_DOWNGRADE_APPLICATION:
return msg`A higher version of this application is already installed. Downgrading is not allowed.`;
default:
assertUnreachable(code);
}