mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
ci: Initialize ci helpers and support scripts for ci automation via mjs (#25931)
This commit is contained in:
parent
bd92dabc5c
commit
581f74014b
8
.github/actions/setup-nodejs/action.yml
vendored
8
.github/actions/setup-nodejs/action.yml
vendored
|
|
@ -18,6 +18,10 @@ inputs:
|
|||
description: 'Command to execute for building the project or an optional command. Leave empty to skip build step.'
|
||||
required: false
|
||||
default: 'pnpm build'
|
||||
install-command:
|
||||
description: 'Command to execute for installing project dependencies. Leave empty to skip install step.'
|
||||
required: false
|
||||
default: 'pnpm install --frozen-lockfile'
|
||||
|
||||
runs:
|
||||
using: 'composite'
|
||||
|
|
@ -44,7 +48,9 @@ runs:
|
|||
shell: bash
|
||||
|
||||
- name: Install Dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
if: ${{ inputs.install-command != '' }}
|
||||
run: |
|
||||
${{ inputs.install-command }}
|
||||
shell: bash
|
||||
|
||||
- name: Disable safe-chain
|
||||
|
|
|
|||
73
.github/scripts/create-release-candidate-branch.mjs
vendored
Normal file
73
.github/scripts/create-release-candidate-branch.mjs
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import semver from 'semver';
|
||||
import {
|
||||
ensureEnvVar,
|
||||
RELEASE_PREFIX,
|
||||
sh,
|
||||
stripReleasePrefixes,
|
||||
trySh,
|
||||
writeGithubOutput,
|
||||
} from './github-helpers.mjs';
|
||||
|
||||
function remoteBranchExists(branch) {
|
||||
const res = trySh('git', ['ls-remote', '--heads', 'origin', branch]);
|
||||
return res.ok && res.out.length > 0;
|
||||
}
|
||||
|
||||
function localRefExists(ref) {
|
||||
const res = trySh('git', ['show-ref', '--verify', '--quiet', ref]);
|
||||
return res.ok;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const rawVersion = ensureEnvVar('VERSION');
|
||||
|
||||
const version = stripReleasePrefixes(rawVersion);
|
||||
if (!semver.valid(version)) {
|
||||
throw new Error(
|
||||
`Invalid VERSION. Expected semver like X.Y.Z (optionally prefixed by n8n@). Got: ${rawVersion}`,
|
||||
);
|
||||
}
|
||||
|
||||
const major = semver.major(version);
|
||||
const minor = semver.minor(version);
|
||||
const branch = `release-candidate/${major}.${minor}.x`;
|
||||
|
||||
// We create the RC branch from the corresponding release tag
|
||||
const releaseTag = `${RELEASE_PREFIX}${version}`;
|
||||
|
||||
if (remoteBranchExists(branch)) {
|
||||
console.log(`Branch already exists on origin: ${branch}`);
|
||||
writeGithubOutput({ branch });
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure the tag exists locally (as a tag ref)
|
||||
// `git rev-parse <tag>^{}` will fail if it doesn't exist.
|
||||
const tagCommitRes = trySh('git', ['rev-parse', `${releaseTag}^{}`]);
|
||||
if (!tagCommitRes.ok || !tagCommitRes.out) {
|
||||
throw new Error(
|
||||
`Cannot find release tag "${releaseTag}". Make sure the tag exists and has been pushed.`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log(`Creating branch ${branch} from ${releaseTag} (${tagCommitRes.out})`);
|
||||
|
||||
// Create local branch (force safe: it shouldn't exist, but keep it robust)
|
||||
if (localRefExists(`refs/heads/${branch}`)) {
|
||||
sh('git', ['branch', '-f', branch, tagCommitRes.out]);
|
||||
} else {
|
||||
sh('git', ['switch', '-c', branch, tagCommitRes.out]);
|
||||
}
|
||||
|
||||
sh('git', ['push', 'origin', branch]);
|
||||
|
||||
console.log(`Created and pushed: ${branch}`);
|
||||
writeGithubOutput({ branch });
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (err) {
|
||||
console.error(String(err?.message ?? err));
|
||||
process.exit(1);
|
||||
}
|
||||
63
.github/scripts/get-release-versions.mjs
vendored
Normal file
63
.github/scripts/get-release-versions.mjs
vendored
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import semver from 'semver';
|
||||
import {
|
||||
getCommitForRef,
|
||||
listTagsPointingAt,
|
||||
RELEASE_PREFIX,
|
||||
RELEASE_TRACKS,
|
||||
stripReleasePrefixes,
|
||||
writeGithubOutput,
|
||||
} from './github-helpers.mjs';
|
||||
|
||||
/**
|
||||
* Given a list of tag names, return the highest semver tag (keeping the original 'v' prefix),
|
||||
* or "" if none match semver.
|
||||
*
|
||||
* @param {string[]} tags
|
||||
**/
|
||||
function highestSemverTag(tags) {
|
||||
const candidates = tags
|
||||
.filter((t) => t.startsWith(RELEASE_PREFIX))
|
||||
.map((t) => ({
|
||||
tag: t,
|
||||
version: stripReleasePrefixes(t),
|
||||
}))
|
||||
.filter(({ version }) => semver.valid(version));
|
||||
|
||||
if (candidates.length === 0) return '';
|
||||
|
||||
candidates.sort((a, b) => semver.rcompare(a.version, b.version));
|
||||
return candidates[0]?.tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} track
|
||||
**/
|
||||
function getSemverTagForTrack(track) {
|
||||
const commit = getCommitForRef(track);
|
||||
if (!commit) return '';
|
||||
|
||||
const tags = listTagsPointingAt(commit);
|
||||
return highestSemverTag(tags);
|
||||
}
|
||||
|
||||
function main() {
|
||||
/** @type { Record<string, string> } */
|
||||
const outputs = {};
|
||||
for (const track of RELEASE_TRACKS) {
|
||||
outputs[track] = getSemverTagForTrack(track);
|
||||
}
|
||||
|
||||
writeGithubOutput(outputs);
|
||||
|
||||
console.log('Current release versions: ');
|
||||
for (const [k, v] of Object.entries(outputs)) {
|
||||
console.log(`${k}: ${v || '(not found)'}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch (err) {
|
||||
console.error(String(err?.message ?? err));
|
||||
process.exit(1);
|
||||
}
|
||||
183
.github/scripts/github-helpers.mjs
vendored
Normal file
183
.github/scripts/github-helpers.mjs
vendored
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import semver from 'semver';
|
||||
|
||||
export const RELEASE_TRACKS = /** @type { const } */ ([
|
||||
//
|
||||
'stable',
|
||||
'beta',
|
||||
'v1',
|
||||
]);
|
||||
|
||||
export const RELEASE_PREFIX = 'n8n@';
|
||||
|
||||
/**
|
||||
* Given a list of tags, return the highest semver for tags like "n8n@2.7.0".
|
||||
* Returns the *tag string* (e.g. "n8n@2.7.0") or null.
|
||||
*
|
||||
* @param {string[]} tags
|
||||
* */
|
||||
export function pickHighestReleaseTag(tags) {
|
||||
const versions = tags
|
||||
.filter((t) => t.startsWith(RELEASE_PREFIX))
|
||||
.map((t) => ({ tag: t, v: stripReleasePrefixes(t) }))
|
||||
.filter(({ v }) => semver.valid(v))
|
||||
.sort((a, b) => semver.rcompare(a.v, b.v));
|
||||
|
||||
return versions[0]?.tag ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} track
|
||||
*
|
||||
* @returns { typeof RELEASE_TRACKS[number] }
|
||||
* */
|
||||
export function ensureReleaseTrack(track) {
|
||||
if (!RELEASE_TRACKS.includes(track)) {
|
||||
throw new Error(`Invalid track ${track}. Available tracks are ${RELEASE_TRACKS.join(', ')}`);
|
||||
}
|
||||
|
||||
return track;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* pointing at the same commit.
|
||||
*
|
||||
* Returns null if the track tag or release tag is missing.
|
||||
*
|
||||
* @param { typeof RELEASE_TRACKS[number] } track
|
||||
* */
|
||||
export function resolveRcBranchForTrack(track) {
|
||||
const commit = getCommitForRef(track);
|
||||
if (!commit) return null;
|
||||
|
||||
const tagsAtCommit = listTagsPointingAt(commit);
|
||||
const releaseTag = pickHighestReleaseTag(tagsAtCommit);
|
||||
if (!releaseTag) return null;
|
||||
|
||||
const version = stripReleasePrefixes(releaseTag);
|
||||
const parsed = semver.parse(version);
|
||||
if (!parsed) return null;
|
||||
|
||||
return `release-candidate/${parsed.major}.${parsed.minor}.x`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} tag
|
||||
* */
|
||||
export function stripReleasePrefixes(tag) {
|
||||
return tag.startsWith(RELEASE_PREFIX) ? tag.slice(RELEASE_PREFIX.length) : tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns { string[] }
|
||||
* */
|
||||
export function readPrLabels() {
|
||||
const eventPath = ensureEnvVar('GITHUB_EVENT_PATH');
|
||||
|
||||
const event = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
|
||||
/** @type { string[] | { name: string }[] } */
|
||||
const labels = event?.pull_request?.labels ?? [];
|
||||
|
||||
return labels.map((l) => (typeof l === 'string' ? l : l?.name)).filter(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures git tag exists.
|
||||
*
|
||||
* @throws { Error } if no tag was found
|
||||
* */
|
||||
export function ensureTagExists(tag) {
|
||||
sh('git', ['fetch', '--force', 'origin', `refs/tags/${tag}:refs/tags/${tag}`]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} bump
|
||||
*
|
||||
* @returns { bump is import("semver").ReleaseType }
|
||||
* */
|
||||
export function isReleaseType(bump) {
|
||||
return ['major', 'minor', 'patch'].includes(bump);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} variableName
|
||||
*/
|
||||
export function ensureEnvVar(variableName) {
|
||||
const v = process.env[variableName];
|
||||
if (!v) {
|
||||
throw new Error(`Missing required env var: ${variableName}`);
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} cmd
|
||||
* @param {readonly string[]} args
|
||||
* @param {import("node:child_process").ExecFileOptionsWithStringEncoding} args
|
||||
*
|
||||
* @example sh("git", ["tag", "--points-at", commit]);
|
||||
* */
|
||||
export function sh(cmd, args, opts = {}) {
|
||||
return execFileSync(cmd, args, { encoding: 'utf8', ...opts }).trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} cmd
|
||||
* @param {readonly string[]} args
|
||||
* @param {import("node:child_process").ExecFileOptionsWithStringEncoding} args
|
||||
*
|
||||
* @example trySh("git", ["tag", "--points-at", commit]);
|
||||
* */
|
||||
export function trySh(cmd, args, opts = {}) {
|
||||
try {
|
||||
return { ok: true, out: sh(cmd, args, opts) };
|
||||
} catch {
|
||||
return { ok: false, out: '' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Append outputs to GITHUB_OUTPUT if available.
|
||||
*
|
||||
* @param {Record<string, string>} obj
|
||||
*/
|
||||
export function writeGithubOutput(obj) {
|
||||
const path = process.env.GITHUB_OUTPUT;
|
||||
if (!path) return;
|
||||
|
||||
const lines = Object.entries(obj)
|
||||
.map(([k, v]) => `${k}=${v ?? ''}`)
|
||||
.join('\n');
|
||||
|
||||
fs.appendFileSync(path, lines + '\n', 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a ref (tag/branch/SHA) to the underlying commit SHA.
|
||||
* Uses ^{} so annotated tags are peeled to the commit.
|
||||
* Returns null if ref doesn't exist.
|
||||
*
|
||||
* @param {string} ref
|
||||
*/
|
||||
export function getCommitForRef(ref) {
|
||||
const res = trySh('git', ['rev-parse', `${ref}^{}`]);
|
||||
return res.ok && res.out ? res.out : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* List all tags that point at the given commit SHA.
|
||||
*
|
||||
* @param {string} commit
|
||||
*/
|
||||
export function listTagsPointingAt(commit) {
|
||||
const res = trySh('git', ['tag', '--points-at', commit]);
|
||||
if (!res.ok || !res.out) return [];
|
||||
|
||||
return res.out
|
||||
.split('\n')
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
18
.github/scripts/move-track-tag.mjs
vendored
Normal file
18
.github/scripts/move-track-tag.mjs
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { ensureEnvVar, ensureReleaseTrack, ensureTagExists, sh } from './github-helpers.mjs';
|
||||
|
||||
function main() {
|
||||
const trackEnv = ensureEnvVar('TRACK');
|
||||
const track = ensureReleaseTrack(trackEnv);
|
||||
|
||||
const versionInput = ensureEnvVar('VERSION_TAG'); // e.g. n8n@2.7.0
|
||||
|
||||
ensureTagExists(versionInput);
|
||||
|
||||
sh('git', ['tag', '-f', track, versionInput]);
|
||||
|
||||
sh('git', ['push', 'origin', '-f', `refs/tags/${track}:refs/tags/${track}`]);
|
||||
|
||||
console.log(`Moved pointer tag ${track} to point to ${versionInput}`);
|
||||
}
|
||||
|
||||
main();
|
||||
62
.github/scripts/plan-release.mjs
vendored
Normal file
62
.github/scripts/plan-release.mjs
vendored
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import semver from 'semver';
|
||||
import {
|
||||
ensureEnvVar,
|
||||
isReleaseType,
|
||||
RELEASE_PREFIX,
|
||||
stripReleasePrefixes,
|
||||
writeGithubOutput,
|
||||
} from './github-helpers.mjs';
|
||||
|
||||
const track = ensureEnvVar('TRACK');
|
||||
const bump = ensureEnvVar('BUMP');
|
||||
|
||||
const stable = process.env['STABLE_VERSION'];
|
||||
const beta = process.env['BETA_VERSION'];
|
||||
const v1 = process.env['V1_VERSION'];
|
||||
|
||||
let base = null;
|
||||
switch (track) {
|
||||
case 'stable':
|
||||
base = stable;
|
||||
break;
|
||||
case 'beta':
|
||||
base = beta;
|
||||
break;
|
||||
case 'v1':
|
||||
base = v1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!base) {
|
||||
console.error(
|
||||
`Unknown track or missing base version. track=${track} stable=${stable} beta=${beta} v1=${v1}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const cleanedBase = stripReleasePrefixes(base);
|
||||
if (!cleanedBase) {
|
||||
console.error(`Invalid base version: ${base}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!isReleaseType(bump)) {
|
||||
console.error(`Invalid release type in $bump: ${bump}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const next = semver.inc(cleanedBase, bump);
|
||||
if (!next) {
|
||||
console.error(`Could not bump version. base=${cleanedBase} bump=${bump}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const output = {
|
||||
base_version: cleanedBase,
|
||||
new_version: next,
|
||||
new_version_tag: `${RELEASE_PREFIX}${next}`,
|
||||
};
|
||||
|
||||
writeGithubOutput(output);
|
||||
|
||||
console.log(`Releasing track=${track} bump=${bump} base=${cleanedBase} -> new=${next}`);
|
||||
52
.github/workflows/ensure-release-candidate-branch.yml
vendored
Normal file
52
.github/workflows/ensure-release-candidate-branch.yml
vendored
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
name: Ensure release-candidate branch
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Latest release version (e.g. 2.8.1 or n8n@2.8.1)'
|
||||
required: true
|
||||
type: string
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
description: 'Latest release version (e.g. 2.8.1 or n8n@2.8.1)'
|
||||
required: true
|
||||
type: string
|
||||
outputs:
|
||||
branch:
|
||||
description: 'release-candidate branch name (e.g. release-candidate/2.8.x)'
|
||||
value: ${{ jobs.ensure.outputs.branch }}
|
||||
|
||||
jobs:
|
||||
ensure:
|
||||
runs-on: ubuntu-slim
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
outputs:
|
||||
branch: ${{ steps.rc.outputs.branch }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Setup NodeJS
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
install-command: npm install --prefix=.github/scripts --no-package-lock
|
||||
build-command: ''
|
||||
|
||||
- name: Ensure release-candidate branch exists
|
||||
id: rc
|
||||
env:
|
||||
VERSION: ${{ inputs.version }}
|
||||
run: node ./.github/scripts/create-release-candidate-branch.mjs
|
||||
|
||||
- name: Print branch
|
||||
run: |
|
||||
echo "Release-candidate branch: ${{ steps.rc.outputs.branch }}"
|
||||
54
.github/workflows/release-update-pointer-tag.yml
vendored
Normal file
54
.github/workflows/release-update-pointer-tag.yml
vendored
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
name: 'Release: Update pointer tag'
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
track:
|
||||
required: true
|
||||
type: string
|
||||
version-tag:
|
||||
required: true
|
||||
type: string
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
track:
|
||||
description: 'Release Track'
|
||||
required: true
|
||||
type: choice
|
||||
options: [stable, beta, v1]
|
||||
version-tag:
|
||||
description: 'Version tag (e.g. n8n@2.7.0). Track tag will point to this version tag.'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
update-pointer-tags:
|
||||
name: Update pointer tags
|
||||
runs-on: ubuntu-slim
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Setup NodeJS
|
||||
uses: ./.github/actions/setup-nodejs
|
||||
with:
|
||||
install-command: npm install --prefix=.github/scripts --no-package-lock
|
||||
build-command: ''
|
||||
|
||||
- name: Configure git author
|
||||
run: |
|
||||
git config user.name "n8n-release-tag-merge[bot]"
|
||||
git config user.email "256767729+n8n-release-tag-merge[bot]@users.noreply.github.com"
|
||||
|
||||
- name: Move track tag
|
||||
env:
|
||||
TRACK: ${{ inputs.track }}
|
||||
VERSION_TAG: ${{ inputs.version-tag }}
|
||||
run: node ./.github/scripts/move-track-tag.mjs
|
||||
Loading…
Reference in New Issue
Block a user