ci: Allow manual execution of backport workflow (#26576)

This commit is contained in:
Matsu 2026-03-05 10:23:18 +02:00 committed by GitHub
parent b0a4d3db26
commit 7df0125b6d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 145 additions and 9 deletions

View File

@ -1,6 +1,11 @@
// 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> } */
const BACKPORT_BY_TAG_MAP = {
@ -50,8 +55,38 @@ export function labelsToReleaseCandidateBranches(labels) {
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() {
const labels = new Set(readPrLabels());
const labels = await getLabels();
if (!labels || labels.size === 0) {
console.log('No labels on PR. Exiting...');
return;

View File

@ -1,5 +1,6 @@
import { describe, it, mock, before } from 'node:test';
import assert from 'node:assert/strict';
import { readPrLabels } from './github-helpers.mjs';
/**
* Run these tests by running
@ -12,9 +13,14 @@ import assert from 'node:assert/strict';
mock.module('./github-helpers.mjs', {
namedExports: {
ensureEnvVar: () => {}, // no-op
readPrLabels: () => {}, // no-op
readPrLabels: readPrLabels,
resolveRcBranchForTrack: mockResolveRcBranchForTrack,
writeGithubOutput: () => {}, //no-op
getPullRequestById: () => {
return {
labels: ['n8n team', 'Backport to Beta'],
};
},
},
});
@ -28,9 +34,11 @@ function mockResolveRcBranchForTrack(track) {
return undefined;
}
let labelsToReleaseCandidateBranches;
let labelsToReleaseCandidateBranches, getLabels;
before(async () => {
({ labelsToReleaseCandidateBranches } = await import('./compute-backport-targets.mjs'));
({ labelsToReleaseCandidateBranches, getLabels } = await import(
'./compute-backport-targets.mjs'
));
});
describe('Compute backport targets', () => {
@ -59,4 +67,38 @@ describe('Compute backport targets', () => {
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);
});
});

View File

@ -0,0 +1,5 @@
{
"pull_request": {
"labels": ["release", "Backport to Stable"]
}
}

View File

@ -1,5 +1,7 @@
import { getOctokit } from '@actions/github';
import { execFileSync } from 'node:child_process';
import fs from 'node:fs';
import path from 'node:path';
import semver from 'semver';
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
*
@ -124,8 +134,7 @@ export function stripReleasePrefixes(tag) {
*/
export function readPrLabels(pullRequest) {
if (!pullRequest) {
const eventPath = ensureEnvVar('GITHUB_EVENT_PATH');
const event = JSON.parse(fs.readFileSync(eventPath, 'utf8'));
const event = getEventFromGithubEventPath();
pullRequest = event.pull_request;
}
/** @type { string[] | { name: string }[] } */
@ -247,3 +256,38 @@ export function localRefExists(ref) {
const res = trySh('git', ['show-ref', '--verify', '--quiet', ref]);
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;
}

View File

@ -1,6 +1,12 @@
on:
pull_request:
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:
contents: write
@ -8,7 +14,9 @@ permissions:
jobs:
backport:
if: github.event.pull_request.merged == true
if: |
github.event.pull_request.merged == true ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-slim
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@ -23,6 +31,8 @@ jobs:
- name: Compute backport targets
id: targets
env:
PULL_REQUEST_ID: ${{ inputs.pull-request-id }}
run: node .github/scripts/compute-backport-targets.mjs
- name: Backport
@ -30,7 +40,7 @@ jobs:
uses: korthout/backport-action@c656f5d5851037b2b38fb5db2691a03fa229e3b2 # v4.0.1
with:
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 }}
pull_description: |-
# Description