[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:
Paul Rastoin
2025-03-13 15:46:27 +01:00
committed by GitHub
parent 15019d2c66
commit bd5d211590
14 changed files with 884 additions and 51 deletions

View File

@ -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"`;

View File

@ -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);
}
},
);
});

View File

@ -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}`,
);
}
}
}