From 28a32b8b85eb7c5e67c3693b6a45389ecd012bfc Mon Sep 17 00:00:00 2001 From: Claudio Wunder Date: Mon, 19 Oct 2026 14:32:31 +0200 Subject: [PATCH] feat: support the Alpha release phase Starting with 27.x, every Node.js release line gets Alpha releases, versioned as `vN.0.0-alpha.X.Y.Z`, before its first Current release, and schedule.json has an `alpha` date for it. Releases now keep their pre-release identifiers. Coercing them turned every Alpha release into `vN.0.0`, so they would overwrite each other, and `vN.0.0` itself once it ships. Every release has an `alpha.isAlpha` flag, like `lts.isLts`, and `support.phases` has the `alpha` date, with `alpha` as the current phase until the release line becomes Current. Release lines on the previous schedule get the same data as before, besides `alpha.isAlpha` being `false` on their releases. This holds for all 872 releases in today's index.json. Refs: https://nodejs.org/en/blog/announcements/evolving-the-nodejs-release-schedule Refs: https://github.com/nodejs/nodejs.org/pull/9223 Assisted-by: Claude Opus 5.5 --- core/README.md | 15 ++++-- core/index.js | 22 ++++++++- core/test/alpha.js | 112 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 7 deletions(-) create mode 100644 core/test/alpha.js diff --git a/core/README.md b/core/README.md index ca7cd87f..dfb20eda 100644 --- a/core/README.md +++ b/core/README.md @@ -77,6 +77,8 @@ The data returned by this module follows a pretty specific structure. Here's a d - `architecture` (`String`) - The name of the architecture for the files in `files`. - `lts` (`Object`): - `isLts`(Boolean) - `true` if the release is an LTS release, `false` if it's not. + - `alpha` (`Object`): + - `isAlpha` (Boolean) - `true` if the release is an Alpha release (like `v27.0.0-alpha.0.0.0`), `false` if it's not. - `security` (`Object`): - `isSecurity` (Boolean) - `true` if the release is a security release, `false` if it's not. - `support` (`Object`) - An`Object`to provide information about the project's support of the given release line, including dates, status, LTS codename, and LTS version info: @@ -84,11 +86,14 @@ The data returned by this module follows a pretty specific structure. Here's a d - `lts` (`Object`) - **Only present if applicable to the given release.** An`Object`that provides LTS versioning information. - `newest` (`String`) - The newest release in the release line that is LTS. _Usually_, this will be the latest release in the line. - `oldest` (`String`) - **Only present if applicable to the given release.** The oldest (first) release in the release line that was flagged as LTS. - - `phases` (`Object`) - Start dates for each phase of a release line's lifecycle: - - `start` (`String`) - The start date of the release line (releases in this phase are `current`). Example: `2021-04-20`. - - `lts` (`String`) - **Only present if the release line is an LTS release.** The start date of the LTS lifecycle (releases in this phase are `lts`). Example: `2021-10-26`. - - `maintenance` (`String`) - **Only present if the release line is an LTS release.** The start date of the maintenance phase of the lifecycle. Example: `2022-10-18`. - - `end` (`String`) - the date the release line is no longer supported. Example: `2024-04-30`. + - `phases` (`Object`) - Information about each phase of a release line's lifecycle: + - `dates` (`Object`) - Start dates for each phase of a release line's lifecycle: + - `alpha` (`String`) - **Only present if the release line has an Alpha phase.** The start date of the Alpha phase, before the first Current release of the release line (releases in this phase are `alpha`). Example: `2026-10-28`. + - `start` (`String`) - The start date of the release line's Current phase (releases in this phase are `current`). Example: `2021-04-20`. + - `lts` (`String`) - **Only present if the release line is an LTS release.** The start date of the LTS lifecycle (releases in this phase are `lts`). Example: `2021-10-26`. + - `maintenance` (`String`) - **Only present if the release line has a maintenance phase.** The start date of the maintenance phase of the lifecycle. Example: `2022-10-18`. + - `end` (`String`) - the date the release line is no longer supported. Example: `2024-04-30`. + - `current` (`String`) - The phase the release line is in, based on `now`: `alpha`, `start`, `lts`, `maintenance`, or `end`. An empty `Object` if no phase has started yet. - `security` (`Object`): - `all` (`Array`) - an `Array` of _all_ security releases in the release line. - `newest` (`String`) - the newest (most recent) security release. Example: `17.3.1`. diff --git a/core/index.js b/core/index.js index 88f625f0..9cdc22fd 100644 --- a/core/index.js +++ b/core/index.js @@ -19,7 +19,11 @@ async function core(options) { const data = {}; Object.keys(versions).map(async (version) => { - const versionSemver = semver.coerce(versions[version].version); + // keep the pre-release identifiers, otherwise every Alpha release (like + // `v27.0.0-alpha.0.0.0`) would be coerced into `v27.0.0` + const versionSemver = semver.coerce(versions[version].version, { + includePrerelease: true, + }); const name = versionSemver.major !== 0 ? `v${versionSemver.major}` @@ -119,6 +123,8 @@ async function core(options) { data[name].support.phases = {}; data[name].support.phases.dates = {}; + data[name].support.phases.dates.alpha = + schedule[name]?.alpha ?? undefined; data[name].support.phases.dates.start = schedule[name]?.start ?? undefined; data[name].support.phases.dates.lts = schedule[name]?.lts ?? undefined; @@ -148,6 +154,16 @@ async function core(options) { data[name].releases[`v${versionSemver.version}`].lts.isLts = !!versions[version].lts; + // # Alpha + // ## define the alpha object in each specific version + // + // Alpha releases are the pre-releases a release line gets before its + // first Current release, versioned as `vN.0.0-alpha.X.Y.Z` + data[name].releases[`v${versionSemver.version}`].alpha = {}; + + data[name].releases[`v${versionSemver.version}`].alpha.isAlpha = + versionSemver.prerelease[0] === 'alpha'; + // # Security // ## define the release-line specific security object if (!data[name].security) { @@ -191,6 +207,8 @@ async function determineCurrentReleasePhase(now, dates = {}) { // `true` is in the past // `false` is in the future const isoified = { + alpha: + isInPast(DateTime.fromISO(dates.alpha).diff(now).toMillis()) ?? undefined, start: isInPast(DateTime.fromISO(dates.start).diff(now).toMillis()) ?? undefined, lts: @@ -208,7 +226,7 @@ async function determineCurrentReleasePhase(now, dates = {}) { // iterate over the past/future object and set the above variable to whatever the first date is in the future. for (const phase of Object.keys(isoified)) { // since we're looping, the last true is the current phase - // since the start date will always be in the past + // since the start (or alpha) date will always be in the past if (isoified[phase] === true) { result = phase; } diff --git a/core/test/alpha.js b/core/test/alpha.js new file mode 100644 index 00000000..9d51f0fe --- /dev/null +++ b/core/test/alpha.js @@ -0,0 +1,112 @@ +const { deepStrictEqual } = require('node:assert'); +const nodevu = require('../index'); +const { describe, it } = require('node:test'); + +// these tests use made-up data that follows the release schedule starting with +// 27.x, where every release line gets Alpha releases (versioned as +// `vN.0.0-alpha.X.Y.Z`) before its first Current release. +// +// see https://nodejs.org/en/blog/announcements/evolving-the-nodejs-release-schedule +function release(version, date) { + return { version, date, files: ['linux-x64', 'src'] }; +} + +const index = [ + release('v27.0.0', '2027-04-22'), + release('v27.0.0-alpha.1.0.0', '2026-12-08'), + release('v27.0.0-alpha.0.0.0', '2026-10-28'), + release('v26.12.0', '2026-10-14'), +]; + +// 26.x follows the previous release schedule, while 27.x gets an Alpha phase +// and goes straight from Current to LTS, without a maintenance phase +const schedule = { + v26: { + start: '2026-05-05', + lts: '2026-10-28', + maintenance: '2027-10-20', + end: '2029-04-30', + codename: '', + }, + v27: { + alpha: '2026-10-28', + start: '2027-04-22', + lts: '2027-10-20', + end: '2030-04-30', + codename: '', + }, +}; + +async function mockFetch(url) { + return Response.json(url.endsWith('schedule.json') ? schedule : index); +} + +describe('check to make sure that Alpha releases work as expected', async () => { + it('should have every Alpha release, with its full version', async () => { + const data = await nodevu({ fetch: mockFetch }); + + deepStrictEqual(Object.keys(data.v27.releases), [ + 'v27.0.0', + 'v27.0.0-alpha.1.0.0', + 'v27.0.0-alpha.0.0.0', + ]); + deepStrictEqual(data.v27.releases['v27.0.0'].releaseDate, '2027-04-22'); + deepStrictEqual(data.v27.releases['v27.0.0-alpha.1.0.0'].semver, { + raw: '27.0.0-alpha.1.0.0', + major: 27, + minor: 0, + patch: 0, + line: 'v27', + }); + }); + + it('should have correct values for alpha in every release', async () => { + const data = await nodevu({ fetch: mockFetch }); + + deepStrictEqual(data.v27.releases['v27.0.0'].alpha.isAlpha, false); + deepStrictEqual( + data.v27.releases['v27.0.0-alpha.1.0.0'].alpha.isAlpha, + true, + ); + deepStrictEqual( + data.v27.releases['v27.0.0-alpha.0.0.0'].alpha.isAlpha, + true, + ); + deepStrictEqual(data.v26.releases['v26.12.0'].alpha.isAlpha, false); + }); + + it('should have correct files for an Alpha release', async () => { + const data = await nodevu({ fetch: mockFetch }); + const { linux, source } = + data.v27.releases['v27.0.0-alpha.1.0.0'].files.links; + + deepStrictEqual(linux[0].files, [ + 'node-v27.0.0-alpha.1.0.0-linux-x64.tar.gz', + 'node-v27.0.0-alpha.1.0.0-linux-x64.tar.xz', + ]); + deepStrictEqual(source[0].files, ['node-v27.0.0-alpha.1.0.0.tar.xz']); + }); + + it('should have correct support values for a release line with an Alpha phase', async () => { + const data = await nodevu({ fetch: mockFetch }); + + deepStrictEqual(data.v27.support.phases.dates.alpha, '2026-10-28'); + deepStrictEqual(data.v26.support.phases.dates.alpha, undefined); + }); + + it('should have correct dynamic values for support in a release line with an Alpha phase', async () => { + const phases = [ + ['2026-12-15', 'alpha', 'lts'], + ['2027-05-01', 'start', 'lts'], + ['2027-11-01', 'lts', 'maintenance'], + ['2030-05-01', 'end', 'end'], + ]; + + for (const [now, v27, v26] of phases) { + const data = await nodevu({ fetch: mockFetch, now }); + + deepStrictEqual(data.v27.support.phases.current, v27, now); + deepStrictEqual(data.v26.support.phases.current, v26, now); + } + }); +});