[FEAT] New APP_VERSION env var inferred from tag & refactor upgrade-command to integrate versioning (#10751)
# Introduction This PR contains a big test file and few snapshots Related to https://github.com/twentyhq/core-team-issues/issues/487 ## New env var `APP_VERSION` Now will be injected directly in a built docker image the twenty's built version. Inferred from the build git tag name. Which mean on main or other `not a tag version` built APP_VERSION will be `null` ## New upgrade-commander-runner Refactored the upgrade command to be more strict regarding: - Version management - Sync metadata command always run - Added failing workspaces aggregator + logs on cleanup From now on the `upgrade` command will compare the `WORKSPACE_VERSION` to the `APP_VERSION` in order to bypass any workspace version != than the upgrade version `fromVersion` ## Existing commands Note that the version validation will be done only when passing by the `upgrade` command. Which means that running the following command `upgrade:x.y-some-specific-command` won't result in workspace version mutation This is to enforce that all an upgrade commands + sync-metadata has been run on a workspace ## Will do in other PR but related ### New workspace New workspace will now be inserted with version equal to the APP_VERSION they've been created by ### Old workspace Will create a command that should be ran outside of any `upgrade-runner` extending command, the command will have to be ran on every workspace before making the next release upgrade This command iterates over any active and suspended workspace that has `version` to `NULL` in order to update it `APP_VERSION` -1 minor ### SENTRY_RELEASE - Either deprecate SENTRY_RELEASE in favor of `APP_VERSION` => What about main with null version ? or create a new env var that would be `APP_COMMIT_SHA` instead of SENTRY third party ref ### Update CD to inject APP_VERSION from branch name ### Update docs and release logs Adding documentation for `APP_VERSION` ## Related PRs: https://github.com/twentyhq/twenty-infra/pull/181
This commit is contained in:
@ -66,6 +66,8 @@ ENV REACT_APP_SERVER_BASE_URL $REACT_APP_SERVER_BASE_URL
|
||||
ARG SENTRY_RELEASE
|
||||
ENV SENTRY_RELEASE $SENTRY_RELEASE
|
||||
|
||||
ARG APP_VERSION
|
||||
|
||||
# Copy built applications from previous stages
|
||||
COPY --chown=1000 --from=twenty-server-build /app /app
|
||||
COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server /app/packages/twenty-server
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Set APP_VERSION only if it has a value
|
||||
if [ ! -z "${APP_VERSION}" ]; then
|
||||
export APP_VERSION="${APP_VERSION}"
|
||||
fi
|
||||
|
||||
# Check if the initialization has already been done and that we enabled automatic migration
|
||||
if [ "${DISABLE_DB_MIGRATIONS}" != "true" ] && [ ! -f /app/docker-data/db_status ]; then
|
||||
echo "Running database setup and migrations..."
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`UpgradeCommandRunner Workspace upgrade should fail when APP_VERSION is not defined 1`] = `[Error: Cannot run upgrade command when APP_VERSION is not defined]`;
|
||||
|
||||
exports[`UpgradeCommandRunner Workspace upgrade should fail when workspace version is not defined 1`] = `[Error: WORKSPACE_VERSION_NOT_DEFINED to=2.0.0]`;
|
||||
|
||||
exports[`UpgradeCommandRunner Workspace upgrade should fail when workspace version is not equal to fromVersion 1`] = `[Error: WORKSPACE_VERSION_MISSMATCH Upgrade for workspace workspace_0 failed as its version is beneath fromWorkspaceVersion=2.0.0]`;
|
||||
|
||||
exports[`UpgradeCommandRunner should run upgrade command with failing and successful workspaces 1`] = `[Error: WORKSPACE_VERSION_MISSMATCH Upgrade for workspace outated_version_workspace failed as its version is beneath fromWorkspaceVersion=1.0.0]`;
|
||||
|
||||
exports[`UpgradeCommandRunner should run upgrade command with failing and successful workspaces 2`] = `[Error: Received invalid version: invalid 1.0.0]`;
|
||||
|
||||
exports[`UpgradeCommandRunner should run upgrade command with failing and successful workspaces 3`] = `[Error: WORKSPACE_VERSION_NOT_DEFINED to=2.0.0]`;
|
||||
@ -0,0 +1,417 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { SemVer } from 'semver';
|
||||
import { EachTestingContext } from 'twenty-shared';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { UpgradeCommandRunner } from 'src/database/commands/command-runners/upgrade.command-runner';
|
||||
import { EnvironmentVariables } from 'src/engine/core-modules/environment/environment-variables';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
||||
|
||||
class TestUpgradeCommandRunnerV1 extends UpgradeCommandRunner {
|
||||
fromWorkspaceVersion = new SemVer('1.0.0');
|
||||
VALIDATE_WORKSPACE_VERSION_FEATURE_FLAG = true as const;
|
||||
|
||||
public override async runBeforeSyncMetadata(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
public override async runAfterSyncMetadata(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
class InvalidVersionUpgradeCommandRunner extends UpgradeCommandRunner {
|
||||
fromWorkspaceVersion = new SemVer('invalid');
|
||||
VALIDATE_WORKSPACE_VERSION_FEATURE_FLAG = true as const;
|
||||
|
||||
protected async runBeforeSyncMetadata(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
protected async runAfterSyncMetadata(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
class TestUpgradeCommandRunnerV2 extends UpgradeCommandRunner {
|
||||
fromWorkspaceVersion = new SemVer('2.0.0');
|
||||
VALIDATE_WORKSPACE_VERSION_FEATURE_FLAG = true as const;
|
||||
|
||||
protected async runBeforeSyncMetadata(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
protected async runAfterSyncMetadata(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
type CommandRunnerValues =
|
||||
| typeof TestUpgradeCommandRunnerV1
|
||||
| typeof TestUpgradeCommandRunnerV2
|
||||
| typeof InvalidVersionUpgradeCommandRunner;
|
||||
|
||||
const generateMockWorkspace = (overrides?: Partial<Workspace>) =>
|
||||
({
|
||||
id: 'workspace-id',
|
||||
version: '1.0.0',
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
allowImpersonation: false,
|
||||
isPublicInviteLinkEnabled: false,
|
||||
displayName: 'Test Workspace',
|
||||
domainName: 'test',
|
||||
inviteHash: 'hash',
|
||||
logo: null,
|
||||
deletedAt: null,
|
||||
activationStatus: 'active',
|
||||
workspaceMembersCount: 1,
|
||||
...overrides,
|
||||
}) as Workspace;
|
||||
|
||||
type BuildUpgradeCommandModuleArgs = {
|
||||
workspaces: Workspace[];
|
||||
appVersion: string | null;
|
||||
commandRunner: CommandRunnerValues;
|
||||
};
|
||||
const buildUpgradeCommandModule = async ({
|
||||
workspaces,
|
||||
appVersion,
|
||||
commandRunner,
|
||||
}: BuildUpgradeCommandModuleArgs) => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
commandRunner,
|
||||
{
|
||||
provide: getRepositoryToken(Workspace, 'core'),
|
||||
useValue: {
|
||||
findOneByOrFail: jest
|
||||
.fn()
|
||||
.mockImplementation((args) =>
|
||||
workspaces.find((el) => el.id === args.id),
|
||||
),
|
||||
update: jest.fn(),
|
||||
find: jest.fn().mockResolvedValue(workspaces),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: EnvironmentService,
|
||||
useValue: {
|
||||
get: jest
|
||||
.fn()
|
||||
.mockImplementation((key: keyof EnvironmentVariables) => {
|
||||
switch (key) {
|
||||
case 'APP_VERSION': {
|
||||
return appVersion;
|
||||
}
|
||||
default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: TwentyORMGlobalManager,
|
||||
useValue: {
|
||||
connect: jest.fn(),
|
||||
destroyDataSourceForWorkspace: jest.fn(),
|
||||
getDataSourceForWorkspace: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: SyncWorkspaceMetadataCommand,
|
||||
useValue: {
|
||||
runOnWorkspace: jest.fn(),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
return module;
|
||||
};
|
||||
|
||||
describe('UpgradeCommandRunner', () => {
|
||||
let upgradeCommandRunner: TestUpgradeCommandRunnerV1;
|
||||
let workspaceRepository: Repository<Workspace>;
|
||||
let syncWorkspaceMetadataCommand: jest.Mocked<SyncWorkspaceMetadataCommand>;
|
||||
let runAfterSyncMetadataSpy: jest.SpyInstance;
|
||||
let runBeforeSyncMetadataSpy: jest.SpyInstance;
|
||||
let twentyORMGlobalManagerSpy: TwentyORMGlobalManager;
|
||||
|
||||
type BuildModuleAndSetupSpiesArgs = {
|
||||
numberOfWorkspace?: number;
|
||||
workspaceOverride?: Partial<Workspace>;
|
||||
workspaces?: Workspace[];
|
||||
appVersion?: string | null;
|
||||
commandRunner?: CommandRunnerValues;
|
||||
};
|
||||
const buildModuleAndSetupSpies = async ({
|
||||
numberOfWorkspace = 1,
|
||||
workspaceOverride,
|
||||
workspaces,
|
||||
commandRunner = TestUpgradeCommandRunnerV1,
|
||||
appVersion = '2.0.0',
|
||||
}: BuildModuleAndSetupSpiesArgs) => {
|
||||
const generatedWorkspaces = Array.from(
|
||||
{ length: numberOfWorkspace },
|
||||
(_v, index) =>
|
||||
generateMockWorkspace({
|
||||
id: `workspace_${index}`,
|
||||
...workspaceOverride,
|
||||
}),
|
||||
);
|
||||
const module = await buildUpgradeCommandModule({
|
||||
commandRunner,
|
||||
appVersion,
|
||||
workspaces: [...generatedWorkspaces, ...(workspaces ?? [])],
|
||||
});
|
||||
|
||||
upgradeCommandRunner = module.get(commandRunner);
|
||||
runBeforeSyncMetadataSpy = jest.spyOn(
|
||||
upgradeCommandRunner,
|
||||
'runBeforeSyncMetadata',
|
||||
);
|
||||
runAfterSyncMetadataSpy = jest.spyOn(
|
||||
upgradeCommandRunner,
|
||||
'runAfterSyncMetadata',
|
||||
);
|
||||
jest.spyOn(upgradeCommandRunner, 'runOnWorkspace');
|
||||
|
||||
workspaceRepository = module.get<Repository<Workspace>>(
|
||||
getRepositoryToken(Workspace, 'core'),
|
||||
);
|
||||
syncWorkspaceMetadataCommand = module.get(SyncWorkspaceMetadataCommand);
|
||||
twentyORMGlobalManagerSpy = module.get<TwentyORMGlobalManager>(
|
||||
TwentyORMGlobalManager,
|
||||
);
|
||||
};
|
||||
|
||||
it('should ignore and list as succesfull upgrade on workspace with higher version', async () => {
|
||||
const higherVersionWorkspace = generateMockWorkspace({
|
||||
id: 'higher_version_workspace',
|
||||
version: '42.42.42',
|
||||
});
|
||||
const appVersion = '2.0.0';
|
||||
|
||||
await buildModuleAndSetupSpies({
|
||||
numberOfWorkspace: 0,
|
||||
workspaces: [higherVersionWorkspace],
|
||||
appVersion,
|
||||
});
|
||||
const passedParams = [];
|
||||
const options = {};
|
||||
|
||||
await upgradeCommandRunner.run(passedParams, options);
|
||||
|
||||
const { fail: failReport, success: successReport } =
|
||||
upgradeCommandRunner.migrationReport;
|
||||
|
||||
expect(successReport.length).toBe(1);
|
||||
expect(failReport.length).toBe(0);
|
||||
|
||||
[
|
||||
twentyORMGlobalManagerSpy.destroyDataSourceForWorkspace,
|
||||
upgradeCommandRunner.runOnWorkspace,
|
||||
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(1));
|
||||
|
||||
[
|
||||
upgradeCommandRunner.runBeforeSyncMetadata,
|
||||
syncWorkspaceMetadataCommand.runOnWorkspace,
|
||||
upgradeCommandRunner.runAfterSyncMetadata,
|
||||
workspaceRepository.update,
|
||||
].forEach((fn) => expect(fn).not.toHaveBeenCalled());
|
||||
});
|
||||
|
||||
it('should run upgrade command with failing and successful workspaces', async () => {
|
||||
const outdatedVersionWorkspaces = generateMockWorkspace({
|
||||
id: 'outated_version_workspace',
|
||||
version: '0.42.42',
|
||||
});
|
||||
const invalidVersionWorkspace = generateMockWorkspace({
|
||||
id: 'invalid_version_workspace',
|
||||
version: 'invalid',
|
||||
});
|
||||
const nullVersionWorkspace = generateMockWorkspace({
|
||||
id: 'null_version_workspace',
|
||||
version: null,
|
||||
});
|
||||
const numberOfValidWorkspace = 4;
|
||||
const failingWorkspaces = [
|
||||
outdatedVersionWorkspaces,
|
||||
invalidVersionWorkspace,
|
||||
nullVersionWorkspace,
|
||||
];
|
||||
const totalWorkspace = numberOfValidWorkspace + failingWorkspaces.length;
|
||||
const appVersion = '2.0.0';
|
||||
|
||||
await buildModuleAndSetupSpies({
|
||||
numberOfWorkspace: numberOfValidWorkspace,
|
||||
workspaces: failingWorkspaces,
|
||||
appVersion,
|
||||
});
|
||||
const passedParams = [];
|
||||
const options = {};
|
||||
|
||||
await upgradeCommandRunner.run(passedParams, options);
|
||||
|
||||
// Common assertions
|
||||
const { fail: failReport, success: successReport } =
|
||||
upgradeCommandRunner.migrationReport;
|
||||
|
||||
[
|
||||
twentyORMGlobalManagerSpy.destroyDataSourceForWorkspace,
|
||||
upgradeCommandRunner.runOnWorkspace,
|
||||
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(totalWorkspace));
|
||||
expect(failReport.length + successReport.length).toBe(totalWorkspace);
|
||||
|
||||
// Success assertions
|
||||
[
|
||||
upgradeCommandRunner.runBeforeSyncMetadata,
|
||||
syncWorkspaceMetadataCommand.runOnWorkspace,
|
||||
upgradeCommandRunner.runAfterSyncMetadata,
|
||||
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(numberOfValidWorkspace));
|
||||
expect(successReport.length).toBe(numberOfValidWorkspace);
|
||||
expect(workspaceRepository.update).toHaveBeenNthCalledWith(
|
||||
numberOfValidWorkspace,
|
||||
{ id: expect.any(String) },
|
||||
{ version: appVersion },
|
||||
);
|
||||
|
||||
// Failing assertions
|
||||
expect(failReport.length).toBe(failingWorkspaces.length);
|
||||
failReport.forEach((report) => {
|
||||
expect(
|
||||
failingWorkspaces.some(
|
||||
(workspace) => workspace.id === report.workspaceId,
|
||||
),
|
||||
).toBe(true);
|
||||
expect(report.error).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('should run upgrade over several workspaces', async () => {
|
||||
const numberOfWorkspace = 42;
|
||||
const appVersion = '2.0.0';
|
||||
|
||||
await buildModuleAndSetupSpies({
|
||||
numberOfWorkspace,
|
||||
appVersion,
|
||||
});
|
||||
const passedParams = [];
|
||||
const options = {};
|
||||
|
||||
await upgradeCommandRunner.run(passedParams, options);
|
||||
|
||||
[
|
||||
upgradeCommandRunner.runOnWorkspace,
|
||||
upgradeCommandRunner.runBeforeSyncMetadata,
|
||||
upgradeCommandRunner.runAfterSyncMetadata,
|
||||
syncWorkspaceMetadataCommand.runOnWorkspace,
|
||||
twentyORMGlobalManagerSpy.destroyDataSourceForWorkspace,
|
||||
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(numberOfWorkspace));
|
||||
expect(workspaceRepository.update).toHaveBeenNthCalledWith(
|
||||
numberOfWorkspace,
|
||||
{ id: expect.any(String) },
|
||||
{ version: appVersion },
|
||||
);
|
||||
expect(upgradeCommandRunner.migrationReport.success.length).toBe(42);
|
||||
expect(upgradeCommandRunner.migrationReport.fail.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should run syncMetadataCommand betweensuccessful beforeSyncMetadataUpgradeCommandsToRun and afterSyncMetadataUpgradeCommandsToRun', async () => {
|
||||
await buildModuleAndSetupSpies({});
|
||||
const passedParams = [];
|
||||
const options = {};
|
||||
|
||||
await upgradeCommandRunner.run(passedParams, options);
|
||||
|
||||
[
|
||||
upgradeCommandRunner.runOnWorkspace,
|
||||
upgradeCommandRunner.runBeforeSyncMetadata,
|
||||
upgradeCommandRunner.runAfterSyncMetadata,
|
||||
syncWorkspaceMetadataCommand.runOnWorkspace,
|
||||
twentyORMGlobalManagerSpy.destroyDataSourceForWorkspace,
|
||||
].forEach((fn) => expect(fn).toHaveBeenCalledTimes(1));
|
||||
|
||||
// Verify order of execution
|
||||
const beforeSyncCall = runBeforeSyncMetadataSpy.mock.invocationCallOrder[0];
|
||||
const afterSyncCall = runAfterSyncMetadataSpy.mock.invocationCallOrder[0];
|
||||
const syncMetadataCall =
|
||||
syncWorkspaceMetadataCommand.runOnWorkspace.mock.invocationCallOrder[0];
|
||||
|
||||
expect(beforeSyncCall).toBeLessThan(syncMetadataCall);
|
||||
expect(syncMetadataCall).toBeLessThan(afterSyncCall);
|
||||
expect(upgradeCommandRunner.migrationReport.success.length).toBe(1);
|
||||
expect(upgradeCommandRunner.migrationReport.fail.length).toBe(0);
|
||||
});
|
||||
|
||||
describe('Workspace upgrade should fail', () => {
|
||||
const failingTestUseCases: EachTestingContext<{
|
||||
input: Omit<BuildModuleAndSetupSpiesArgs, 'numberOfWorkspace'>;
|
||||
}>[] = [
|
||||
{
|
||||
title: 'when workspace version is not equal to fromVersion',
|
||||
context: {
|
||||
input: {
|
||||
appVersion: '3.0.0',
|
||||
commandRunner: TestUpgradeCommandRunnerV2,
|
||||
workspaceOverride: {
|
||||
version: '0.1.0',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when workspace version is not defined',
|
||||
context: {
|
||||
input: {
|
||||
workspaceOverride: {
|
||||
version: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when APP_VERSION is not defined',
|
||||
context: {
|
||||
input: {
|
||||
appVersion: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it.each(failingTestUseCases)('$title', async ({ context: { input } }) => {
|
||||
await buildModuleAndSetupSpies(input);
|
||||
|
||||
const passedParams = [];
|
||||
const options = {};
|
||||
|
||||
await upgradeCommandRunner.run(passedParams, options);
|
||||
|
||||
const { fail: failReport, success: successReport } =
|
||||
upgradeCommandRunner.migrationReport;
|
||||
|
||||
expect(successReport.length).toBe(0);
|
||||
expect(failReport.length).toBe(1);
|
||||
const { workspaceId, error } = failReport[0];
|
||||
|
||||
expect(workspaceId).toBe('workspace_0');
|
||||
expect(error).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw if upgrade command version is invalid', async () => {
|
||||
await expect(
|
||||
buildModuleAndSetupSpies({
|
||||
commandRunner: InvalidVersionUpgradeCommandRunner,
|
||||
}),
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Invalid Version: invalid"`);
|
||||
});
|
||||
});
|
||||
@ -24,6 +24,16 @@ export type RunOnWorkspaceArgs = {
|
||||
total: number;
|
||||
};
|
||||
|
||||
export type WorkspaceMigrationReport = {
|
||||
fail: {
|
||||
workspaceId: string;
|
||||
error: Error;
|
||||
}[];
|
||||
success: {
|
||||
workspaceId: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export abstract class ActiveOrSuspendedWorkspacesMigrationCommandRunner<
|
||||
Options extends
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandOptions = ActiveOrSuspendedWorkspacesMigrationCommandOptions,
|
||||
@ -31,6 +41,10 @@ export abstract class ActiveOrSuspendedWorkspacesMigrationCommandRunner<
|
||||
private workspaceIds: string[] = [];
|
||||
private startFromWorkspaceId: string | undefined;
|
||||
private workspaceCountLimit: number | undefined;
|
||||
public migrationReport: WorkspaceMigrationReport = {
|
||||
fail: [],
|
||||
success: [],
|
||||
};
|
||||
|
||||
constructor(
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
@ -107,7 +121,7 @@ export abstract class ActiveOrSuspendedWorkspacesMigrationCommandRunner<
|
||||
override async runMigrationCommand(
|
||||
_passedParams: string[],
|
||||
options: Options,
|
||||
): Promise<void> {
|
||||
) {
|
||||
const activeWorkspaceIds =
|
||||
this.workspaceIds.length > 0
|
||||
? this.workspaceIds
|
||||
@ -117,39 +131,50 @@ export abstract class ActiveOrSuspendedWorkspacesMigrationCommandRunner<
|
||||
this.logger.log(chalk.yellow('Dry run mode: No changes will be applied'));
|
||||
}
|
||||
|
||||
try {
|
||||
for (const [index, workspaceId] of activeWorkspaceIds.entries()) {
|
||||
this.logger.log(
|
||||
`Running command on workspace ${workspaceId} ${index + 1}/${activeWorkspaceIds.length}`,
|
||||
);
|
||||
for (const [index, workspaceId] of activeWorkspaceIds.entries()) {
|
||||
this.logger.log(
|
||||
`Running command on workspace ${workspaceId} ${index + 1}/${activeWorkspaceIds.length}`,
|
||||
);
|
||||
|
||||
try {
|
||||
const dataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
|
||||
workspaceId,
|
||||
false,
|
||||
);
|
||||
|
||||
await this.runOnWorkspace({
|
||||
options,
|
||||
try {
|
||||
const dataSource =
|
||||
await this.twentyORMGlobalManager.getDataSourceForWorkspace(
|
||||
workspaceId,
|
||||
dataSource,
|
||||
index: index,
|
||||
total: activeWorkspaceIds.length,
|
||||
});
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
chalk.red(`Error in workspace ${workspaceId}: ${error.message}`),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
await this.runOnWorkspace({
|
||||
options,
|
||||
workspaceId,
|
||||
dataSource,
|
||||
index: index,
|
||||
total: activeWorkspaceIds.length,
|
||||
});
|
||||
this.migrationReport.success.push({
|
||||
workspaceId,
|
||||
});
|
||||
} catch (error) {
|
||||
this.migrationReport.fail.push({
|
||||
error,
|
||||
workspaceId,
|
||||
});
|
||||
this.logger.warn(
|
||||
chalk.red(`Error in workspace ${workspaceId}: ${error.message}`),
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.twentyORMGlobalManager.destroyDataSourceForWorkspace(
|
||||
workspaceId,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
}
|
||||
|
||||
this.migrationReport.fail.forEach(({ error, workspaceId }) =>
|
||||
this.logger.error(`Error in workspace ${workspaceId}: ${error.message}`),
|
||||
);
|
||||
}
|
||||
|
||||
protected abstract runOnWorkspace(args: RunOnWorkspaceArgs): Promise<void>;
|
||||
|
||||
@ -0,0 +1,132 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { SemVer } from 'semver';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
||||
import { compareVersionMajorAndMinor } from 'src/utils/version/compare-version-minor-and-major';
|
||||
|
||||
type ValidateWorkspaceVersionEqualsWorkspaceFromVersionOrThrowArgs = {
|
||||
workspaceId: string;
|
||||
appVersion: string | undefined;
|
||||
};
|
||||
|
||||
export abstract class UpgradeCommandRunner extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
abstract readonly fromWorkspaceVersion: SemVer;
|
||||
public readonly VALIDATE_WORKSPACE_VERSION_FEATURE_FLAG?: true;
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
protected readonly environmentService: EnvironmentService,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
protected readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
override async runOnWorkspace(args: RunOnWorkspaceArgs): Promise<void> {
|
||||
const { workspaceId, index, total, options } = args;
|
||||
const appVersion = this.environmentService.get('APP_VERSION');
|
||||
|
||||
this.logger.log(
|
||||
chalk.blue(
|
||||
`${options.dryRun ? '(dry run)' : ''} Upgrading workspace ${workspaceId} ${index + 1}/${total}`,
|
||||
),
|
||||
);
|
||||
|
||||
const workspaceVersionCompareResult =
|
||||
await this.retrieveWorkspaceVersionAndCompareToWorkspaceFromVersion({
|
||||
appVersion,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
switch (workspaceVersionCompareResult) {
|
||||
case 'lower': {
|
||||
throw new Error(
|
||||
`WORKSPACE_VERSION_MISSMATCH Upgrade for workspace ${workspaceId} failed as its version is beneath fromWorkspaceVersion=${this.fromWorkspaceVersion.version}`,
|
||||
);
|
||||
}
|
||||
case 'equal': {
|
||||
await this.runBeforeSyncMetadata(args);
|
||||
await this.syncWorkspaceMetadataCommand.runOnWorkspace(args);
|
||||
await this.runAfterSyncMetadata(args);
|
||||
|
||||
await this.workspaceRepository.update(
|
||||
{ id: workspaceId },
|
||||
{ version: appVersion },
|
||||
);
|
||||
this.logger.log(
|
||||
chalk.blue(`Upgrade for workspace ${workspaceId} completed.`),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
case 'higher': {
|
||||
this.logger.log(
|
||||
chalk.blue(
|
||||
`Upgrade for workspace ${workspaceId} ignored as is already at a higher version.`,
|
||||
),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`Should never occur, encountered unexpected value from retrieveWorkspaceVersionAndCompareToWorkspaceFromVersion ${workspaceVersionCompareResult}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async retrieveWorkspaceVersionAndCompareToWorkspaceFromVersion({
|
||||
appVersion,
|
||||
workspaceId,
|
||||
}: ValidateWorkspaceVersionEqualsWorkspaceFromVersionOrThrowArgs) {
|
||||
if (!isDefined(appVersion)) {
|
||||
throw new Error(
|
||||
'Cannot run upgrade command when APP_VERSION is not defined',
|
||||
);
|
||||
}
|
||||
|
||||
// TODO remove after first release has been done using workspace_version
|
||||
if (!isDefined(this.VALIDATE_WORKSPACE_VERSION_FEATURE_FLAG)) {
|
||||
this.logger.warn(
|
||||
'VALIDATE_WORKSPACE_VERSION_FEATURE_FLAG set to true ignoring workspace versions validation step',
|
||||
);
|
||||
const equalVersions = 0;
|
||||
|
||||
return equalVersions;
|
||||
}
|
||||
|
||||
const workspace = await this.workspaceRepository.findOneByOrFail({
|
||||
id: workspaceId,
|
||||
});
|
||||
const currentWorkspaceVersion = workspace.version;
|
||||
|
||||
if (!isDefined(currentWorkspaceVersion)) {
|
||||
throw new Error(`WORKSPACE_VERSION_NOT_DEFINED to=${appVersion}`);
|
||||
}
|
||||
|
||||
return compareVersionMajorAndMinor(
|
||||
currentWorkspaceVersion,
|
||||
this.fromWorkspaceVersion.version,
|
||||
);
|
||||
}
|
||||
|
||||
protected abstract runBeforeSyncMetadata(
|
||||
args: RunOnWorkspaceArgs,
|
||||
): Promise<void>;
|
||||
protected abstract runAfterSyncMetadata(
|
||||
args: RunOnWorkspaceArgs,
|
||||
): Promise<void>;
|
||||
}
|
||||
@ -11,14 +11,12 @@ import { rawDataSource } from 'src/database/typeorm/raw/raw.datasource';
|
||||
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
|
||||
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
|
||||
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
|
||||
|
||||
@Injectable()
|
||||
export class DataSeedDemoWorkspaceService {
|
||||
constructor(
|
||||
private readonly environmentService: EnvironmentService,
|
||||
private readonly workspaceManagerService: WorkspaceManagerService,
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
|
||||
@ -13,7 +13,6 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { FieldMetadataDefaultOption } from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { tasksAssignedToMeView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-assigned-to-me';
|
||||
import { TASK_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids';
|
||||
@ -36,7 +35,6 @@ export class AddTasksAssignedToMeViewCommand extends ActiveOrSuspendedWorkspaces
|
||||
@InjectRepository(FieldMetadataEntity, 'metadata')
|
||||
private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
}
|
||||
|
||||
@ -1,18 +1,17 @@
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import chalk from 'chalk';
|
||||
import { Command } from 'nest-commander';
|
||||
import { SemVer } from 'semver';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ActiveOrSuspendedWorkspacesMigrationCommandRunner,
|
||||
RunOnWorkspaceArgs,
|
||||
} from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { UpgradeCommandRunner } from 'src/database/commands/command-runners/upgrade.command-runner';
|
||||
import { AddTasksAssignedToMeViewCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-add-tasks-assigned-to-me-view.command';
|
||||
import { MigrateIsSearchableForCustomObjectMetadataCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-is-searchable-for-custom-object-metadata.command';
|
||||
import { MigrateRichTextContentPatchCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-rich-text-content-patch.command';
|
||||
import { MigrateSearchVectorOnNoteAndTaskEntitiesCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-migrate-search-vector-on-note-and-task-entities.command';
|
||||
import { UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand } from 'src/database/commands/upgrade-version-command/0-43/0-43-update-default-view-record-opening-on-workflow-objects.command';
|
||||
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command';
|
||||
@ -21,28 +20,31 @@ import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/works
|
||||
name: 'upgrade',
|
||||
description: 'Upgrade workspaces to the latest version',
|
||||
})
|
||||
export class UpgradeCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
|
||||
export class UpgradeCommand extends UpgradeCommandRunner {
|
||||
fromWorkspaceVersion = new SemVer('0.43.0');
|
||||
|
||||
constructor(
|
||||
@InjectRepository(Workspace, 'core')
|
||||
protected readonly workspaceRepository: Repository<Workspace>,
|
||||
protected readonly environmentService: EnvironmentService,
|
||||
protected readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
protected readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||
|
||||
protected readonly migrateRichTextContentPatchCommand: MigrateRichTextContentPatchCommand,
|
||||
protected readonly addTasksAssignedToMeViewCommand: AddTasksAssignedToMeViewCommand,
|
||||
protected readonly migrateIsSearchableForCustomObjectMetadataCommand: MigrateIsSearchableForCustomObjectMetadataCommand,
|
||||
protected readonly updateDefaultViewRecordOpeningOnWorkflowObjectsCommand: UpdateDefaultViewRecordOpeningOnWorkflowObjectsCommand,
|
||||
protected readonly migrateSearchVectorOnNoteAndTaskEntitiesCommand: MigrateSearchVectorOnNoteAndTaskEntitiesCommand,
|
||||
protected readonly syncWorkspaceMetadataCommand: SyncWorkspaceMetadataCommand,
|
||||
) {
|
||||
super(workspaceRepository, twentyORMGlobalManager);
|
||||
super(
|
||||
workspaceRepository,
|
||||
environmentService,
|
||||
twentyORMGlobalManager,
|
||||
syncWorkspaceMetadataCommand,
|
||||
);
|
||||
}
|
||||
|
||||
override async runOnWorkspace(args: RunOnWorkspaceArgs): Promise<void> {
|
||||
this.logger.log(
|
||||
chalk.blue(
|
||||
`${args.options.dryRun ? '(dry run)' : ''} Upgrading workspace ${args.workspaceId} ${args.index + 1}/${args.total}`,
|
||||
),
|
||||
);
|
||||
|
||||
override async runBeforeSyncMetadata(args: RunOnWorkspaceArgs) {
|
||||
await this.migrateRichTextContentPatchCommand.runOnWorkspace(args);
|
||||
|
||||
await this.migrateIsSearchableForCustomObjectMetadataCommand.runOnWorkspace(
|
||||
@ -56,17 +58,13 @@ export class UpgradeCommand extends ActiveOrSuspendedWorkspacesMigrationCommandR
|
||||
await this.migrateIsSearchableForCustomObjectMetadataCommand.runOnWorkspace(
|
||||
args,
|
||||
);
|
||||
}
|
||||
|
||||
await this.syncWorkspaceMetadataCommand.runOnWorkspace(args);
|
||||
|
||||
override async runAfterSyncMetadata(args: RunOnWorkspaceArgs) {
|
||||
await this.updateDefaultViewRecordOpeningOnWorkflowObjectsCommand.runOnWorkspace(
|
||||
args,
|
||||
);
|
||||
|
||||
await this.addTasksAssignedToMeViewCommand.runOnWorkspace(args);
|
||||
|
||||
this.logger.log(
|
||||
chalk.blue(`Upgrade for workspace ${args.workspaceId} completed.`),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,6 +7,7 @@ import {
|
||||
IsEnum,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsSemVer,
|
||||
IsString,
|
||||
IsUrl,
|
||||
ValidateIf,
|
||||
@ -978,6 +979,14 @@ export class EnvironmentVariables {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
IS_ATTACHMENT_PREVIEW_ENABLED = true;
|
||||
|
||||
@EnvironmentVariablesMetadata({
|
||||
group: EnvironmentVariablesGroup.ServerConfig,
|
||||
description: 'Twenty server version',
|
||||
})
|
||||
@IsOptional()
|
||||
@IsSemVer()
|
||||
APP_VERSION?: string;
|
||||
}
|
||||
|
||||
export const validate = (
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`is-same-major-and-minor-version incomplete version1 1`] = `"Received invalid version: 1.0 1.1.0"`;
|
||||
|
||||
exports[`is-same-major-and-minor-version invalid version1 1`] = `"Received invalid version: invalid 1.1.0"`;
|
||||
|
||||
exports[`is-same-major-and-minor-version invalid version2 1`] = `"Received invalid version: 1.0.0 invalid"`;
|
||||
@ -0,0 +1,192 @@
|
||||
import { EachTestingContext } from 'twenty-shared';
|
||||
|
||||
import { compareVersionMajorAndMinor } from 'src/utils/version/compare-version-minor-and-major';
|
||||
|
||||
type IsSameVersionTestCase = EachTestingContext<{
|
||||
version1: string;
|
||||
version2: string;
|
||||
expected?: ReturnType<typeof compareVersionMajorAndMinor>;
|
||||
expectToThrow?: boolean;
|
||||
}>;
|
||||
describe('is-same-major-and-minor-version', () => {
|
||||
const beneathVersionTestCases: IsSameVersionTestCase[] = [
|
||||
{
|
||||
context: {
|
||||
version1: '1.0.0',
|
||||
version2: '1.1.0',
|
||||
expected: 'lower',
|
||||
},
|
||||
title: 'different minor version',
|
||||
},
|
||||
{
|
||||
context: {
|
||||
version1: '2.3.0',
|
||||
version2: '2.4.0',
|
||||
expected: 'lower',
|
||||
},
|
||||
title: 'different minor version',
|
||||
},
|
||||
{
|
||||
context: {
|
||||
version1: '0.1.0',
|
||||
version2: '0.2.0',
|
||||
expected: 'lower',
|
||||
},
|
||||
title: 'different minor version with zero major',
|
||||
},
|
||||
{
|
||||
context: {
|
||||
version1: '2.3.5',
|
||||
version2: '2.4.1',
|
||||
expected: 'lower',
|
||||
},
|
||||
title: 'different minor and patch versions',
|
||||
},
|
||||
{
|
||||
context: {
|
||||
version1: '1.0.0-alpha',
|
||||
version2: '1.1.0-beta',
|
||||
expected: 'lower',
|
||||
},
|
||||
title: 'different minor version with different pre-release tags',
|
||||
},
|
||||
{
|
||||
context: {
|
||||
version1: 'v1.0.0',
|
||||
version2: 'v1.1.0',
|
||||
expected: 'lower',
|
||||
},
|
||||
title: 'different minor version with v prefix',
|
||||
},
|
||||
{
|
||||
context: {
|
||||
version1: '2.0.0',
|
||||
version2: '42.42.42',
|
||||
expected: 'lower',
|
||||
},
|
||||
title: 'above version2',
|
||||
},
|
||||
{
|
||||
context: {
|
||||
version1: '2.0.0',
|
||||
version2: 'v42.42.42',
|
||||
expected: 'lower',
|
||||
},
|
||||
title: 'above version2 with v-prefix',
|
||||
},
|
||||
];
|
||||
|
||||
const sameVersionTestCases: IsSameVersionTestCase[] = [
|
||||
{
|
||||
context: {
|
||||
version1: '1.1.0',
|
||||
version2: '1.1.0',
|
||||
expected: 'equal',
|
||||
},
|
||||
title: 'exact same version',
|
||||
},
|
||||
{
|
||||
context: {
|
||||
version1: '1.1.0',
|
||||
version2: '1.1.42',
|
||||
expected: 'equal',
|
||||
},
|
||||
title: 'exact same major and minor but different patch version',
|
||||
},
|
||||
{
|
||||
context: {
|
||||
version1: 'v1.1.0',
|
||||
version2: 'v1.1.0',
|
||||
expected: 'equal',
|
||||
},
|
||||
title: 'exact same version with v prefix',
|
||||
},
|
||||
{
|
||||
context: {
|
||||
version1: '1.1.0-alpha',
|
||||
version2: '1.1.0-alpha',
|
||||
expected: 'equal',
|
||||
},
|
||||
title: 'exact same version with same pre-release tag',
|
||||
},
|
||||
{
|
||||
context: {
|
||||
version1: '0.0.1',
|
||||
version2: '0.0.1',
|
||||
expected: 'equal',
|
||||
},
|
||||
title: 'exact same version with all zeros',
|
||||
},
|
||||
{
|
||||
context: {
|
||||
version1: 'v1.1.0',
|
||||
version2: '1.1.0',
|
||||
expected: 'equal',
|
||||
},
|
||||
title: 'same version with different v-prefix',
|
||||
},
|
||||
];
|
||||
|
||||
const aboveVersionTestCases: IsSameVersionTestCase[] = [
|
||||
{
|
||||
context: {
|
||||
version1: 'v42.1.0',
|
||||
version2: '2.0.0',
|
||||
expected: 'higher',
|
||||
},
|
||||
title: 'above version',
|
||||
},
|
||||
{
|
||||
context: {
|
||||
version1: '42.42.42',
|
||||
version2: '2.0.0',
|
||||
expected: 'higher',
|
||||
},
|
||||
title: 'above version with prefix',
|
||||
},
|
||||
];
|
||||
const invalidTestCases: IsSameVersionTestCase[] = [
|
||||
{
|
||||
context: {
|
||||
version1: 'invalid',
|
||||
version2: '1.1.0',
|
||||
expectToThrow: true,
|
||||
},
|
||||
title: 'invalid version1',
|
||||
},
|
||||
{
|
||||
context: {
|
||||
version1: '1.0.0',
|
||||
version2: 'invalid',
|
||||
expectToThrow: true,
|
||||
},
|
||||
title: 'invalid version2',
|
||||
},
|
||||
{
|
||||
context: {
|
||||
version1: '1.0',
|
||||
version2: '1.1.0',
|
||||
expectToThrow: true,
|
||||
},
|
||||
title: 'incomplete version1',
|
||||
},
|
||||
];
|
||||
|
||||
test.each([
|
||||
...sameVersionTestCases,
|
||||
...invalidTestCases,
|
||||
...beneathVersionTestCases,
|
||||
...aboveVersionTestCases,
|
||||
])(
|
||||
'$title',
|
||||
({ context: { version1, version2, expected, expectToThrow = false } }) => {
|
||||
if (expectToThrow) {
|
||||
expect(() =>
|
||||
compareVersionMajorAndMinor(version1, version2),
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
} else {
|
||||
expect(compareVersionMajorAndMinor(version1, version2)).toBe(expected);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
@ -0,0 +1,37 @@
|
||||
import * as semver from 'semver';
|
||||
|
||||
type CompareVersionMajorAndMinorReturnType = 'lower' | 'equal' | 'higher';
|
||||
export function compareVersionMajorAndMinor(
|
||||
rawVersion1: string,
|
||||
rawVersion2: string,
|
||||
): CompareVersionMajorAndMinorReturnType {
|
||||
const [version1, version2] = [rawVersion1, rawVersion2].map((version) =>
|
||||
semver.parse(version),
|
||||
);
|
||||
|
||||
if (version1 === null || version2 === null) {
|
||||
throw new Error(`Received invalid version: ${rawVersion1} ${rawVersion2}`);
|
||||
}
|
||||
|
||||
const v1WithoutPatch = `${version1.major}.${version1.minor}.0`;
|
||||
const v2WithoutPatch = `${version2.major}.${version2.minor}.0`;
|
||||
|
||||
const compareResult = semver.compare(v1WithoutPatch, v2WithoutPatch);
|
||||
|
||||
switch (compareResult) {
|
||||
case -1: {
|
||||
return 'lower';
|
||||
}
|
||||
case 0: {
|
||||
return 'equal';
|
||||
}
|
||||
case 1: {
|
||||
return 'higher';
|
||||
}
|
||||
default: {
|
||||
throw new Error(
|
||||
`Should never occur, encountered an unexpected value from semver.compare ${compareResult}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -161,7 +161,7 @@ yarn command:prod cron:calendar:ongoing-stale
|
||||
['SERVER_URL', 'http://localhost:3000', 'Url to the hosted server'],
|
||||
['FRONTEND_URL', '$SERVER_URL', 'Url to the frontend server. Same as SERVER_URL by default'],
|
||||
['PORT', '3000', 'Port of the backend server'],
|
||||
['CACHE_STORAGE_TTL', '3600 * 24 * 7', 'Cache TTL in seconds']
|
||||
['CACHE_STORAGE_TTL', '3600 * 24 * 7', 'Cache TTL in seconds'],
|
||||
]}></ArticleTable>
|
||||
|
||||
### Security
|
||||
|
||||
Reference in New Issue
Block a user