From 927b7bc818066af3fe16c87a5f5f4b49ea6d212d Mon Sep 17 00:00:00 2001 From: Matsu Date: Wed, 18 Mar 2026 11:19:31 +0200 Subject: [PATCH] ci: Create github releases from mjs scripts (#27121) --- .github/scripts/create-github-release.mjs | 81 +++++++++++++++++++ .github/scripts/determine-version-info.mjs | 4 +- .github/scripts/github-helpers.mjs | 47 +++++++++++ .github/scripts/jsconfig.json | 2 +- .github/scripts/package.json | 3 +- .github/scripts/pnpm-lock.yaml | 5 +- .github/scripts/promote-github-release.mjs | 45 +++++++++++ .../release-create-github-releases.yml | 78 ++++++++++++++++++ .../release-promote-github-release.yml | 50 ++++++++++++ .../release-publish-post-release.yml | 10 +++ .github/workflows/release-publish.yml | 33 ++------ 11 files changed, 329 insertions(+), 29 deletions(-) create mode 100644 .github/scripts/create-github-release.mjs create mode 100644 .github/scripts/promote-github-release.mjs create mode 100644 .github/workflows/release-create-github-releases.yml create mode 100644 .github/workflows/release-promote-github-release.yml diff --git a/.github/scripts/create-github-release.mjs b/.github/scripts/create-github-release.mjs new file mode 100644 index 00000000000..93312e512a9 --- /dev/null +++ b/.github/scripts/create-github-release.mjs @@ -0,0 +1,81 @@ +import { + deleteRelease, + ensureEnvVar, + getExistingRelease, + initGithub, + isReleaseTrack, + writeGithubOutput, +} from './github-helpers.mjs'; + +/** + * Creates release in GitHub. + * + * Required env variables: + * - RELEASE_TAG - Release tag on git e.g. n8n@2.13.0 + * - BODY - Body of the release. Contains release notes etc. + * - IS_PRE_RELEASE - If releasing in pre-release. Currently only for beta track. + * - MAKE_LATEST - If released version should be marked as latest on GitHub + * - COMMIT - Commitish for release to point to + * + * Optional env variables: + * - ADDITIONAL_TAGS - Comma-separated list of additional tags to release under e.g. beta + * + * GitHub variables + * - GITHUB_TOKEN - Used to authenticate to octokit - Can be overwritten for privileged access + * - GITHUB_REPOSITORY - Used to determine target repository + * */ +async function createGitHubRelease() { + const RELEASE_TAG = ensureEnvVar('RELEASE_TAG'); + const ADDITIONAL_TAGS = process.env.ADDITIONAL_TAGS ?? ''; + const BODY = ensureEnvVar('BODY'); + const IS_PRE_RELEASE = ensureEnvVar('IS_PRE_RELEASE'); + const MAKE_LATEST = ensureEnvVar('MAKE_LATEST'); + const COMMIT = ensureEnvVar('COMMIT'); + + const { octokit, owner, repo } = initGithub(); + + const allTags = [ + RELEASE_TAG, + ...ADDITIONAL_TAGS.split(',') + .map((t) => t.trim()) + .filter(Boolean), + ]; + const releases = []; + + for (const tag of allTags) { + const existingRelease = await getExistingRelease(tag); + const isReleaseTrackTag = isReleaseTrack(tag); + + // If we have an existing track release, we want to + // delete the old release before pushing a new one. + if (isReleaseTrackTag && existingRelease) { + await deleteRelease(existingRelease.id); + } + + const releaseResponse = await octokit.rest.repos.createRelease({ + tag_name: tag, + name: tag, + body: BODY, + draft: false, + prerelease: IS_PRE_RELEASE === 'true', + make_latest: MAKE_LATEST === 'true' ? 'true' : 'false', + target_commitish: COMMIT, + owner, + repo, + }); + + const release = releaseResponse.data; + releases.push(release); + + console.log(`Successfully created release ${release.html_url}`); + } + + writeGithubOutput({ + release_urls: releases.map((release) => release.html_url).join(', '), + }); +} + +// only run when executed directly, not when imported by tests +if (import.meta.url === `file://${process.argv[1]}`) { + createGitHubRelease(); +} diff --git a/.github/scripts/determine-version-info.mjs b/.github/scripts/determine-version-info.mjs index db4904697fb..d13588e2f23 100644 --- a/.github/scripts/determine-version-info.mjs +++ b/.github/scripts/determine-version-info.mjs @@ -74,7 +74,9 @@ export function determineTrack(packageVersion) { writeGithubOutput(output); console.log( - `Determined track info: track=${track}, version=${packageVersion}, previous_version=${previousVersion}, new_stable_version=${newStable}, release_type=${releaseType}, rc_branch=${rc_branch}`, + `Determined track info: ${Object.entries(output) + .map(([key, val]) => `${key}=${val}`) + .join(', ')}`, ); return output; diff --git a/.github/scripts/github-helpers.mjs b/.github/scripts/github-helpers.mjs index 905f6c28069..81f328b30e2 100644 --- a/.github/scripts/github-helpers.mjs +++ b/.github/scripts/github-helpers.mjs @@ -14,6 +14,10 @@ export const RELEASE_TRACKS = /** @type { const } */ ([ 'v1', ]); +/** + * @typedef { InstanceType } GitHubInstance + * */ + /** * @typedef {typeof RELEASE_TRACKS[number]} ReleaseTrack * */ @@ -50,6 +54,15 @@ export function pickHighestReleaseTag(tags) { return /** @type { ReleaseVersion } */ (versions[0]?.tag) ?? null; } +/** + * @param {any} track + * + * @returns { track is ReleaseTrack } + * */ +export function isReleaseTrack(track) { + return RELEASE_TRACKS.includes(track); +} + /** * @param {any} track * @@ -337,3 +350,37 @@ export async function getPullRequestById(pullRequestId) { return pullRequest.data; } + +/** + * @param {string} tag + */ +export async function getExistingRelease(tag) { + const { octokit, owner, repo } = initGithub(); + + try { + const releaseRequest = await octokit.rest.repos.getReleaseByTag({ + owner, + repo, + tag, + }); + + return releaseRequest.data; + } catch (ex) { + if (ex?.status === 404) { + return undefined; + } + throw ex; + } +} + +/** + * @param {number} releaseId + */ +export async function deleteRelease(releaseId) { + const { octokit, owner, repo } = initGithub(); + await octokit.rest.repos.deleteRelease({ + owner, + repo, + release_id: releaseId, + }); +} diff --git a/.github/scripts/jsconfig.json b/.github/scripts/jsconfig.json index 8af00d2f5cd..ae12830a9d6 100644 --- a/.github/scripts/jsconfig.json +++ b/.github/scripts/jsconfig.json @@ -3,7 +3,7 @@ "module": "esnext", "target": "esnext", "checkJs": true, - "moduleResolution": "node" + "moduleResolution": "bundler" }, "exclude": ["node_modules"] } diff --git a/.github/scripts/package.json b/.github/scripts/package.json index 8e7b99e99de..e1bfd49911f 100644 --- a/.github/scripts/package.json +++ b/.github/scripts/package.json @@ -5,6 +5,7 @@ }, "dependencies": { "@actions/github": "9.0.0", + "@octokit/core": "7.0.6", "conventional-changelog": "7.2.0", "debug": "4.4.3", "glob": "13.0.6", @@ -12,6 +13,6 @@ "tempfile": "6.0.1" }, "devDependencies": { - "conventional-changelog-angular": "^8.3.0" + "conventional-changelog-angular": "8.3.0" } } diff --git a/.github/scripts/pnpm-lock.yaml b/.github/scripts/pnpm-lock.yaml index 90b0e623533..b547faa90a8 100644 --- a/.github/scripts/pnpm-lock.yaml +++ b/.github/scripts/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@actions/github': specifier: 9.0.0 version: 9.0.0 + '@octokit/core': + specifier: 7.0.6 + version: 7.0.6 conventional-changelog: specifier: 7.2.0 version: 7.2.0(conventional-commits-filter@5.0.0) @@ -28,7 +31,7 @@ importers: version: 6.0.1 devDependencies: conventional-changelog-angular: - specifier: ^8.3.0 + specifier: 8.3.0 version: 8.3.0 packages: diff --git a/.github/scripts/promote-github-release.mjs b/.github/scripts/promote-github-release.mjs new file mode 100644 index 00000000000..faa56b16616 --- /dev/null +++ b/.github/scripts/promote-github-release.mjs @@ -0,0 +1,45 @@ +import { + ensureEnvVar, + getExistingRelease, + initGithub, + writeGithubOutput, +} from './github-helpers.mjs'; + +/** + * Promotes a GitHub release to latest + * + * Required env variables: + * - RELEASE_TAG - Release tag on git e.g. n8n@2.13.0 + * + * GitHub variables + * - GITHUB_TOKEN - Used to authenticate to octokit - Can be overwritten for privileged access + * - GITHUB_REPOSITORY - Used to determine target repository + * */ +async function promoteGitHubRelease() { + const RELEASE_TAG = ensureEnvVar('RELEASE_TAG'); + const { octokit, owner, repo } = initGithub(); + + const existingRelease = await getExistingRelease(RELEASE_TAG); + if (!existingRelease) { + console.warn("Couldn't find release by tag. Exiting..."); + process.exit(1); + } + + const releaseResponse = await octokit.rest.repos.updateRelease({ + owner, + repo, + release_id: existingRelease.id, + make_latest: 'true', + }); + + console.log(`Successfully updated release ${releaseResponse.data.html_url}`); + + writeGithubOutput({ + release_url: releaseResponse.data.html_url, + }); +} + +// only run when executed directly, not when imported by tests +if (import.meta.url === `file://${process.argv[1]}`) { + promoteGitHubRelease(); +} diff --git a/.github/workflows/release-create-github-releases.yml b/.github/workflows/release-create-github-releases.yml new file mode 100644 index 00000000000..e2f3383e607 --- /dev/null +++ b/.github/workflows/release-create-github-releases.yml @@ -0,0 +1,78 @@ +name: 'Release: Create GitHub Releases' +run-name: 'Creating GitHub Releases for ${{ inputs.version-tag }} (${{ inputs.track }})' + +on: + workflow_call: + inputs: + track: + required: true + type: string + version-tag: + required: true + type: string + body: + required: true + type: string + commit: + 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).' + required: true + type: string + body: + description: 'Release notes body.' + required: true + type: string + commit: + description: 'Commitish the release points to (e.g. branch name or SHA).' + required: true + type: string + +permissions: + contents: write + id-token: write + +jobs: + create-github-releases: + name: Create GitHub releases + runs-on: ubuntu-slim + environment: release + + 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: + token: ${{ steps.generate_token.outputs.token }} + fetch-depth: 1 + + - name: Setup NodeJS + uses: ./.github/actions/setup-nodejs + with: + build-command: '' + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + + - name: Create GitHub releases + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + RELEASE_TAG: ${{ inputs.version-tag }} + BODY: ${{ inputs.body }} + IS_PRE_RELEASE: ${{ inputs.track == 'beta' }} + MAKE_LATEST: ${{ inputs.track == 'stable' }} + COMMIT: ${{ inputs.commit }} + ADDITIONAL_TAGS: ${{ inputs.track }} + run: node ./.github/scripts/create-github-release.mjs diff --git a/.github/workflows/release-promote-github-release.yml b/.github/workflows/release-promote-github-release.yml new file mode 100644 index 00000000000..1282a31b996 --- /dev/null +++ b/.github/workflows/release-promote-github-release.yml @@ -0,0 +1,50 @@ +name: 'Release: Promote GitHub Releases' +run-name: 'Promoting GitHub Release ${{ inputs.version-tag }} to latest' + +on: + workflow_call: + inputs: + version-tag: + required: true + type: string + workflow_dispatch: + inputs: + version-tag: + description: 'Version tag (e.g. n8n@2.7.0).' + required: true + type: string + +permissions: + contents: write + +jobs: + promote-github-releases: + name: Promote GitHub release to latest + runs-on: ubuntu-slim + environment: release + + 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: + token: ${{ steps.generate_token.outputs.token }} + fetch-depth: 1 + + - name: Setup NodeJS + uses: ./.github/actions/setup-nodejs + with: + build-command: '' + install-command: pnpm install --frozen-lockfile --dir ./.github/scripts --ignore-workspace + + - name: Promote GitHub releases + env: + GITHUB_TOKEN: ${{ steps.generate_token.outputs.token }} + RELEASE_TAG: ${{ inputs.version-tag }} + run: node ./.github/scripts/promote-github-release.mjs diff --git a/.github/workflows/release-publish-post-release.yml b/.github/workflows/release-publish-post-release.yml index ee38e419a2a..7d313b9176b 100644 --- a/.github/workflows/release-publish-post-release.yml +++ b/.github/workflows/release-publish-post-release.yml @@ -49,6 +49,16 @@ jobs: version: ${{ inputs.new_stable_version }} release-channel: stable + promote-previous-minor-github-release-to-latest: + name: Promote previous minor Github Release to latest + if: | + inputs.release_type != 'rc' && + inputs.bump == 'minor' + uses: ./.github/workflows/release-promote-github-release.yml + secrets: inherit + with: + version-tag: 'n8n@${{ inputs.new_stable_version }}' + ensure-release-candidate-branches: name: 'Ensure release candidate branches' if: | diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 43a1ccee52f..8aa586a988d 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -97,33 +97,16 @@ jobs: secrets: inherit create-github-release: - name: Create a GitHub Release + name: Create GitHub Release needs: [determine-version-info, publish-to-npm, publish-to-docker-hub] - runs-on: ubuntu-latest if: github.event.pull_request.merged == true - timeout-minutes: 5 - - permissions: - contents: write - id-token: write - - 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: Create a Release on GitHub - uses: ncipollo/release-action@b7eabc95ff50cbeeedec83973935c8f306dfcd0b # v1.20.0 - with: - token: ${{ steps.generate_token.outputs.token }} - commit: ${{github.event.pull_request.base.ref}} - tag: 'n8n@${{ needs.determine-version-info.outputs.version }}' - prerelease: ${{ needs.determine-version-info.outputs.track == 'beta' }} - makeLatest: ${{ needs.determine-version-info.outputs.track == 'stable' }} - body: ${{github.event.pull_request.body}} + uses: ./.github/workflows/release-create-github-releases.yml + with: + track: ${{ needs.determine-version-info.outputs.track }} + version-tag: 'n8n@${{ needs.determine-version-info.outputs.version }}' + body: ${{ github.event.pull_request.body }} + commit: ${{ github.event.pull_request.base.ref }} + secrets: inherit move-track-tag: name: Move track tag