ci: Pin standalone package latest dist-tag to newest beta (no-changelog) (#34770)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Charlie Kolb
2026-07-23 12:57:54 +00:00
committed by GitHub
parent d8d89cc381
commit 151ca180f9
3 changed files with 102 additions and 6 deletions
@@ -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);
}
}
@@ -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);
});
});
@@ -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