mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-31 03:47:57 +08:00
ci: Clarify and automate release process (#26181)
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { RELEASE_TRACKS, resolveReleaseTagForTrack, writeGithubOutput } from './github-helpers.mjs';
|
||||
import semver from 'semver';
|
||||
|
||||
/**
|
||||
* @param {any} packageVersion
|
||||
*/
|
||||
export function determineTrack(packageVersion) {
|
||||
if (!semver.valid(packageVersion)) {
|
||||
throw new Error(`Package semver not valid. Got ${packageVersion}`);
|
||||
}
|
||||
|
||||
/** @type { Partial<Record<import('./github-helpers.mjs').ReleaseTrack, import('./github-helpers.mjs').TagVersionInfo>> } */
|
||||
const trackToReleaseMap = {};
|
||||
for (const t of RELEASE_TRACKS) {
|
||||
trackToReleaseMap[t] = resolveReleaseTagForTrack(t);
|
||||
}
|
||||
|
||||
console.log('Current Tracks: ', JSON.stringify(trackToReleaseMap, null, 4));
|
||||
|
||||
let track = null;
|
||||
let newStable = null;
|
||||
let bump = determineBump(packageVersion);
|
||||
const releaseType = determineReleaseType(packageVersion);
|
||||
|
||||
// Check through our current release versions, if semver matches,
|
||||
// we inherit the track pointer from them
|
||||
for (const [releaseTrack, tagVersionInfo] of Object.entries(trackToReleaseMap)) {
|
||||
if (tagVersionInfo && matchesTrack(tagVersionInfo, packageVersion)) {
|
||||
track = releaseTrack;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!track) {
|
||||
if (!trackToReleaseMap.beta?.version) {
|
||||
throw new Error(
|
||||
'Likely updating to new beta release, but no existing beta tag was found in git.',
|
||||
);
|
||||
}
|
||||
// If not track was found in current versions, we verify we're building a
|
||||
// new beta version and the input is not invalid.
|
||||
assertNewBetaRelease(trackToReleaseMap.beta.version, packageVersion);
|
||||
|
||||
track = 'beta';
|
||||
newStable = trackToReleaseMap.beta.version;
|
||||
}
|
||||
|
||||
if (!track) {
|
||||
throw new Error('Could not determine track for release. Exiting...');
|
||||
}
|
||||
|
||||
const output = {
|
||||
version: packageVersion,
|
||||
track,
|
||||
bump,
|
||||
new_stable_version: newStable,
|
||||
release_type: releaseType,
|
||||
};
|
||||
|
||||
writeGithubOutput(output);
|
||||
console.log(
|
||||
`Determined track info: track=${track}, version=${packageVersion}, new_stable_version=${newStable}, release_type=${releaseType}`,
|
||||
);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* The current version matches the track, if their Major and Minor semvers match.
|
||||
*
|
||||
* This means that we are working with a patch release
|
||||
*
|
||||
* @param {import("./github-helpers.mjs").TagVersionInfo} tagVersionInfo
|
||||
* @param {any} currentVersion
|
||||
*/
|
||||
function matchesTrack(tagVersionInfo, currentVersion) {
|
||||
if (semver.major(tagVersionInfo.version) !== semver.major(currentVersion)) {
|
||||
return false;
|
||||
}
|
||||
if (semver.minor(tagVersionInfo.version) !== semver.minor(currentVersion)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} currentBetaVersion
|
||||
* @param {any} currentVersion
|
||||
*/
|
||||
function assertNewBetaRelease(currentBetaVersion, currentVersion) {
|
||||
if (semver.major(currentBetaVersion) !== semver.major(currentVersion)) {
|
||||
throw new Error('Major version bumps are not allowed by this pipeline');
|
||||
}
|
||||
|
||||
const bumpedCurrentBeta = semver.inc(currentBetaVersion, 'minor');
|
||||
if (semver.minor(bumpedCurrentBeta) !== semver.minor(currentVersion)) {
|
||||
throw new Error(
|
||||
`Trying to upgrade minor version by more than one increment. Previous: ${bumpedCurrentBeta}, Requested: ${currentVersion}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function determineReleaseType(currentVersion) {
|
||||
if (currentVersion.includes('-rc.')) {
|
||||
return 'rc';
|
||||
}
|
||||
return 'stable';
|
||||
}
|
||||
|
||||
function determineBump(currentVersion) {
|
||||
if (semver.patch(currentVersion) === 0 && determineReleaseType(currentVersion) != 'rc') {
|
||||
return 'minor';
|
||||
}
|
||||
return 'patch';
|
||||
}
|
||||
|
||||
// only run when executed directly, not when imported by tests
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
const packageJson = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||
determineTrack(packageJson.version);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { describe, it, mock, before } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
/**
|
||||
* Run these tests by running
|
||||
*
|
||||
* node --test --experimental-test-module-mocks ./.github/scripts/determine-version-info.test.mjs
|
||||
* */
|
||||
|
||||
// mock.module must be called before the module under test is imported,
|
||||
// because static imports are hoisted and resolve before any code runs.
|
||||
mock.module('./github-helpers.mjs', {
|
||||
namedExports: {
|
||||
RELEASE_TRACKS: ['stable', 'beta', 'v1'],
|
||||
resolveReleaseTagForTrack: (track) => {
|
||||
// Always return deterministic data
|
||||
if (track === 'stable') return { version: '2.9.2' };
|
||||
if (track === 'beta') return { version: '2.10.0' };
|
||||
return { version: '1.123.33' };
|
||||
},
|
||||
writeGithubOutput: () => {}, // no-op in tests
|
||||
},
|
||||
});
|
||||
|
||||
let determineTrack;
|
||||
before(async () => {
|
||||
({ determineTrack } = await import('./determine-version-info.mjs'));
|
||||
});
|
||||
|
||||
describe('determine-tracks', () => {
|
||||
it('Allow patch releases on stable', () => {
|
||||
const output = determineTrack('2.9.3');
|
||||
|
||||
assert.equal(output.track, 'stable');
|
||||
assert.equal(output.version, '2.9.3');
|
||||
assert.equal(output.bump, 'patch');
|
||||
assert.equal(output.new_stable_version, null);
|
||||
assert.equal(output.release_type, 'stable');
|
||||
});
|
||||
|
||||
it('Allow patch releases on beta', () => {
|
||||
const output = determineTrack('2.10.1');
|
||||
|
||||
assert.equal(output.track, 'beta');
|
||||
assert.equal(output.version, '2.10.1');
|
||||
assert.equal(output.bump, 'patch');
|
||||
assert.equal(output.new_stable_version, null);
|
||||
assert.equal(output.release_type, 'stable');
|
||||
});
|
||||
|
||||
// This use case might happen if a patch release fails and we proceed with rolling over to next release
|
||||
it('Allow skipping versions in patches', () => {
|
||||
const output = determineTrack('2.9.4');
|
||||
|
||||
assert.equal(output.track, 'stable');
|
||||
assert.equal(output.version, '2.9.4');
|
||||
assert.equal(output.bump, 'patch');
|
||||
assert.equal(output.new_stable_version, null);
|
||||
assert.equal(output.release_type, 'stable');
|
||||
});
|
||||
|
||||
it('Disallow skipping versions in minors', () => {
|
||||
assert.throws(() => determineTrack('2.12.0'));
|
||||
});
|
||||
it('Disallow changing major version', () => {
|
||||
assert.throws(() => determineTrack('3.0.0'));
|
||||
});
|
||||
it('Throw when track is not determinable', () => {
|
||||
assert.throws(() => determineTrack(''));
|
||||
});
|
||||
|
||||
it('Set track as "beta" when doing a minor bump', () => {
|
||||
const output = determineTrack('2.11.0');
|
||||
|
||||
assert.equal(output.track, 'beta');
|
||||
assert.equal(output.version, '2.11.0');
|
||||
assert.equal(output.bump, 'minor');
|
||||
assert.equal(output.new_stable_version, '2.10.0');
|
||||
assert.equal(output.release_type, 'stable');
|
||||
});
|
||||
|
||||
it('Set release_type accordingly on rc releases', () => {
|
||||
const output = determineTrack('2.10.1-rc.1');
|
||||
|
||||
assert.equal(output.track, 'beta');
|
||||
assert.equal(output.version, '2.10.1-rc.1');
|
||||
assert.equal(output.bump, 'patch');
|
||||
assert.equal(output.new_stable_version, null);
|
||||
assert.equal(output.release_type, 'rc');
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,22 @@ export const RELEASE_TRACKS = /** @type { const } */ ([
|
||||
'v1',
|
||||
]);
|
||||
|
||||
/**
|
||||
* @typedef {typeof RELEASE_TRACKS[number]} ReleaseTrack
|
||||
* */
|
||||
|
||||
/**
|
||||
* @typedef {`${number}.${number}.${number}`} SemVer
|
||||
* */
|
||||
|
||||
/**
|
||||
* @typedef {`${RELEASE_PREFIX}${SemVer}`} ReleaseVersion
|
||||
* */
|
||||
|
||||
/**
|
||||
* @typedef {{ tag: ReleaseVersion, version: SemVer}} TagVersionInfo
|
||||
* */
|
||||
|
||||
export const RELEASE_PREFIX = 'n8n@';
|
||||
|
||||
/**
|
||||
@@ -16,6 +32,8 @@ export const RELEASE_PREFIX = 'n8n@';
|
||||
* Returns the *tag string* (e.g. "n8n@2.7.0") or null.
|
||||
*
|
||||
* @param {string[]} tags
|
||||
*
|
||||
* @returns { ReleaseVersion | null }
|
||||
* */
|
||||
export function pickHighestReleaseTag(tags) {
|
||||
const versions = tags
|
||||
@@ -24,13 +42,13 @@ export function pickHighestReleaseTag(tags) {
|
||||
.filter(({ v }) => semver.valid(v))
|
||||
.sort((a, b) => semver.rcompare(a.v, b.v));
|
||||
|
||||
return versions[0]?.tag ?? null;
|
||||
return /** @type { ReleaseVersion } */ (versions[0]?.tag) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} track
|
||||
* @param {any} track
|
||||
*
|
||||
* @returns { typeof RELEASE_TRACKS[number] }
|
||||
* @returns { ReleaseTrack }
|
||||
* */
|
||||
export function ensureReleaseTrack(track) {
|
||||
if (!RELEASE_TRACKS.includes(track)) {
|
||||
@@ -40,6 +58,30 @@ export function ensureReleaseTrack(track) {
|
||||
return track;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a release track tag (stable/beta/etc.) to the corresponding
|
||||
* n8n@x.y.z tag pointing at the same commit.
|
||||
*
|
||||
* Returns null if the track tag or release tag is missing.
|
||||
*
|
||||
* @param { typeof RELEASE_TRACKS[number] } track
|
||||
*
|
||||
* @returns { TagVersionInfo }
|
||||
* */
|
||||
export function resolveReleaseTagForTrack(track) {
|
||||
const commit = getCommitForRef(track);
|
||||
if (!commit) return null;
|
||||
|
||||
const tagsAtCommit = listTagsPointingAt(commit);
|
||||
const releaseTag = pickHighestReleaseTag(tagsAtCommit);
|
||||
if (!releaseTag) return null;
|
||||
|
||||
return {
|
||||
tag: releaseTag,
|
||||
version: stripReleasePrefixes(releaseTag),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a release track tag (stable/beta/etc.) to the corresponding
|
||||
* release-candidate/<major>.<minor>.x branch, based on the n8n@<x.y.z> tag
|
||||
@@ -66,9 +108,13 @@ export function resolveRcBranchForTrack(track) {
|
||||
|
||||
/**
|
||||
* @param {string} tag
|
||||
*
|
||||
* @returns { SemVer }
|
||||
* */
|
||||
export function stripReleasePrefixes(tag) {
|
||||
return tag.startsWith(RELEASE_PREFIX) ? tag.slice(RELEASE_PREFIX.length) : tag;
|
||||
return /** @type { SemVer } */ (
|
||||
tag.startsWith(RELEASE_PREFIX) ? tag.slice(RELEASE_PREFIX.length) : tag
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "esnext",
|
||||
"target": "esnext",
|
||||
"checkJs": true,
|
||||
"moduleResolution": "node"
|
||||
},
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user