diff --git a/.github/workflows/official-pr.yml b/.github/workflows/official-pr.yml index a1429eb090..2c6aeaa125 100644 --- a/.github/workflows/official-pr.yml +++ b/.github/workflows/official-pr.yml @@ -10,7 +10,7 @@ on: - '**/Dockerfile' - '**/docker-entrypoint.sh' - 'versions.json' - - 'stackbrew.js' + - 'stackbrew.mjs' jobs: pr: @@ -36,7 +36,7 @@ jobs: - name: Generate Stackbrew for diff run: | cd docker-node - ./stackbrew.js > ../official-images/library/node + ./stackbrew.mjs ../official-images/library/node - name: Create PR in official-images id: create-pr diff --git a/genMatrix.js b/genMatrix.js index fc10f8d994..a1249d9e61 100644 --- a/genMatrix.js +++ b/genMatrix.js @@ -52,7 +52,7 @@ const getAffectedDockerfiles = (filesAdded, filesModified, filesRenamed) => { }; const getFullNodeVersionFromDockerfile = (file) => - fs.readFileSync(file, 'utf8').match(/^ENV NODE_VERSION=(\d*\.*\d*\.\d*)/m)[1]; + fs.readFileSync(file, 'utf8').match(/^ENV NODE_VERSION=(\S*)/m)[1]; const getDockerfileMatrixEntry = (file) => { const [variant] = path.dirname(file).split(path.sep).slice(-1); diff --git a/stackbrew.js b/stackbrew.js deleted file mode 100755 index 5a00e332ee..0000000000 --- a/stackbrew.js +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env node - -const fs = require('fs'); -const path = require('path'); - -// Grab last git commit -function getCommitHasForPath(path) { - return require('child_process') - .execSync(`git log -1 --format=%H HEAD -- ${path}`) - .toString() - .trim(); -} - -const stackbrewPath = path.basename(__filename); - -// Header -let stackbrew = `# this file is generated via https://github.com/nodejs/docker-node/blob/${getCommitHasForPath(stackbrewPath)}/${stackbrewPath} - -Maintainers: The Node.js Docker Team (@nodejs) -GitRepo: https://github.com/nodejs/docker-node.git -GitFetch: refs/heads/main\n`; - -// Loop versions - -const config = require('./versions.json'); - -const versions = Object.keys(config).reverse(); - -let midnight = new Date(); -midnight.setHours(0, 0, 0, 0); -const now = midnight.getTime(); -const alpineRE = new RegExp(/alpine*/); -const slimRE = new RegExp(/\*-slim/); -let foundLTS = false; -let foundCurrent = false; - -for (const version of versions) { - let lts = new Date(`${config[version].lts}T00:00:00.00`).getTime(); - let isCurrent = foundCurrent ? false : isNaN(lts) || lts >= now; - foundCurrent = isCurrent || foundCurrent; - let isLTS = foundLTS ? false : now >= lts; - foundLTS = isLTS || foundLTS; - let codename = config[version].codename; - let defaultAlpine = config[version]['alpine-default']; - let defaultDebian = config[version]['debian-default']; - let variants = config[version].variants; - for (const variant in variants) { - let dockerfilePath = path.join(version, variant, 'Dockerfile'); - let isAlpine = alpineRE.test(variant); - let isSlim = slimRE.test(variant); - let isDefaultSlim = new RegExp(`${defaultDebian}-slim`).test(variant); - - // Get full version from the Dockerfile - let dockerfile = fs.readFileSync(dockerfilePath, 'utf-8'); - let fullVersion = dockerfile.match( - /ENV NODE_VERSION=(?\d+)\.(?\d+)\.(?\d+)/, - ); - - let tags = [ - `${fullVersion.groups.major}.${fullVersion.groups.minor}.${fullVersion.groups.patch}-${variant}`, - `${fullVersion.groups.major}.${fullVersion.groups.minor}-${variant}`, - `${fullVersion.groups.major}-${variant}`, - ]; - - if (codename) { - tags.push(`${codename}-${variant}`); - } - - if (variant === defaultAlpine) { - tags.push( - `${fullVersion.groups.major}.${fullVersion.groups.minor}.${fullVersion.groups.patch}-alpine`, - ); - tags.push( - `${fullVersion.groups.major}.${fullVersion.groups.minor}-alpine`, - ); - tags.push(`${fullVersion.groups.major}-alpine`); - if (codename) { - tags.push(`${codename}-alpine`); - } - } - - if (variant === defaultDebian) { - tags.push( - `${fullVersion.groups.major}.${fullVersion.groups.minor}.${fullVersion.groups.patch}`, - ); - tags.push(`${fullVersion.groups.major}.${fullVersion.groups.minor}`); - tags.push(`${fullVersion.groups.major}`); - if (isSlim) { - tags.push( - `${fullVersion.groups.major}.${fullVersion.groups.minor}.${fullVersion.groups.patch}-slim`, - ); - tags.push( - `${fullVersion.groups.major}.${fullVersion.groups.minor}-slim`, - ); - tags.push(`${fullVersion.groups.major}-slim`); - } - if (codename) { - tags.push(`${codename}`); - } - } - if (isDefaultSlim) { - tags.push( - `${fullVersion.groups.major}.${fullVersion.groups.minor}.${fullVersion.groups.patch}-slim`, - ); - tags.push(`${fullVersion.groups.major}.${fullVersion.groups.minor}-slim`); - tags.push(`${fullVersion.groups.major}-slim`); - if (codename) { - tags.push(`${codename}-slim`); - } - } - - if (isCurrent) { - if (variant === defaultAlpine) { - tags.push(variant); - tags.push( - `${fullVersion.groups.major}.${fullVersion.groups.minor}.${fullVersion.groups.patch}-alpine`, - ); - tags.push( - `${fullVersion.groups.major}.${fullVersion.groups.minor}-alpine`, - ); - tags.push(`${fullVersion.groups.major}-alpine`); - tags.push('alpine'); - tags.push('current-alpine'); - } - if (variant === defaultDebian) { - tags.push(variant); - tags.push('latest'); - tags.push('current'); - } - if (isAlpine) { - tags.push(`${variant}`); - tags.push(`current-${variant}`); - } - if (!isAlpine) { - tags.push(`${variant}`); - tags.push(`current-${variant}`); - } - if (isDefaultSlim) { - tags.push('slim'); - tags.push('current-slim'); - } - } - - if (isLTS) { - tags.push(`lts-${variant}`); - if (variant === defaultDebian) { - tags.push('lts'); - if (codename) { - tags.push(`lts-${codename}`); - } - } - if (isDefaultSlim) { - tags.push(`lts-slim`); - } - if (variant === defaultAlpine) { - tags.push(`lts-alpine`); - } - } - - // remove duplicates - tags = tags.filter((x, i, a) => a.indexOf(x) == i); - tags = tags.sort(); - let directory = `${version}/${variant}`; - stackbrew += `\nTags: ${tags.join(', ')}\n`; - stackbrew += `Architectures: ${config[version].variants[variant].join(', ')}\n`; - stackbrew += `GitCommit: ${getCommitHasForPath(directory)}\n`; - stackbrew += `Directory: ${directory}\n`; - } -} - -// output -console.log(stackbrew); diff --git a/stackbrew.mjs b/stackbrew.mjs new file mode 100644 index 0000000000..62e4c78a64 --- /dev/null +++ b/stackbrew.mjs @@ -0,0 +1,184 @@ +#!/usr/bin/env node + +import { readFileSync, writeFileSync } from 'node:fs'; +import { basename, resolve } from 'node:path'; +import { execSync } from 'node:child_process'; + +// Grab last git commit +function getCommitHasForPath(path) { + return execSync(`git log -1 --format=%H HEAD -- ${path}`).toString().trim(); +} + +// Grab nodejs/Release values as the authoritative start/maintenance/end dates +const releaseTimes = await fetch( + 'https://raw.githubusercontent.com/nodejs/Release/refs/heads/main/schedule.json', +); +const releaseJson = await releaseTimes.json(); + +// Grab the nodejs.org index.json as the authoritative Codename values, since that can lag the Release repo JSON +const availableVersions = await fetch( + 'https://nodejs.org/download/release/index.json', +); +const officialIndexJson = await availableVersions.json(); + +const stackbrewPath = basename(import.meta.filename); + +// Header +let stackbrew = `# this file is generated via https://github.com/nodejs/docker-node/blob/${getCommitHasForPath(stackbrewPath)}/${stackbrewPath} + +Maintainers: The Node.js Docker Team (@nodejs) +GitRepo: https://github.com/nodejs/docker-node.git +GitFetch: refs/heads/main\n`; + +const config = JSON.parse(readFileSync('versions.json')); + +const localVersions = Object.keys(config).reverse(); + +let midnight = new Date(); +midnight.setHours(0, 0, 0, 0); +const now = midnight.getTime(); +let foundLTS = false; +let foundCurrent = false; + +for (const majorVersion of localVersions) { + let releaseRepoInfo = releaseJson[`v${majorVersion}`]; + let nodejsOrgReleaseInfo = officialIndexJson.find((indexVersion) => + indexVersion.version.startsWith(`v${majorVersion}`), + ); + let currentStart = new Date(`${releaseRepoInfo.start}T00:00:00.00`).getTime(); + // First Alpha build is the only active one, and when the "start" of the release hasn't cut over + let isAlpha = nodejsOrgReleaseInfo.version.includes('alpha'); + // The Current/Latest release is the first non-Alpha major that has hit it's "start" date + let isCurrent = foundCurrent + ? false + : !isAlpha && (isNaN(currentStart) || now >= currentStart); + foundCurrent |= isCurrent; + let codename = nodejsOrgReleaseInfo.lts + ? nodejsOrgReleaseInfo.lts.toLowerCase() + : ''; + // The LTS tag is only set on the most recent LTS release + let isLTS = foundLTS ? false : codename !== ''; + foundLTS |= isLTS; + let defaultAlpine = config[majorVersion]['alpine-default']; + let defaultDebian = config[majorVersion]['debian-default']; + let defaultSlim = `${config[majorVersion]['debian-default']}-slim`; + let variants = config[majorVersion].variants; + for (const variant in variants) { + let tags; + + if (isAlpha) { + // Only push explicit variants during the Alpha phase + tags = [`${nodejsOrgReleaseInfo.version.substring(1)}-${variant}`]; + } else { + let fullVersion = nodejsOrgReleaseInfo.version.match( + /v(?\d+)\.(?\d+)\.(?\d+)/, + ); + tags = [ + `${fullVersion.groups.major}.${fullVersion.groups.minor}.${fullVersion.groups.patch}-${variant}`, + `${fullVersion.groups.major}.${fullVersion.groups.minor}-${variant}`, + `${fullVersion.groups.major}-${variant}`, + ]; + + if (codename) { + tags.push(`${codename}-${variant}`); + } + + if (variant === defaultAlpine) { + tags.push( + `${fullVersion.groups.major}.${fullVersion.groups.minor}.${fullVersion.groups.patch}-alpine`, + ); + tags.push( + `${fullVersion.groups.major}.${fullVersion.groups.minor}-alpine`, + ); + tags.push(`${fullVersion.groups.major}-alpine`); + + if (codename) { + tags.push(`${codename}-alpine`); + } + + if (isCurrent) { + tags.push('alpine'); + tags.push('current-alpine'); + } + + if (isLTS) { + tags.push(`lts-alpine`); + } + } else if (variant === defaultDebian) { + tags.push( + `${fullVersion.groups.major}.${fullVersion.groups.minor}.${fullVersion.groups.patch}`, + ); + tags.push(`${fullVersion.groups.major}.${fullVersion.groups.minor}`); + tags.push(`${fullVersion.groups.major}`); + + if (codename) { + tags.push(`${codename}`); + } + + if (isCurrent) { + tags.push(variant); + tags.push('latest'); + tags.push('current'); + } + + if (isLTS) { + tags.push('lts'); + if (codename) { + tags.push(`lts-${codename}`); + } + } + } else if (variant === defaultSlim) { + tags.push( + `${fullVersion.groups.major}.${fullVersion.groups.minor}.${fullVersion.groups.patch}-slim`, + ); + tags.push( + `${fullVersion.groups.major}.${fullVersion.groups.minor}-slim`, + ); + tags.push(`${fullVersion.groups.major}-slim`); + + if (codename) { + tags.push(`${codename}-slim`); + } + + if (isCurrent) { + tags.push('slim'); + tags.push('current-slim'); + } + + if (isLTS) { + tags.push(`lts-slim`); + } + } + + if (isCurrent) { + tags.push(`${variant}`); + tags.push(`current-${variant}`); + } + + if (isLTS) { + tags.push(`lts-${variant}`); + } + } + + stackbrew += ` +Tags: ${tags + .filter((x, i, a) => a.indexOf(x) == i) + .sort() + .join(', ')} +Architectures: ${config[majorVersion].variants[variant].join(', ')} +GitCommit: ${getCommitHasForPath(`${majorVersion}/${variant}`)} +Directory: ${`${majorVersion}/${variant}`}\n`; + } +} + +try { + const outputFile = process.argv[2]; + + if (outputFile) { + writeFileSync(resolve(outputFile), stackbrew); + } else { + console.log(stackbrew); + } +} catch (ex) { + console.error("Couldn't handle output", ex); +} diff --git a/versions.json b/versions.json index 479c550111..66f09361ed 100644 --- a/versions.json +++ b/versions.json @@ -1,10 +1,5 @@ { "26": { - "start": "2026-05-05", - "lts": "2026-10-28", - "maintenance": "2027-10-20", - "end": "2029-04-30", - "codename": "lithium", "alpine-default": "alpine3.24", "debian-default": "trixie", "variants": { @@ -41,11 +36,6 @@ } }, "24": { - "start": "2025-05-06", - "lts": "2025-10-28", - "maintenance": "2026-10-20", - "end": "2028-04-30", - "codename": "krypton", "alpine-default": "alpine3.24", "debian-default": "bookworm", "variants": { @@ -84,11 +74,6 @@ } }, "22": { - "start": "2024-04-23", - "lts": "2024-10-29", - "maintenance": "2025-10-21", - "end": "2027-04-30", - "codename": "jod", "alpine-default": "alpine3.24", "debian-default": "bookworm", "variants": {