mirror of
https://github.com/n8n-io/n8n.git
synced 2026-08-30 18:01:23 +08:00
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:
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user