mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
ci: Allow manual execution of backport workflow (#26576)
This commit is contained in:
parent
b0a4d3db26
commit
7df0125b6d
39
.github/scripts/compute-backport-targets.mjs
vendored
39
.github/scripts/compute-backport-targets.mjs
vendored
|
|
@ -1,6 +1,11 @@
|
||||||
// Creates backport PR's according to labels on merged PR
|
// Creates backport PR's according to labels on merged PR
|
||||||
|
|
||||||
import { readPrLabels, resolveRcBranchForTrack, writeGithubOutput } from './github-helpers.mjs';
|
import {
|
||||||
|
getPullRequestById,
|
||||||
|
readPrLabels,
|
||||||
|
resolveRcBranchForTrack,
|
||||||
|
writeGithubOutput,
|
||||||
|
} from './github-helpers.mjs';
|
||||||
|
|
||||||
/** @type { Record<string, import('./github-helpers.mjs').ReleaseTrack> } */
|
/** @type { Record<string, import('./github-helpers.mjs').ReleaseTrack> } */
|
||||||
const BACKPORT_BY_TAG_MAP = {
|
const BACKPORT_BY_TAG_MAP = {
|
||||||
|
|
@ -50,8 +55,38 @@ export function labelsToReleaseCandidateBranches(labels) {
|
||||||
return targets;
|
return targets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This script is called in 2 cases:
|
||||||
|
*
|
||||||
|
* 1. When a PR is merged, in which case functions like `readPrLabels` reads PR info from GITHUB_EVENT_PATH
|
||||||
|
* 2. Manually via Workflow Dispatch, where a Pull Request ID is passed as an env parameter
|
||||||
|
*
|
||||||
|
* @returns { Promise<undefined | any> } Pull request object, if ID was provided in env params
|
||||||
|
*/
|
||||||
|
async function fetchPossiblePullRequestFromEnv() {
|
||||||
|
const pullRequestEnv = process.env.PULL_REQUEST_ID;
|
||||||
|
if (!pullRequestEnv) {
|
||||||
|
// No ID provided, will proceed to read data from GITHUB_EVENT_PATH
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pullRequestNumber = parseInt(pullRequestEnv);
|
||||||
|
if (isNaN(pullRequestNumber)) {
|
||||||
|
throw new Error(
|
||||||
|
"PULL_REQUEST_ID must be a number. It shouldn't contain any other symbols (#, PR, etc.)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await getPullRequestById(pullRequestNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLabels() {
|
||||||
|
const pullRequest = await fetchPossiblePullRequestFromEnv();
|
||||||
|
return new Set(readPrLabels(pullRequest));
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const labels = new Set(readPrLabels());
|
const labels = await getLabels();
|
||||||
if (!labels || labels.size === 0) {
|
if (!labels || labels.size === 0) {
|
||||||
console.log('No labels on PR. Exiting...');
|
console.log('No labels on PR. Exiting...');
|
||||||
return;
|
return;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { describe, it, mock, before } from 'node:test';
|
import { describe, it, mock, before } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
|
import { readPrLabels } from './github-helpers.mjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Run these tests by running
|
* Run these tests by running
|
||||||
|
|
@ -12,9 +13,14 @@ import assert from 'node:assert/strict';
|
||||||
mock.module('./github-helpers.mjs', {
|
mock.module('./github-helpers.mjs', {
|
||||||
namedExports: {
|
namedExports: {
|
||||||
ensureEnvVar: () => {}, // no-op
|
ensureEnvVar: () => {}, // no-op
|
||||||
readPrLabels: () => {}, // no-op
|
readPrLabels: readPrLabels,
|
||||||
resolveRcBranchForTrack: mockResolveRcBranchForTrack,
|
resolveRcBranchForTrack: mockResolveRcBranchForTrack,
|
||||||
writeGithubOutput: () => {}, //no-op
|
writeGithubOutput: () => {}, //no-op
|
||||||
|
getPullRequestById: () => {
|
||||||
|
return {
|
||||||
|
labels: ['n8n team', 'Backport to Beta'],
|
||||||
|
};
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -28,9 +34,11 @@ function mockResolveRcBranchForTrack(track) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let labelsToReleaseCandidateBranches;
|
let labelsToReleaseCandidateBranches, getLabels;
|
||||||
before(async () => {
|
before(async () => {
|
||||||
({ labelsToReleaseCandidateBranches } = await import('./compute-backport-targets.mjs'));
|
({ labelsToReleaseCandidateBranches, getLabels } = await import(
|
||||||
|
'./compute-backport-targets.mjs'
|
||||||
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Compute backport targets', () => {
|
describe('Compute backport targets', () => {
|
||||||
|
|
@ -59,4 +67,38 @@ describe('Compute backport targets', () => {
|
||||||
|
|
||||||
assert.equal(result.size, 0);
|
assert.equal(result.size, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Should parse labels properly in Pull request context', async () => {
|
||||||
|
process.env.GITHUB_EVENT_PATH = './fixtures/mock-github-event.json';
|
||||||
|
/** @type { Set<string> } */
|
||||||
|
const labels = await getLabels();
|
||||||
|
|
||||||
|
assert.equal(labels.size, 2);
|
||||||
|
assert.ok(labels.has('release'));
|
||||||
|
assert.ok(labels.has('Backport to Stable'));
|
||||||
|
});
|
||||||
|
it('Should parse labels properly in manual workflow context', async () => {
|
||||||
|
process.env.PULL_REQUEST_ID = '123';
|
||||||
|
/** @type { Set<string> } */
|
||||||
|
const labels = await getLabels();
|
||||||
|
|
||||||
|
assert.equal(labels.size, 2);
|
||||||
|
assert.ok(labels.has('n8n team'));
|
||||||
|
assert.ok(labels.has('Backport to Beta'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should throw when passed pull request id with #', async () => {
|
||||||
|
process.env.PULL_REQUEST_ID = '#123';
|
||||||
|
await assert.rejects(getLabels);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should not throw when passed pull request id with just a number', async () => {
|
||||||
|
process.env.PULL_REQUEST_ID = '123';
|
||||||
|
await assert.doesNotReject(getLabels);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should throw when passed pull request id with other than numbers included', async () => {
|
||||||
|
process.env.PULL_REQUEST_ID = 'abc-123';
|
||||||
|
await assert.rejects(getLabels);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
5
.github/scripts/fixtures/mock-github-event.json
vendored
Normal file
5
.github/scripts/fixtures/mock-github-event.json
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"pull_request": {
|
||||||
|
"labels": ["release", "Backport to Stable"]
|
||||||
|
}
|
||||||
|
}
|
||||||
48
.github/scripts/github-helpers.mjs
vendored
48
.github/scripts/github-helpers.mjs
vendored
|
|
@ -1,5 +1,7 @@
|
||||||
|
import { getOctokit } from '@actions/github';
|
||||||
import { execFileSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
|
|
||||||
export const RELEASE_TRACKS = /** @type { const } */ ([
|
export const RELEASE_TRACKS = /** @type { const } */ ([
|
||||||
|
|
@ -117,6 +119,14 @@ export function stripReleasePrefixes(tag) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getEventFromGithubEventPath() {
|
||||||
|
let eventPath = ensureEnvVar('GITHUB_EVENT_PATH');
|
||||||
|
if (!path.isAbsolute(eventPath)) {
|
||||||
|
eventPath = import.meta.dirname + '/' + eventPath;
|
||||||
|
}
|
||||||
|
return JSON.parse(fs.readFileSync(eventPath, 'utf8'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {any} [pullRequest] Optional pull request object. If not provided, reads from GITHUB_EVENT_PATH
|
* @param {any} [pullRequest] Optional pull request object. If not provided, reads from GITHUB_EVENT_PATH
|
||||||
*
|
*
|
||||||
|
|
@ -124,8 +134,7 @@ export function stripReleasePrefixes(tag) {
|
||||||
*/
|
*/
|
||||||
export function readPrLabels(pullRequest) {
|
export function readPrLabels(pullRequest) {
|
||||||
if (!pullRequest) {
|
if (!pullRequest) {
|
||||||
const eventPath = ensureEnvVar('GITHUB_EVENT_PATH');
|
const event = getEventFromGithubEventPath();
|
||||||
const event = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
|
|
||||||
pullRequest = event.pull_request;
|
pullRequest = event.pull_request;
|
||||||
}
|
}
|
||||||
/** @type { string[] | { name: string }[] } */
|
/** @type { string[] | { name: string }[] } */
|
||||||
|
|
@ -247,3 +256,38 @@ export function localRefExists(ref) {
|
||||||
const res = trySh('git', ['show-ref', '--verify', '--quiet', ref]);
|
const res = trySh('git', ['show-ref', '--verify', '--quiet', ref]);
|
||||||
return res.ok;
|
return res.ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes octokit with GITHUB_TOKEN from env vars.
|
||||||
|
*
|
||||||
|
* Also ensures the existence of useful environment variables.
|
||||||
|
* */
|
||||||
|
export function initGithub() {
|
||||||
|
const token = ensureEnvVar('GITHUB_TOKEN');
|
||||||
|
const repoFullName = ensureEnvVar('GITHUB_REPOSITORY');
|
||||||
|
|
||||||
|
const [owner, repo] = repoFullName.split('/');
|
||||||
|
|
||||||
|
const octokit = getOctokit(token);
|
||||||
|
|
||||||
|
return {
|
||||||
|
octokit,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} pullRequestId
|
||||||
|
*/
|
||||||
|
export async function getPullRequestById(pullRequestId) {
|
||||||
|
const { octokit, owner, repo } = initGithub();
|
||||||
|
|
||||||
|
const pullRequest = await octokit.rest.pulls.get({
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
pull_number: pullRequestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return pullRequest.data;
|
||||||
|
}
|
||||||
|
|
|
||||||
14
.github/workflows/backport.yml
vendored
14
.github/workflows/backport.yml
vendored
|
|
@ -1,6 +1,12 @@
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [closed]
|
types: [closed]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
pull-request-id:
|
||||||
|
description: 'The ID number of the pull request (e.g. 3342). No #, no extra letters.'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
@ -8,7 +14,9 @@ permissions:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
backport:
|
backport:
|
||||||
if: github.event.pull_request.merged == true
|
if: |
|
||||||
|
github.event.pull_request.merged == true ||
|
||||||
|
github.event_name == 'workflow_dispatch'
|
||||||
runs-on: ubuntu-slim
|
runs-on: ubuntu-slim
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
|
@ -23,6 +31,8 @@ jobs:
|
||||||
|
|
||||||
- name: Compute backport targets
|
- name: Compute backport targets
|
||||||
id: targets
|
id: targets
|
||||||
|
env:
|
||||||
|
PULL_REQUEST_ID: ${{ inputs.pull-request-id }}
|
||||||
run: node .github/scripts/compute-backport-targets.mjs
|
run: node .github/scripts/compute-backport-targets.mjs
|
||||||
|
|
||||||
- name: Backport
|
- name: Backport
|
||||||
|
|
@ -30,7 +40,7 @@ jobs:
|
||||||
uses: korthout/backport-action@c656f5d5851037b2b38fb5db2691a03fa229e3b2 # v4.0.1
|
uses: korthout/backport-action@c656f5d5851037b2b38fb5db2691a03fa229e3b2 # v4.0.1
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
source_pr_number: ${{ github.event.pull_request.number }}
|
source_pr_number: ${{ github.event.pull_request.number || inputs.pull-request-id }}
|
||||||
target_branches: ${{ steps.targets.outputs.target_branches }}
|
target_branches: ${{ steps.targets.outputs.target_branches }}
|
||||||
pull_description: |-
|
pull_description: |-
|
||||||
# Description
|
# Description
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user