mirror of
https://github.com/n8n-io/n8n.git
synced 2026-06-19 07:36:52 +00:00
ci: Support easy experimental releases (#30947)
Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -10,22 +10,15 @@ const exec = promisify(child_process.exec);
|
||||
|
||||
/**
|
||||
* @param {string | semver.SemVer} currentVersion
|
||||
* @param {string} sha first 8 chars of the commit SHA being released
|
||||
*/
|
||||
export function generateExperimentalVersion(currentVersion) {
|
||||
export function generateExperimentalVersion(currentVersion, sha) {
|
||||
const parsed = semver.parse(currentVersion);
|
||||
if (!parsed) throw new Error(`Invalid version: ${currentVersion}`);
|
||||
if (!sha) throw new Error('sha is required to generate an experimental version');
|
||||
|
||||
// Check if it's already an experimental version
|
||||
if (parsed.prerelease.length > 0 && parsed.prerelease[0] === 'exp') {
|
||||
const minor = parsed.prerelease[1] || 0;
|
||||
const minorInt = typeof minor === 'string' ? parseInt(minor) : minor;
|
||||
// Increment the experimental minor version
|
||||
const expMinor = minorInt + 1;
|
||||
return `${parsed.major}.${parsed.minor}.${parsed.patch}-exp.${expMinor}`;
|
||||
}
|
||||
|
||||
// Create new experimental version: <major>.<minor>.<patch>-exp.0
|
||||
return `${parsed.major}.${parsed.minor}.${parsed.patch}-exp.0`;
|
||||
// Prefix the exp sha with a g to prevent invalid semver from a leading 0 (e.g. -exp.01234567)
|
||||
return `${parsed.major}.${parsed.minor}.${parsed.patch}-exp.g${sha}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,12 +148,13 @@ export function propagateDirtyTransitively(packageMap, depsByPackage) {
|
||||
/**
|
||||
* @param {string} version
|
||||
* @param {import('semver').ReleaseType | 'experimental'} releaseType
|
||||
* @param {string} [sha] required when releaseType is 'experimental'
|
||||
* @returns {string}
|
||||
*/
|
||||
export function computeNewVersion(version, releaseType) {
|
||||
export function computeNewVersion(version, releaseType, sha) {
|
||||
switch (releaseType) {
|
||||
case 'experimental':
|
||||
return generateExperimentalVersion(version);
|
||||
return generateExperimentalVersion(version, /** @type {string} */ (sha));
|
||||
case 'premajor':
|
||||
return /** @type {string} */ (
|
||||
semver.inc(
|
||||
@@ -186,6 +180,10 @@ async function bumpVersions() {
|
||||
// TODO: if releaseType is `auto` determine release type based on the changelog
|
||||
|
||||
const lastTag = (await exec('git describe --tags --match "n8n@*" --abbrev=0')).stdout.trim();
|
||||
const sha =
|
||||
releaseType === 'experimental'
|
||||
? (await exec('git rev-parse --short=8 HEAD')).stdout.trim()
|
||||
: undefined;
|
||||
const packages = JSON.parse(
|
||||
(
|
||||
await exec(
|
||||
@@ -277,7 +275,7 @@ async function bumpVersions() {
|
||||
let newVersion = version;
|
||||
|
||||
if (isDirty || dependencyIsDirty) {
|
||||
newVersion = computeNewVersion(version, releaseType);
|
||||
newVersion = computeNewVersion(version, releaseType, sha);
|
||||
}
|
||||
|
||||
packageJson.version = packageMap[packageName].nextVersion = newVersion;
|
||||
|
||||
@@ -19,28 +19,32 @@ import {
|
||||
} from './bump-versions.mjs';
|
||||
|
||||
describe('generateExperimentalVersion', () => {
|
||||
it('creates -exp.0 from a stable version', () => {
|
||||
assert.equal(generateExperimentalVersion('1.2.3'), '1.2.3-exp.0');
|
||||
it('appends -exp.g<sha> to a stable version', () => {
|
||||
assert.equal(generateExperimentalVersion('1.2.3', 'abcd1234'), '1.2.3-exp.gabcd1234');
|
||||
});
|
||||
|
||||
it('increments exp minor when already at exp.0', () => {
|
||||
assert.equal(generateExperimentalVersion('1.2.3-exp.0'), '1.2.3-exp.1');
|
||||
it('replaces an existing exp.g<sha> with the new sha', () => {
|
||||
assert.equal(
|
||||
generateExperimentalVersion('1.2.3-exp.deadbeef', 'abcd1234'),
|
||||
'1.2.3-exp.gabcd1234',
|
||||
);
|
||||
});
|
||||
|
||||
it('increments exp minor when already at exp.5', () => {
|
||||
assert.equal(generateExperimentalVersion('1.2.3-exp.5'), '1.2.3-exp.6');
|
||||
});
|
||||
|
||||
it('creates -exp.0 from a version with a different pre-release tag', () => {
|
||||
assert.equal(generateExperimentalVersion('1.2.3-beta.1'), '1.2.3-exp.0');
|
||||
it('replaces a different pre-release tag with -exp.g<sha>', () => {
|
||||
assert.equal(generateExperimentalVersion('1.2.3-beta.1', 'abcd1234'), '1.2.3-exp.gabcd1234');
|
||||
});
|
||||
|
||||
it('handles multi-digit version numbers', () => {
|
||||
assert.equal(generateExperimentalVersion('10.20.30'), '10.20.30-exp.0');
|
||||
assert.equal(generateExperimentalVersion('10.20.30', 'abcd1234'), '10.20.30-exp.gabcd1234');
|
||||
});
|
||||
|
||||
it('throws on an invalid version string', () => {
|
||||
assert.throws(() => generateExperimentalVersion('not-a-version'), /Invalid version/);
|
||||
assert.throws(() => generateExperimentalVersion('not-a-version', 'abcd1234'), /Invalid version/);
|
||||
});
|
||||
|
||||
it('throws when sha is missing', () => {
|
||||
// @ts-expect-error - intentionally calling without sha to verify the guard
|
||||
assert.throws(() => generateExperimentalVersion('1.2.3'), /sha is required/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -358,12 +362,15 @@ describe('computeNewVersion', () => {
|
||||
assert.equal(computeNewVersion('1.2.3', 'major'), '2.0.0');
|
||||
});
|
||||
|
||||
it('creates -exp.0 from a stable version for experimental', () => {
|
||||
assert.equal(computeNewVersion('1.2.3', 'experimental'), '1.2.3-exp.0');
|
||||
it('appends -exp.g<sha> to a stable version for experimental', () => {
|
||||
assert.equal(computeNewVersion('1.2.3', 'experimental', 'abcd1234'), '1.2.3-exp.gabcd1234');
|
||||
});
|
||||
|
||||
it('increments exp minor for experimental when already an exp version', () => {
|
||||
assert.equal(computeNewVersion('1.2.3-exp.0', 'experimental'), '1.2.3-exp.1');
|
||||
it('replaces an existing exp.g<sha> with the new sha for experimental', () => {
|
||||
assert.equal(
|
||||
computeNewVersion('1.2.3-exp.deadbeef', 'experimental', 'abcd1234'),
|
||||
'1.2.3-exp.gabcd1234',
|
||||
);
|
||||
});
|
||||
|
||||
it('creates a premajor rc version from a stable version', () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import packageJson from '../../package.json' with { type: 'json' };
|
||||
|
||||
const baseDir = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const fullChangelogFile = resolve(baseDir, 'CHANGELOG.md');
|
||||
// Version includes experimental versions (e.g., 1.2.3-exp.0)
|
||||
// Version includes experimental versions (e.g., 1.2.3-exp.g123abcgg)
|
||||
const versionChangelogFile = resolve(baseDir, `CHANGELOG-${packageJson.version}.md`);
|
||||
|
||||
const changelogStream = new ConventionalChangelog()
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
name: 'Release: Create Experiment PR'
|
||||
run-name: 'Release: Create Experiment Release PR'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
commits:
|
||||
description: 'Space-separated list of commit SHAs to cherry-pick onto the new experimental branch.'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
prepare-experiment-branch:
|
||||
name: Prepare experiment branch
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
outputs:
|
||||
experiment-branch: ${{ steps.cherry-pick.outputs.experiment-branch }}
|
||||
steps:
|
||||
- name: Generate GitHub App Token
|
||||
id: generate_token
|
||||
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
|
||||
with:
|
||||
app-id: ${{ secrets.N8N_ASSISTANT_APP_ID }}
|
||||
private-key: ${{ secrets.N8N_ASSISTANT_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.generate_token.outputs.token }}
|
||||
|
||||
- name: Branch off stable, cherry-pick, and push
|
||||
id: cherry-pick
|
||||
env:
|
||||
COMMITS: ${{ inputs.commits }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# The `stable` ref is a moving git tag pointing at the latest stable release commit.
|
||||
STABLE_COMMIT="$(git rev-parse 'stable^{}')"
|
||||
STABLE_TAG="$(git tag --points-at "$STABLE_COMMIT" | grep '^n8n@' | sort -V | tail -n1)"
|
||||
|
||||
if [ -z "$STABLE_TAG" ]; then
|
||||
echo "Could not resolve stable n8n@... tag at commit $STABLE_COMMIT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
STABLE_VERSION="${STABLE_TAG#n8n@}"
|
||||
EXP_BRANCH="exp/${STABLE_VERSION}-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}"
|
||||
|
||||
echo "Stable tag: $STABLE_TAG"
|
||||
echo "Experiment branch: $EXP_BRANCH"
|
||||
|
||||
git config user.email "n8n-assistant-bot@users.noreply.github.com"
|
||||
git config user.name "n8n-assistant"
|
||||
|
||||
git switch --detach "$STABLE_TAG"
|
||||
git switch --create "$EXP_BRANCH"
|
||||
|
||||
for sha in $COMMITS; do
|
||||
echo "::group::Cherry-picking $sha"
|
||||
git cherry-pick -X theirs "$sha"
|
||||
echo "::endgroup::"
|
||||
done
|
||||
|
||||
git push origin "$EXP_BRANCH"
|
||||
|
||||
echo "experiment-branch=$EXP_BRANCH" >> "$GITHUB_OUTPUT"
|
||||
|
||||
create-release-pr:
|
||||
name: Create release PR
|
||||
needs: [prepare-experiment-branch]
|
||||
uses: ./.github/workflows/release-create-pr.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
base-branch: ${{ needs.prepare-experiment-branch.outputs.experiment-branch }}
|
||||
release-type: experimental
|
||||
Reference in New Issue
Block a user