From 0a172919390121962cf64e5aafd31a29b8b78825 Mon Sep 17 00:00:00 2001 From: Matsu Date: Wed, 4 Mar 2026 13:15:09 +0200 Subject: [PATCH] ci: Introduce backport workflow and compute-backport-targets script (#26518) --- .github/scripts/compute-backport-targets.mjs | 74 +++++++++++++++++++ .../scripts/compute-backport-targets.test.mjs | 62 ++++++++++++++++ .github/workflows/backport.yml | 42 +++++++---- .../release-publish-post-release.yml | 25 ++----- ...util-ensure-release-candidate-branches.yml | 27 +++++++ 5 files changed, 195 insertions(+), 35 deletions(-) create mode 100644 .github/scripts/compute-backport-targets.mjs create mode 100644 .github/scripts/compute-backport-targets.test.mjs create mode 100644 .github/workflows/util-ensure-release-candidate-branches.yml diff --git a/.github/scripts/compute-backport-targets.mjs b/.github/scripts/compute-backport-targets.mjs new file mode 100644 index 00000000000..077abe98b89 --- /dev/null +++ b/.github/scripts/compute-backport-targets.mjs @@ -0,0 +1,74 @@ +// Creates backport PR's according to labels on merged PR + +import { readPrLabels, resolveRcBranchForTrack, writeGithubOutput } from './github-helpers.mjs'; + +/** @type { Record } */ +const BACKPORT_BY_TAG_MAP = { + 'Backport to Beta': 'beta', + 'Backport to Stable': 'stable', +}; + +const BACKPORT_BY_BRANCH_MAP = { + 'Backport to v1': '1.x', +}; + +/** + * @param {Set} labels + * + * @returns { Set } + */ +export function labelsToReleaseCandidateBranches(labels) { + const targets = new Set(); + + // Backport by tag map includes mapping of label to git tag to resolve + for (const [label, tag] of Object.entries(BACKPORT_BY_TAG_MAP)) { + // Check if backport label is present + if (!labels.has(label)) { + continue; + } + + const branch = resolveRcBranchForTrack(tag); + // Make sure our backport branch exists + if (!branch) { + continue; + } + + targets.add(branch); + } + + // Backport by branch map includes mapping of label to git branch. This is used for + // older versions of n8n. v1, etc. + for (const [label, branch] of Object.entries(BACKPORT_BY_BRANCH_MAP)) { + // Check if backport label is present + if (!labels.has(label)) { + continue; + } + + targets.add(branch); + } + + return targets; +} + +async function main() { + const labels = new Set(readPrLabels()); + if (!labels || labels.size === 0) { + console.log('No labels on PR. Exiting...'); + return; + } + + const backportBranches = labelsToReleaseCandidateBranches(labels); + + if (backportBranches.size === 0) { + console.log('No backports needed. Exiting...'); + return; + } + + const target_branches = [...backportBranches].join(' '); // korthout/backport-action@v4 uses space-delimited branch list + writeGithubOutput({ target_branches }); +} + +// only run when executed directly, not when imported by tests +if (import.meta.url === `file://${process.argv[1]}`) { + await main(); +} diff --git a/.github/scripts/compute-backport-targets.test.mjs b/.github/scripts/compute-backport-targets.test.mjs new file mode 100644 index 00000000000..5fea8d63f31 --- /dev/null +++ b/.github/scripts/compute-backport-targets.test.mjs @@ -0,0 +1,62 @@ +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/compute-backport-targets.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: { + ensureEnvVar: () => {}, // no-op + readPrLabels: () => {}, // no-op + resolveRcBranchForTrack: mockResolveRcBranchForTrack, + writeGithubOutput: () => {}, //no-op + }, +}); + +function mockResolveRcBranchForTrack(track) { + switch (track) { + case 'beta': + return 'release-candidate/2.10.1'; + case 'stable': + return 'release-candidate/2.9.4'; + } + return undefined; +} + +let labelsToReleaseCandidateBranches; +before(async () => { + ({ labelsToReleaseCandidateBranches } = await import('./compute-backport-targets.mjs')); +}); + +describe('Compute backport targets', () => { + it('Finds backport branches for pointer tag labels', () => { + const labels = new Set(['Backport to Beta', 'Backport to Stable']); + /** @type { Set } */ + const result = labelsToReleaseCandidateBranches(labels); + + assert.equal(result.size, 2); + assert.ok(result.has('release-candidate/2.10.1')); + assert.ok(result.has('release-candidate/2.9.4')); + }); + + it("Doesn't parse other labes to backport branches", () => { + const labels = new Set(['n8n team', 'release']); + /** @type { Set } */ + const result = labelsToReleaseCandidateBranches(labels); + + assert.equal(result.size, 0); + }); + + it("Doesn't parse malformed backport labels", () => { + const labels = new Set(['Backport to Fork', 'Backport to my Home']); + /** @type { Set } */ + const result = labelsToReleaseCandidateBranches(labels); + + assert.equal(result.size, 0); + }); +}); diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index 7a709c972ff..9bf114831ce 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -1,37 +1,49 @@ -name: Backport PRs - on: pull_request: - types: [closed, labeled] + types: [closed] + +permissions: + contents: write + pull-requests: write jobs: backport: - name: Create backport PRs - runs-on: ubuntu-latest - - if: > - ( - github.event_name == 'pull_request' && - github.event.pull_request.merged - ) + if: github.event.pull_request.merged == true + runs-on: ubuntu-slim steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + fetch-depth: 0 - - name: Create backport PRs + - name: Setup Node.js + uses: ./.github/actions/setup-nodejs + with: + build-command: '' + install-command: npm install --prefix=.github/scripts --no-package-lock + + - name: Compute backport targets + id: targets + run: node .github/scripts/compute-backport-targets.mjs + + - name: Backport + if: steps.targets.outputs.target_branches != '' uses: korthout/backport-action@c656f5d5851037b2b38fb5db2691a03fa229e3b2 # v4.0.1 with: + github_token: ${{ secrets.GITHUB_TOKEN }} + source_pr_number: ${{ github.event.pull_request.number }} + target_branches: ${{ steps.targets.outputs.target_branches }} pull_description: |- # Description - Backport of #${pull_number} to `${target_branch}`. + Backport of #${pull_number} to `${target_branch}` # Original description ${pull_description} - pull_title: ${pull_title} (backport ${target_branch}) + pull_title: ${pull_title} (backport to ${target_branch}) add_author_as_assignee: true copy_assignees: true copy_requested_reviewers: true - copy_labels_pattern: '.*' + copy_labels_pattern: '^(?!Backport to\b).+' # Copy everything except backport labels experimental: > { "conflict_resolution": "draft_commit_conflicts" diff --git a/.github/workflows/release-publish-post-release.yml b/.github/workflows/release-publish-post-release.yml index 8e0a494ccec..b14203b8b83 100644 --- a/.github/workflows/release-publish-post-release.yml +++ b/.github/workflows/release-publish-post-release.yml @@ -46,23 +46,8 @@ jobs: release-channel: stable ensure-release-candidate-branches: - name: Ensure release-candidate branches - if: inputs.release_type != 'rc' - runs-on: ubuntu-slim - permissions: - contents: write - 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 branches - run: node ./.github/scripts/ensure-release-candidate-branches.mjs + name: 'Ensure release candidate branches' + if: | + inputs.release_type != 'rc' + uses: ./.github/workflows/util-ensure-release-candidate-branches.yml + secrets: inherit diff --git a/.github/workflows/util-ensure-release-candidate-branches.yml b/.github/workflows/util-ensure-release-candidate-branches.yml new file mode 100644 index 00000000000..b8a05899c28 --- /dev/null +++ b/.github/workflows/util-ensure-release-candidate-branches.yml @@ -0,0 +1,27 @@ +name: 'Util: Ensure release candidate branches' + +on: + workflow_dispatch: + workflow_call: + +jobs: + ensure-release-candidate-branches: + name: Ensure release-candidate branches + runs-on: ubuntu-slim + permissions: + contents: write + 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 branches + run: node ./.github/scripts/ensure-release-candidate-branches.mjs