From 151ca180f98d58e281e7d061012b491d198e5f05 Mon Sep 17 00:00:00 2001 From: Charlie Kolb Date: Thu, 23 Jul 2026 12:57:54 +0000 Subject: [PATCH] ci: Pin standalone package latest dist-tag to newest beta (no-changelog) (#34770) Co-authored-by: Claude Fable 5 --- .../set-latest-for-monorepo-packages.mjs | 50 +++++++++++++++-- .../set-latest-for-monorepo-packages.test.mjs | 53 +++++++++++++++++++ .../release-publish-post-release.yml | 5 +- 3 files changed, 102 insertions(+), 6 deletions(-) create mode 100644 .github/scripts/set-latest-for-monorepo-packages.test.mjs diff --git a/.github/scripts/set-latest-for-monorepo-packages.mjs b/.github/scripts/set-latest-for-monorepo-packages.mjs index 979a671488a..1b090a15d83 100644 --- a/.github/scripts/set-latest-for-monorepo-packages.mjs +++ b/.github/scripts/set-latest-for-monorepo-packages.mjs @@ -1,7 +1,18 @@ +import semver from 'semver'; + import { getMonorepoProjects } from './pnpm-utils.mjs'; const NPM_REGISTRY = 'https://registry.npmjs.org'; +// Standalone packages release from master out-of-sync with the main pipeline +// (release-standalone-package.yml), so their `latest` follows the newest beta +// on npm instead of the version recorded in the stable checkout. +const STANDALONE_PACKAGES_FOLLOWING_BETA = new Set([ + '@n8n/create-node', + '@n8n/eslint-plugin-community-nodes', + '@n8n/scan-community-package', +]); + /** * @param {string} name * @param {string} version @@ -23,6 +34,30 @@ async function setDistTag(name, version, tag, token) { }); } +/** @param {string} name */ +async function getDistTags(name) { + const res = await fetch(`${NPM_REGISTRY}/-/package/${encodeURIComponent(name)}/dist-tags`); + if (!res.ok) { + throw new Error(`Failed to fetch dist-tags for ${name}: HTTP ${res.status}`); + } + return await res.json(); +} + +/** + * Resolve the version `latest` should point at, or null if no move is needed. + * Standalone packages follow the npm `beta` dist-tag; a standalone release + * from master may already have pushed `latest` past beta, so never downgrade. + * @param {import('./pnpm-utils.mjs').PnpmPackage} pkg + */ +export async function resolveLatestVersion(pkg) { + if (!STANDALONE_PACKAGES_FOLLOWING_BETA.has(pkg.name)) return pkg.version; + + const tags = await getDistTags(pkg.name); + const target = tags.beta ?? pkg.version; + if (tags.latest && semver.gte(tags.latest, target)) return null; + return target; +} + async function setLatestForMonorepoPackages() { const token = process.env.NPM_TOKEN; if (!token) { @@ -39,10 +74,15 @@ async function setLatestForMonorepoPackages() { const failures = []; for (const pkg of publishedPackages) { - const versionName = `${pkg.name}@${pkg.version}`; - try { - const res = await setDistTag(pkg.name, pkg.version, 'latest', token); + const version = await resolveLatestVersion(pkg); + if (version === null) { + console.log(`Skipped ${pkg.name}: latest is already at or ahead of beta`); + continue; + } + + const versionName = `${pkg.name}@${version}`; + const res = await setDistTag(pkg.name, version, 'latest', token); if (res.ok) { console.log(`Set ${versionName} as latest`); @@ -53,8 +93,8 @@ async function setLatestForMonorepoPackages() { } } catch (err) { const message = err instanceof Error ? err.message : String(err); - console.error(`Failed to set ${versionName} as latest: ${message}`); - failures.push(versionName); + console.error(`Failed to set latest for ${pkg.name}: ${message}`); + failures.push(pkg.name); } } diff --git a/.github/scripts/set-latest-for-monorepo-packages.test.mjs b/.github/scripts/set-latest-for-monorepo-packages.test.mjs new file mode 100644 index 00000000000..f00553c604b --- /dev/null +++ b/.github/scripts/set-latest-for-monorepo-packages.test.mjs @@ -0,0 +1,53 @@ +import { describe, it, before, afterEach } from 'node:test'; +import assert from 'node:assert/strict'; + +/** + * Run these tests by running + * + * node --test --experimental-test-module-mocks ./.github/scripts/set-latest-for-monorepo-packages.test.mjs + * */ + +let resolveLatestVersion; +before(async () => { + ({ resolveLatestVersion } = await import('./set-latest-for-monorepo-packages.mjs')); +}); + +const originalFetch = globalThis.fetch; +afterEach(() => { + globalThis.fetch = originalFetch; +}); + +function mockDistTags(tags) { + globalThis.fetch = async () => ({ ok: true, json: async () => tags }); +} + +describe('resolveLatestVersion', () => { + it('uses the checkout version for regular monorepo packages', async () => { + globalThis.fetch = async () => { + throw new Error('should not fetch dist-tags for regular packages'); + }; + const version = await resolveLatestVersion({ name: '@n8n/config', version: '1.2.3' }); + assert.equal(version, '1.2.3'); + }); + + it('pins standalone packages to the beta dist-tag', async () => { + mockDistTags({ beta: '0.40.1', latest: '0.39.3' }); + const version = await resolveLatestVersion({ name: '@n8n/create-node', version: '0.39.3' }); + assert.equal(version, '0.40.1'); + }); + + it('never downgrades latest below a newer standalone release', async () => { + mockDistTags({ beta: '0.40.1', latest: '0.41.0' }); + const version = await resolveLatestVersion({ name: '@n8n/create-node', version: '0.39.3' }); + assert.equal(version, null); + }); + + it('skips standalone packages without a beta tag when latest is current', async () => { + mockDistTags({ latest: '0.25.0' }); + const version = await resolveLatestVersion({ + name: '@n8n/eslint-plugin-community-nodes', + version: '0.25.0', + }); + assert.equal(version, null); + }); +}); diff --git a/.github/workflows/release-publish-post-release.yml b/.github/workflows/release-publish-post-release.yml index 03176cd143a..f54616dfed2 100644 --- a/.github/workflows/release-publish-post-release.yml +++ b/.github/workflows/release-publish-post-release.yml @@ -80,9 +80,12 @@ jobs: ensure-correct-latest-version-on-npm: name: Ensure correct latest version on npm + # Also runs on beta patches: standalone packages pin `latest` to the newest + # beta, so every beta publish needs a re-pin, not just stable promotions. if: | inputs.bump == 'minor' || - (inputs.track == 'stable' && inputs.release_type != 'rc') + (inputs.track == 'stable' && inputs.release_type != 'rc') || + (inputs.track == 'beta' && inputs.release_type != 'rc') uses: ./.github/workflows/release-set-stable-npm-packages-to-latest.yml secrets: inherit