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:
@@ -66,12 +66,18 @@ The share link uses the server's base URL (without any workspace subdomain) so i
|
||||
|
||||
### Version management
|
||||
|
||||
When updating an already deployed tarball app, the server requires the `version` in `package.json` to be **strictly higher** (per [semver](https://semver.org) ordering) than the currently deployed version. Re-deploying the same version, or pushing a lower one, is rejected before the tarball is stored — you'll see a `VERSION_ALREADY_EXISTS` error from the CLI.
|
||||
|
||||
To release an update:
|
||||
|
||||
1. Bump the `version` field in your `package.json`
|
||||
1. Bump the `version` field in your `package.json` (e.g. `1.2.3` → `1.2.4`, `1.3.0`, or `2.0.0`)
|
||||
2. Run `yarn twenty deploy` (or `yarn twenty deploy --remote production`)
|
||||
3. Workspaces that have the app installed will see the upgrade available in their settings
|
||||
|
||||
<Note>
|
||||
Pre-release tags work as expected: bumping `1.0.0-rc.1` → `1.0.0-rc.2` is allowed, and a final release like `1.0.0` is correctly recognized as higher than `1.0.0-rc.5`. The version in `package.json` must itself be a valid semver string.
|
||||
</Note>
|
||||
|
||||
{/* TODO: add screenshot of the Upgrade button */}
|
||||
|
||||
## Publishing to npm
|
||||
@@ -189,3 +195,12 @@ You can also install apps from the command line:
|
||||
```bash filename="Terminal"
|
||||
yarn twenty install
|
||||
```
|
||||
|
||||
<Note>
|
||||
The server enforces semver versioning on install, mirroring the rules on deploy:
|
||||
|
||||
- Installing the same version that is already installed in your workspace is rejected with an `APP_ALREADY_INSTALLED` error.
|
||||
- Installing a lower version than the one currently installed is rejected with a `CANNOT_DOWNGRADE_APPLICATION` error.
|
||||
|
||||
To install a newer version, deploy or publish it first, then re-run `yarn twenty install`.
|
||||
</Note>
|
||||
|
||||
+2
@@ -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:
|
||||
|
||||
+32
@@ -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,
|
||||
|
||||
+3
@@ -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);
|
||||
}
|
||||
|
||||
+28
@@ -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);
|
||||
}
|
||||
|
||||
+46
-2
@@ -173,7 +173,7 @@ describe('App Distribution (integration)', () => {
|
||||
expect(rows[0].sourceType).toBe('tarball');
|
||||
});
|
||||
|
||||
it('should update existing tarball registration on re-upload', async () => {
|
||||
it('should fail to update existing version', async () => {
|
||||
const uid = crypto.randomUUID();
|
||||
const manifest = createValidManifest(uid);
|
||||
|
||||
@@ -192,9 +192,53 @@ describe('App Distribution (integration)', () => {
|
||||
|
||||
createdRegistrationIds.push(firstResult.data!.uploadAppTarball.id);
|
||||
|
||||
const secondResult = await uploadAppTarball({
|
||||
const { errors } = await uploadAppTarball({
|
||||
tarballBuffer: tarball,
|
||||
universalIdentifier: uid,
|
||||
expectToFail: true,
|
||||
});
|
||||
|
||||
expect(errors).toBeDefined();
|
||||
|
||||
expect(
|
||||
errors?.some((error: { message: string }) =>
|
||||
error.message.includes(
|
||||
'version must be higher than the currently deployed version',
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('should update existing tarball registration on re-upload', async () => {
|
||||
const uid = crypto.randomUUID();
|
||||
const manifest = createValidManifest(uid);
|
||||
|
||||
const firstTarball = await createTestTarball({
|
||||
'manifest.json': manifest,
|
||||
'package.json': JSON.stringify({
|
||||
name: 'test-app',
|
||||
version: '1.1.0',
|
||||
}),
|
||||
});
|
||||
|
||||
const firstResult = await uploadAppTarball({
|
||||
tarballBuffer: firstTarball,
|
||||
universalIdentifier: uid,
|
||||
});
|
||||
|
||||
createdRegistrationIds.push(firstResult.data!.uploadAppTarball.id);
|
||||
|
||||
const secondTarball = await createTestTarball({
|
||||
'manifest.json': manifest,
|
||||
'package.json': JSON.stringify({
|
||||
name: 'test-app',
|
||||
version: '1.2.0',
|
||||
}),
|
||||
});
|
||||
|
||||
const secondResult = await uploadAppTarball({
|
||||
tarballBuffer: secondTarball,
|
||||
universalIdentifier: uid,
|
||||
});
|
||||
|
||||
expect(secondResult.data?.uploadAppTarball.id).toBe(
|
||||
|
||||
Reference in New Issue
Block a user