From d1911f49e5fc54b5c2cdb518b268d394f9feb7da Mon Sep 17 00:00:00 2001 From: Sythos Date: Mon, 24 Aug 2026 22:19:45 +0200 Subject: [PATCH 1/8] Automate multi-arch Ubuntu container releases --- .github/workflows/ci.yml | 142 ++++++++++++++++- .github/workflows/release.yml | 261 +++++++++++++++++++++++++++++--- Dockerfile | 62 +++++++- README.md | 25 ++- package.json | 1 + scripts/check-release.mjs | 4 +- scripts/check-tag-increment.mjs | 125 +++++++++++++++ scripts/prepare-release.mjs | 4 +- 8 files changed, 586 insertions(+), 38 deletions(-) create mode 100644 scripts/check-tag-increment.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index db5b7fa..b6e2f2b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,13 +5,18 @@ on: branches: - main - 'fix/**' + - 'feat/**' tags-ignore: - 'v*' pull_request: workflow_dispatch: +permissions: + contents: read + jobs: lint-and-check: + name: Lint and check runs-on: ubuntu-latest steps: @@ -21,11 +26,142 @@ jobs: - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24.19.0 cache: npm - - name: Install dependencies - run: npm ci + - name: Install dependencies without lifecycle scripts + run: npm ci --ignore-scripts - name: Lint and validate run: npm run check + + npm-audit: + name: Audit production dependencies + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 24.19.0 + cache: npm + + - name: Install dependencies without lifecycle scripts + run: npm ci --ignore-scripts + + - name: Check production dependency vulnerabilities + run: npm audit --omit=dev --audit-level=high + + node-compatibility: + name: Node.js ${{ matrix.node-version }} compatibility + runs-on: ubuntu-latest + continue-on-error: ${{ matrix.node-version == '18.19.1' }} + strategy: + fail-fast: false + matrix: + node-version: + - '18.19.1' + - '24.19.0' + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + + - name: Install dependencies without lifecycle scripts + run: npm ci --ignore-scripts + + - name: Check runtime compatibility + run: npm run check + + codeql: + name: CodeQL + runs-on: ubuntu-latest + permissions: + contents: read + actions: read + security-events: write + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: javascript-typescript + + - name: Analyze with CodeQL + uses: github/codeql-action/analyze@v3 + with: + category: /language:javascript-typescript + + dependency-review: + name: Dependency review + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + + steps: + - name: Review dependency changes + uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high + + container-build: + name: Build and smoke-test Ubuntu ${{ matrix.ubuntu }} ${{ matrix.platform }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ubuntu: + - '24.04' + - '26.04' + platform: + - linux/amd64 + - linux/arm64 + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build container + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + platforms: ${{ matrix.platform }} + push: false + load: true + build-args: | + UBUNTU_VERSION=${{ matrix.ubuntu }} + tags: bzo:ci-${{ matrix.ubuntu }}-${{ strategy.job-index }} + provenance: false + sbom: false + + - name: Smoke-test container + shell: bash + run: | + set -euo pipefail + image="bzo:ci-${{ matrix.ubuntu }}-${{ strategy.job-index }}" + case "${{ matrix.ubuntu }}" in + 24.04) expected_node="v18.19.1" ;; + 26.04) expected_node="v24.19.0" ;; + *) echo "Unsupported Ubuntu matrix value" >&2; exit 1 ;; + esac + actual_node="$(docker run --rm --platform "${{ matrix.platform }}" "$image" node --version)" + test "$actual_node" = "$expected_node" + docker run --rm --platform "${{ matrix.platform }}" "$image" node --check server.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 13296fe..153229c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,13 +5,20 @@ on: tags: - 'v*' +concurrency: + group: release-${{ github.repository }} + cancel-in-progress: false + permissions: - contents: write - packages: write + contents: read jobs: - release: + gate: + name: Validate release runs-on: ubuntu-latest + outputs: + image: ${{ steps.image.outputs.name }} + version: ${{ steps.version.outputs.value }} steps: - name: Check out repository @@ -19,28 +26,50 @@ jobs: with: fetch-depth: 0 + - name: Verify tag commit is on main + shell: bash + run: | + set -euo pipefail + git fetch origin main --no-tags --quiet + if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then + echo "Release tags must point to a commit reachable from main." >&2 + exit 1 + fi + - name: Set up Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24.19.0 cache: npm - - name: Install dependencies - run: npm ci + - name: Install dependencies without lifecycle scripts + run: npm ci --ignore-scripts - name: Lint and validate run: npm run check + - name: Check production dependency vulnerabilities + run: npm audit --omit=dev --audit-level=high + - name: Validate release metadata run: npm run release:check -- "${GITHUB_REF_NAME}" + - name: Validate tag increment + run: npm run release:check:increment -- "${GITHUB_REF_NAME}" + - name: Extract release notes run: npm run changelog:extract -- "${GITHUB_REF_NAME}" > release-notes.md - name: Compute release version id: version + shell: bash run: echo "value=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT" + - name: Compute image name + id: image + shell: bash + run: echo "name=ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" + - name: Create release archive run: | git archive \ @@ -49,9 +78,172 @@ jobs: -o "bzo-${{ steps.version.outputs.value }}.tar.gz" \ "$GITHUB_SHA" - - name: Compute image name - id: image - run: echo "name=ghcr.io/$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" + - name: Upload release assets + uses: actions/upload-artifact@v4 + with: + name: release-assets + path: | + release-notes.md + bzo-${{ steps.version.outputs.value }}.tar.gz + retention-days: 7 + + codeql: + name: CodeQL release scan + needs: gate + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: javascript-typescript + + - name: Analyze with CodeQL + uses: github/codeql-action/analyze@v3 + with: + category: /language:javascript-typescript + + build: + name: Build Ubuntu ${{ matrix.ubuntu }} images + needs: + - gate + - codeql + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ubuntu: + - '24.04' + - '26.04' + permissions: + contents: read + packages: write + actions: read + attestations: write + id-token: write + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push staging image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + build-args: | + UBUNTU_VERSION=${{ matrix.ubuntu }} + tags: | + ${{ needs.gate.outputs.image }}:ci-${{ needs.gate.outputs.version }}-ubuntu${{ matrix.ubuntu }}-${{ github.run_id }} + labels: | + org.opencontainers.image.title=BZFlag Battlezone Online + org.opencontainers.image.version=${{ needs.gate.outputs.version }} + org.opencontainers.image.revision=${{ github.sha }} + cache-from: type=gha,scope=release-ubuntu-${{ matrix.ubuntu }} + provenance: mode=max + sbom: true + + verify: + name: Verify Ubuntu ${{ matrix.ubuntu }} manifest and smoke test + needs: + - gate + - build + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ubuntu: + - '24.04' + - '26.04' + permissions: + contents: read + packages: read + + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Verify published manifest + shell: bash + env: + IMAGE: ${{ needs.gate.outputs.image }} + STAGING_TAG: ci-${{ needs.gate.outputs.version }}-ubuntu${{ matrix.ubuntu }}-${{ github.run_id }} + run: | + set -euo pipefail + manifest="$(docker buildx imagetools inspect "${IMAGE}:${STAGING_TAG}")" + printf '%s\n' "$manifest" + grep -q 'linux/amd64' <<<"$manifest" + grep -q 'linux/arm64' <<<"$manifest" + + - name: Smoke test both architectures + shell: bash + env: + IMAGE: ${{ needs.gate.outputs.image }} + STAGING_TAG: ci-${{ needs.gate.outputs.version }}-ubuntu${{ matrix.ubuntu }}-${{ github.run_id }} + run: | + set -euo pipefail + case "${{ matrix.ubuntu }}" in + 24.04) expected_node="v18.19.1" ;; + 26.04) expected_node="v24.19.0" ;; + *) echo "Unsupported Ubuntu matrix value" >&2; exit 1 ;; + esac + for platform in linux/amd64 linux/arm64; do + actual_node="$(docker run --rm --pull=always --platform "$platform" "${IMAGE}:${STAGING_TAG}" node --version)" + test "$actual_node" = "$expected_node" + docker run --rm --pull=always --platform "$platform" "${IMAGE}:${STAGING_TAG}" node --check server.js + done + + promote: + name: Promote verified images + needs: + - gate + - codeql + - verify + if: ${{ needs.verify.result == 'success' }} + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Log in to GHCR uses: docker/login-action@v3 @@ -60,21 +252,54 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - - name: Build Docker image + - name: Promote verified tags + shell: bash + env: + IMAGE: ${{ needs.gate.outputs.image }} + VERSION: ${{ needs.gate.outputs.version }} + RUN_ID: ${{ github.run_id }} run: | - docker build --no-cache --progress=plain \ - -t ${{ steps.image.outputs.name }}:${{ steps.version.outputs.value }} \ - -t ${{ steps.image.outputs.name }}:latest \ - . + set -euo pipefail + for ubuntu in 24.04 26.04; do + source="${IMAGE}:ci-${VERSION}-ubuntu${ubuntu}-${RUN_ID}" + docker buildx imagetools create --tag "${IMAGE}:${VERSION}-ubuntu${ubuntu}" "$source" + docker buildx imagetools create --tag "${IMAGE}:ubuntu${ubuntu}" "$source" + done + + docker buildx imagetools create --tag "${IMAGE}:${VERSION}" "${IMAGE}:${VERSION}-ubuntu26.04" + docker buildx imagetools create --tag "${IMAGE}:latest" "${IMAGE}:ubuntu26.04" - - name: Push Docker image + - name: Verify promoted default tag + shell: bash + env: + IMAGE: ${{ needs.gate.outputs.image }} run: | - docker push ${{ steps.image.outputs.name }}:${{ steps.version.outputs.value }} - docker push ${{ steps.image.outputs.name }}:latest + set -euo pipefail + manifest="$(docker buildx imagetools inspect "${IMAGE}:latest")" + printf '%s\n' "$manifest" + grep -q 'linux/amd64' <<<"$manifest" + grep -q 'linux/arm64' <<<"$manifest" + + release: + name: Create GitHub release + needs: + - gate + - promote + if: ${{ needs.promote.result == 'success' }} + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Download release assets + uses: actions/download-artifact@v4 + with: + name: release-assets + path: . - name: Create GitHub release uses: softprops/action-gh-release@v2 with: body_path: release-notes.md files: | - bzo-${{ steps.version.outputs.value }}.tar.gz + bzo-${{ needs.gate.outputs.version }}.tar.gz diff --git a/Dockerfile b/Dockerfile index 2b48e53..9260fbb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,23 +3,75 @@ # Source: https://github.com/BZFlag-Dev/bzo # See LICENSE or https://www.gnu.org/licenses/agpl-3.0.html -FROM ubuntu:24.04 +# Pin the Node.js runtime for each Ubuntu base and verify each architecture independently. +# Ubuntu 24.04 intentionally keeps Node.js 18.19.1 for compatibility; prefer +# the Ubuntu 26.04 image for the supported Node.js 24.19.0 runtime. +ARG UBUNTU_VERSION=24.04 +ARG NODE_VERSION_UBUNTU24=18.19.1 +ARG NODE_VERSION_UBUNTU26=24.19.0 +ARG NODE_SHA256_UBUNTU24_AMD64=724802c45237477dbe5777923743e6c77906830cae03a82b5653ebd75b301dda +ARG NODE_SHA256_UBUNTU24_ARM64=2913e8544d95c8be9e6034c539ec0584014532166a088bf742629756c3ec42e2 +ARG NODE_SHA256_UBUNTU26_AMD64=f625d97cd707df4ff96254916fbc5ff014f09c09effe5a1e0ca8f6d41a8789d4 +ARG NODE_SHA256_UBUNTU26_ARM64=d28c8a5bf0a808f0ed434a1dce8c54ae98f0371c0bd86ac58abc613f73e6643f + +FROM ubuntu:${UBUNTU_VERSION} + +ARG UBUNTU_VERSION +ARG NODE_VERSION_UBUNTU24 +ARG NODE_VERSION_UBUNTU26 +ARG NODE_SHA256_UBUNTU24_AMD64 +ARG NODE_SHA256_UBUNTU24_ARM64 +ARG NODE_SHA256_UBUNTU26_AMD64 +ARG NODE_SHA256_UBUNTU26_ARM64 +ARG TARGETARCH +RUN case "${UBUNTU_VERSION}" in \ + 24.04|26.04) ;; \ + *) echo "Unsupported Ubuntu version: ${UBUNTU_VERSION}" >&2; exit 1 ;; \ + esac ENV NODE_ENV=production \ PORT=3000 \ SERVER_CONFIG_PATH=/data/server-config.json \ DEBIAN_FRONTEND=noninteractive -RUN apt-get update \ - && apt-get install -y --no-install-recommends nodejs npm \ - && rm -rf /var/lib/apt/lists/* +RUN set -eu; \ + apt-get update; \ + apt-get install -y --no-install-recommends ca-certificates curl libstdc++6; \ + case "${UBUNTU_VERSION}" in \ + 24.04) node_version="${NODE_VERSION_UBUNTU24}" ;; \ + 26.04) node_version="${NODE_VERSION_UBUNTU26}" ;; \ + *) echo "Unsupported Ubuntu version: ${UBUNTU_VERSION}" >&2; exit 1 ;; \ + esac; \ + case "${TARGETARCH}" in \ + amd64) node_arch=x64 ;; \ + arm64) node_arch=arm64 ;; \ + *) echo "Unsupported target architecture: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + case "${UBUNTU_VERSION}:${TARGETARCH}" in \ + 24.04:amd64) node_checksum="${NODE_SHA256_UBUNTU24_AMD64}" ;; \ + 24.04:arm64) node_checksum="${NODE_SHA256_UBUNTU24_ARM64}" ;; \ + 26.04:amd64) node_checksum="${NODE_SHA256_UBUNTU26_AMD64}" ;; \ + 26.04:arm64) node_checksum="${NODE_SHA256_UBUNTU26_ARM64}" ;; \ + *) echo "Unsupported Ubuntu and architecture combination" >&2; exit 1 ;; \ + esac; \ + node_archive="node-v${node_version}-linux-${node_arch}.tar.gz"; \ + curl --fail --silent --show-error --location --retry 3 \ + --output "/tmp/${node_archive}" \ + "https://nodejs.org/dist/v${node_version}/${node_archive}"; \ + echo "${node_checksum} /tmp/${node_archive}" | sha256sum --check --strict; \ + tar --extract --gzip --file "/tmp/${node_archive}" \ + --strip-components=1 --directory /usr/local; \ + test "$(node --version)" = "v${node_version}"; \ + rm -f "/tmp/${node_archive}"; \ + apt-get purge -y --auto-remove curl; \ + rm -rf /var/lib/apt/lists/* RUN useradd --system --create-home --shell /bin/bash node WORKDIR /app COPY package.json package-lock.json ./ -RUN npm ci --omit=dev && npm cache clean --force +RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force COPY public ./public COPY maps ./maps diff --git a/README.md b/README.md index 0e17bc7..fe1922a 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,16 @@ Each tagged release publishes: - a GitHub release with notes generated from [CHANGELOG.md](CHANGELOG.md) - a source tarball -- a versioned Docker image at `ghcr.io/bzflag-dev/bzo:` -- a moving Docker tag at `ghcr.io/bzflag-dev/bzo:latest` +- a versioned Ubuntu 24.04 image at `ghcr.io/bzflag-dev/bzo:-ubuntu24.04` +- a versioned Ubuntu 26.04 image at `ghcr.io/bzflag-dev/bzo:-ubuntu26.04` +- moving `ubuntu24.04` and `ubuntu26.04` tags for the two base images +- `ghcr.io/bzflag-dev/bzo:` and `ghcr.io/bzflag-dev/bzo:latest`, both using Ubuntu 26.04 + +Every published image contains `linux/amd64` and `linux/arm64` variants. Release +tags use stable `vX.Y.Z` SemVer only; prerelease and build-metadata tags are not +published. The Ubuntu 24.04 image keeps the pinned Ubuntu-compatible Node.js +`18.19.1` runtime, while Ubuntu 26.04 uses Node.js `24.19.0`; Node.js 18 is EOL, +so new deployments should prefer the Ubuntu 26.04 image. ## Install with Docker @@ -54,7 +62,7 @@ The image defaults to `SERVER_CONFIG_PATH=/data/server-config.json`. ### Prerequisites -- Node.js 20+ +- Node.js 24 LTS or a newer supported LTS - npm ### Setup @@ -190,6 +198,7 @@ Validate locally: ```bash npm run check npm run release:check -- v1.0.1 +npm run release:check:increment -- v1.0.1 ``` Then commit, tag, and push: @@ -204,13 +213,13 @@ git push origin v1.0.1 The release workflow will: -1. install dependencies -2. run lint and validation +1. verify that the stable tag is newer than the previous release and points to `main` +2. install dependencies and run lint, validation, audit, and CodeQL checks 3. fail if `package.json` does not match the pushed tag 4. fail if [CHANGELOG.md](CHANGELOG.md) does not contain a matching non-placeholder section -5. publish a GitHub release -6. attach a source tarball -7. build and publish a multi-arch Docker image to GHCR +5. build and smoke-test both Ubuntu LTS images, including their pinned Node.js runtimes, for `linux/amd64` and `linux/arm64` +6. promote the verified versioned and moving Docker tags to GHCR +7. publish a GitHub release and attach a source tarball ## AGPL source availability diff --git a/package.json b/package.json index 3a1bf4f..cb1b3ac 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "check": "npm run check:server && npm run lint", "release:prepare": "node scripts/prepare-release.mjs", "release:check": "node scripts/check-release.mjs", + "release:check:increment": "node scripts/check-tag-increment.mjs", "changelog:extract": "node scripts/extract-changelog.mjs", "prepare": "node -e \"const { existsSync } = require('node:fs'); process.exit(existsSync('node_modules/.bin/husky') ? 0 : 1)\" && husky || true" }, diff --git a/scripts/check-release.mjs b/scripts/check-release.mjs index 4099978..a93525b 100644 --- a/scripts/check-release.mjs +++ b/scripts/check-release.mjs @@ -55,8 +55,8 @@ if (!tagInput) { } const tagVersion = normalizeVersion(tagInput); -if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(tagVersion)) { - fail(`tag "${tagInput}" is not a valid release version`); +if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(tagVersion)) { + fail(`tag "${tagInput}" must be a stable SemVer release version in the form X.Y.Z (prerelease and build metadata are not published)`); } const packageJson = readJson(packageJsonPath); diff --git a/scripts/check-tag-increment.mjs b/scripts/check-tag-increment.mjs new file mode 100644 index 0000000..74ec32a --- /dev/null +++ b/scripts/check-tag-increment.mjs @@ -0,0 +1,125 @@ +#!/usr/bin/env node +/* + * Copyright (C) 2025-2026 Tim Riker + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3). + * Source: https://github.com/BZFlag-Dev/bzo + * See LICENSE or https://www.gnu.org/licenses/agpl-3.0.html + */ + +import { execFileSync } from 'child_process'; +import { resolve, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const rootDir = resolve(__dirname, '..'); +const semverPattern = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/; + +function fail(message) { + console.error(`Tag increment check failed: ${message}`); + process.exit(1); +} + +function normalizeTag(input) { + if (!input || typeof input !== 'string') return ''; + return input.trim().replace(/^refs\/tags\//, ''); +} + +function parseVersion(input) { + const tag = normalizeTag(input); + const version = tag.replace(/^v/, ''); + const match = semverPattern.exec(version); + if (!match) return null; + + return { + tag, + version, + major: Number(match[1]), + minor: Number(match[2]), + patch: Number(match[3]), + prerelease: [], + }; +} + +function compareIdentifiers(left, right) { + const leftIsNumeric = /^\d+$/.test(left); + const rightIsNumeric = /^\d+$/.test(right); + + if (leftIsNumeric && rightIsNumeric) return Number(left) - Number(right); + if (leftIsNumeric) return -1; + if (rightIsNumeric) return 1; + return left < right ? -1 : left > right ? 1 : 0; +} + +function compareVersions(left, right) { + for (const field of ['major', 'minor', 'patch']) { + if (left[field] !== right[field]) return left[field] - right[field]; + } + + if (!left.prerelease.length && !right.prerelease.length) return 0; + if (!left.prerelease.length) return 1; + if (!right.prerelease.length) return -1; + + const length = Math.max(left.prerelease.length, right.prerelease.length); + for (let index = 0; index < length; index += 1) { + if (index >= left.prerelease.length) return -1; + if (index >= right.prerelease.length) return 1; + + const comparison = compareIdentifiers(left.prerelease[index], right.prerelease[index]); + if (comparison !== 0) return comparison; + } + + return 0; +} + +function readReleaseTags() { + try { + return execFileSync('git', ['tag', '--list', 'v*'], { + cwd: rootDir, + encoding: 'utf8', + }) + .split(/\r?\n/) + .map((tag) => tag.trim()) + .filter(Boolean); + } catch (error) { + fail(`unable to read release tags from Git: ${error.message}`); + } +} + +const tagInput = process.argv[2] || process.env.GITHUB_REF_NAME; +if (!tagInput) { + fail('missing tag argument'); +} + +const candidate = parseVersion(tagInput); +if (!candidate) { + fail(`tag "${tagInput}" must be a stable SemVer release tag in the form vX.Y.Z (prerelease and build metadata are not published)`); +} + +const releaseTags = readReleaseTags(); +const parsedTags = []; +for (const tag of releaseTags) { + const parsed = parseVersion(tag); + if (parsed) { + parsedTags.push(parsed); + } else { + console.warn(`Ignoring non-SemVer release tag "${tag}"`); + } +} + +if (parsedTags.some((tag) => tag.version === candidate.version)) { + fail(`tag "${candidate.tag}" already exists in the release history`); +} + +const previous = parsedTags + .sort((left, right) => compareVersions(right, left))[0]; + +if (!previous) { + console.log(`Tag increment check passed: ${candidate.tag} is the first release tag`); + process.exit(0); +} + +if (compareVersions(candidate, previous) <= 0) { + fail(`tag "${candidate.tag}" must be greater than the previous release tag "${previous.tag}"`); +} + +console.log(`Tag increment check passed: ${candidate.tag} is greater than ${previous.tag}`); diff --git a/scripts/prepare-release.mjs b/scripts/prepare-release.mjs index 85d5de4..4355159 100644 --- a/scripts/prepare-release.mjs +++ b/scripts/prepare-release.mjs @@ -56,8 +56,8 @@ function getSectionRange(content, headingPrefix) { } const version = normalizeVersion(process.argv[2] || process.env.npm_config_version); -if (!/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/.test(version)) { - fail('usage: npm run release:prepare -- '); +if (!/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version)) { + fail('usage: npm run release:prepare -- (stable SemVer only)'); } const today = new Date().toISOString().slice(0, 10); From cad8ab126fcd2a797de2703870f8bc9cb4aed64b Mon Sep 17 00:00:00 2001 From: Sythos Date: Mon, 24 Aug 2026 22:28:54 +0200 Subject: [PATCH 2/8] Handle the current release tag in the increment gate --- scripts/check-tag-increment.mjs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/scripts/check-tag-increment.mjs b/scripts/check-tag-increment.mjs index 74ec32a..de341d8 100644 --- a/scripts/check-tag-increment.mjs +++ b/scripts/check-tag-increment.mjs @@ -106,11 +106,36 @@ for (const tag of releaseTags) { } } -if (parsedTags.some((tag) => tag.version === candidate.version)) { +function readTagCommit(tag) { + try { + return execFileSync('git', ['rev-parse', '-q', '--verify', `refs/tags/${tag}^{}`], { + cwd: rootDir, + encoding: 'utf8', + }).trim(); + } catch (error) { + fail(`unable to resolve release tag "${tag}": ${error.message}`); + } +} + +const currentRef = normalizeTag(process.env.GITHUB_REF_NAME || process.env.GITHUB_REF); +const currentSha = process.env.GITHUB_SHA || ''; +const candidateTagExists = parsedTags.some((tag) => tag.tag === candidate.tag); +const isCurrentWorkflowTag = currentRef === candidate.tag && Boolean(currentSha); + +if (candidateTagExists && !isCurrentWorkflowTag) { fail(`tag "${candidate.tag}" already exists in the release history`); } +if (candidateTagExists && isCurrentWorkflowTag && readTagCommit(candidate.tag) !== currentSha) { + fail(`tag "${candidate.tag}" does not resolve to the workflow commit`); +} + +if (parsedTags.some((tag) => tag.version === candidate.version && tag.tag !== candidate.tag)) { + fail(`release version "${candidate.version}" already exists under another tag`); +} + const previous = parsedTags + .filter((tag) => tag.tag !== candidate.tag) .sort((left, right) => compareVersions(right, left))[0]; if (!previous) { From 6af6bcf62685b75ff059877ffd3800a38b54b181 Mon Sep 17 00:00:00 2001 From: Sythos Date: Mon, 24 Aug 2026 22:35:21 +0200 Subject: [PATCH 3/8] Allow the release job to read workflow artifacts --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 153229c..c1d9f13 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -288,6 +288,7 @@ jobs: if: ${{ needs.promote.result == 'success' }} runs-on: ubuntu-latest permissions: + actions: read contents: write steps: From d536adac6d010c1df4edeb0dcf95bf6dd59f1362 Mon Sep 17 00:00:00 2001 From: Sythos Date: Mon, 24 Aug 2026 22:39:34 +0200 Subject: [PATCH 4/8] Use supported Node.js 24 across both Ubuntu images --- .github/workflows/ci.yml | 7 ++----- .github/workflows/release.yml | 3 +-- Dockerfile | 6 ++---- README.md | 4 +--- 4 files changed, 6 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6e2f2b..347b8d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,14 +56,12 @@ jobs: run: npm audit --omit=dev --audit-level=high node-compatibility: - name: Node.js ${{ matrix.node-version }} compatibility + name: Node.js runtime compatibility runs-on: ubuntu-latest - continue-on-error: ${{ matrix.node-version == '18.19.1' }} strategy: fail-fast: false matrix: node-version: - - '18.19.1' - '24.19.0' steps: @@ -158,8 +156,7 @@ jobs: set -euo pipefail image="bzo:ci-${{ matrix.ubuntu }}-${{ strategy.job-index }}" case "${{ matrix.ubuntu }}" in - 24.04) expected_node="v18.19.1" ;; - 26.04) expected_node="v24.19.0" ;; + 24.04|26.04) expected_node="v24.19.0" ;; *) echo "Unsupported Ubuntu matrix value" >&2; exit 1 ;; esac actual_node="$(docker run --rm --platform "${{ matrix.platform }}" "$image" node --version)" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c1d9f13..4892968 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -219,8 +219,7 @@ jobs: run: | set -euo pipefail case "${{ matrix.ubuntu }}" in - 24.04) expected_node="v18.19.1" ;; - 26.04) expected_node="v24.19.0" ;; + 24.04|26.04) expected_node="v24.19.0" ;; *) echo "Unsupported Ubuntu matrix value" >&2; exit 1 ;; esac for platform in linux/amd64 linux/arm64; do diff --git a/Dockerfile b/Dockerfile index 9260fbb..a75c614 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,11 +3,9 @@ # Source: https://github.com/BZFlag-Dev/bzo # See LICENSE or https://www.gnu.org/licenses/agpl-3.0.html -# Pin the Node.js runtime for each Ubuntu base and verify each architecture independently. -# Ubuntu 24.04 intentionally keeps Node.js 18.19.1 for compatibility; prefer -# the Ubuntu 26.04 image for the supported Node.js 24.19.0 runtime. +# Pin the supported Node.js runtime for each Ubuntu base and verify each architecture independently. ARG UBUNTU_VERSION=24.04 -ARG NODE_VERSION_UBUNTU24=18.19.1 +ARG NODE_VERSION_UBUNTU24=24.19.0 ARG NODE_VERSION_UBUNTU26=24.19.0 ARG NODE_SHA256_UBUNTU24_AMD64=724802c45237477dbe5777923743e6c77906830cae03a82b5653ebd75b301dda ARG NODE_SHA256_UBUNTU24_ARM64=2913e8544d95c8be9e6034c539ec0584014532166a088bf742629756c3ec42e2 diff --git a/README.md b/README.md index fe1922a..3511a1a 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,7 @@ Each tagged release publishes: Every published image contains `linux/amd64` and `linux/arm64` variants. Release tags use stable `vX.Y.Z` SemVer only; prerelease and build-metadata tags are not -published. The Ubuntu 24.04 image keeps the pinned Ubuntu-compatible Node.js -`18.19.1` runtime, while Ubuntu 26.04 uses Node.js `24.19.0`; Node.js 18 is EOL, -so new deployments should prefer the Ubuntu 26.04 image. +published. Both Ubuntu bases use the pinned supported Node.js `24.19.0` runtime. ## Install with Docker From 1f369342c9dc724471d5a587902ea9c58449ce6b Mon Sep 17 00:00:00 2001 From: Sythos Date: Mon, 24 Aug 2026 22:41:12 +0200 Subject: [PATCH 5/8] Correct Node 24 container checksums --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index a75c614..59a0baf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,8 +7,8 @@ ARG UBUNTU_VERSION=24.04 ARG NODE_VERSION_UBUNTU24=24.19.0 ARG NODE_VERSION_UBUNTU26=24.19.0 -ARG NODE_SHA256_UBUNTU24_AMD64=724802c45237477dbe5777923743e6c77906830cae03a82b5653ebd75b301dda -ARG NODE_SHA256_UBUNTU24_ARM64=2913e8544d95c8be9e6034c539ec0584014532166a088bf742629756c3ec42e2 +ARG NODE_SHA256_UBUNTU24_AMD64=f625d97cd707df4ff96254916fbc5ff014f09c09effe5a1e0ca8f6d41a8789d4 +ARG NODE_SHA256_UBUNTU24_ARM64=d28c8a5bf0a808f0ed434a1dce8c54ae98f0371c0bd86ac58abc613f73e6643f ARG NODE_SHA256_UBUNTU26_AMD64=f625d97cd707df4ff96254916fbc5ff014f09c09effe5a1e0ca8f6d41a8789d4 ARG NODE_SHA256_UBUNTU26_ARM64=d28c8a5bf0a808f0ed434a1dce8c54ae98f0371c0bd86ac58abc613f73e6643f From f6a20acfbfbaff17f076f079105d59b503dc16b1 Mon Sep 17 00:00:00 2001 From: Tim Riker Date: Mon, 24 Aug 2026 14:48:00 -0600 Subject: [PATCH 6/8] Validate Node 18/24 before release builds --- .github/workflows/ci.yml | 1 + .github/workflows/release.yml | 30 ++++++++++++++++++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 347b8d0..82182d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,6 +62,7 @@ jobs: fail-fast: false matrix: node-version: + - '18.19.1' - '24.19.0' steps: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4892968..b8375e3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -112,11 +112,41 @@ jobs: with: category: /language:javascript-typescript + node-compatibility: + name: Release Node.js compatibility + needs: gate + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + node-version: + - '18.19.1' + - '24.19.0' + + steps: + - name: Check out repository + uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + + - name: Install dependencies without lifecycle scripts + run: npm ci --ignore-scripts + + - name: Check runtime compatibility + run: npm run check + build: name: Build Ubuntu ${{ matrix.ubuntu }} images needs: - gate - codeql + - node-compatibility runs-on: ubuntu-latest strategy: fail-fast: false From 8738f8503034de42579c1a8de43a5d3a39bfce82 Mon Sep 17 00:00:00 2001 From: Sythos Date: Mon, 24 Aug 2026 22:55:00 +0200 Subject: [PATCH 7/8] Restore the Ubuntu 24.04 Node.js compatibility runtime --- .github/workflows/ci.yml | 4 +++- .github/workflows/release.yml | 4 +++- Dockerfile | 10 ++++++---- README.md | 5 ++++- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82182d4..2f2be96 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,6 +58,7 @@ jobs: node-compatibility: name: Node.js runtime compatibility runs-on: ubuntu-latest + continue-on-error: ${{ matrix.node-version == '18.19.1' }} strategy: fail-fast: false matrix: @@ -157,7 +158,8 @@ jobs: set -euo pipefail image="bzo:ci-${{ matrix.ubuntu }}-${{ strategy.job-index }}" case "${{ matrix.ubuntu }}" in - 24.04|26.04) expected_node="v24.19.0" ;; + 24.04) expected_node="v18.19.1" ;; + 26.04) expected_node="v24.19.0" ;; *) echo "Unsupported Ubuntu matrix value" >&2; exit 1 ;; esac actual_node="$(docker run --rm --platform "${{ matrix.platform }}" "$image" node --version)" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b8375e3..fcde7e5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -116,6 +116,7 @@ jobs: name: Release Node.js compatibility needs: gate runs-on: ubuntu-latest + continue-on-error: ${{ matrix.node-version == '18.19.1' }} strategy: fail-fast: false matrix: @@ -249,7 +250,8 @@ jobs: run: | set -euo pipefail case "${{ matrix.ubuntu }}" in - 24.04|26.04) expected_node="v24.19.0" ;; + 24.04) expected_node="v18.19.1" ;; + 26.04) expected_node="v24.19.0" ;; *) echo "Unsupported Ubuntu matrix value" >&2; exit 1 ;; esac for platform in linux/amd64 linux/arm64; do diff --git a/Dockerfile b/Dockerfile index 59a0baf..9cd3812 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,12 +3,14 @@ # Source: https://github.com/BZFlag-Dev/bzo # See LICENSE or https://www.gnu.org/licenses/agpl-3.0.html -# Pin the supported Node.js runtime for each Ubuntu base and verify each architecture independently. +# Pin the base-specific Node.js runtime and verify each architecture independently. +# Ubuntu 24.04 intentionally keeps Node.js 18.19.1 for compatibility; Ubuntu +# 26.04 uses the supported Node.js 24.19.0 runtime. Node.js 18 is EOL. ARG UBUNTU_VERSION=24.04 -ARG NODE_VERSION_UBUNTU24=24.19.0 +ARG NODE_VERSION_UBUNTU24=18.19.1 ARG NODE_VERSION_UBUNTU26=24.19.0 -ARG NODE_SHA256_UBUNTU24_AMD64=f625d97cd707df4ff96254916fbc5ff014f09c09effe5a1e0ca8f6d41a8789d4 -ARG NODE_SHA256_UBUNTU24_ARM64=d28c8a5bf0a808f0ed434a1dce8c54ae98f0371c0bd86ac58abc613f73e6643f +ARG NODE_SHA256_UBUNTU24_AMD64=724802c45237477dbe5777923743e6c77906830cae03a82b5653ebd75b301dda +ARG NODE_SHA256_UBUNTU24_ARM64=2913e8544d95c8be9e6034c539ec0584014532166a088bf742629756c3ec42e2 ARG NODE_SHA256_UBUNTU26_AMD64=f625d97cd707df4ff96254916fbc5ff014f09c09effe5a1e0ca8f6d41a8789d4 ARG NODE_SHA256_UBUNTU26_ARM64=d28c8a5bf0a808f0ed434a1dce8c54ae98f0371c0bd86ac58abc613f73e6643f diff --git a/README.md b/README.md index 3511a1a..81d1faf 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,10 @@ Each tagged release publishes: Every published image contains `linux/amd64` and `linux/arm64` variants. Release tags use stable `vX.Y.Z` SemVer only; prerelease and build-metadata tags are not -published. Both Ubuntu bases use the pinned supported Node.js `24.19.0` runtime. +published. The Ubuntu 24.04 image intentionally keeps the pinned Node.js +`18.19.1` runtime for compatibility, while Ubuntu 26.04 uses the supported +Node.js `24.19.0` runtime. Node.js 18 is EOL, so new deployments should prefer +the Ubuntu 26.04 image. ## Install with Docker From 2244ec7e9ea9d669b177f7011cc24c12f4b08fc7 Mon Sep 17 00:00:00 2001 From: Tim Riker Date: Mon, 24 Aug 2026 15:01:37 -0600 Subject: [PATCH 8/8] Validate Node 18/24 and publish Docker on Ubuntu 26.04 --- .github/workflows/ci.yml | 19 +++--------- .github/workflows/release.yml | 43 ++++++-------------------- Dockerfile | 57 ++++++++--------------------------- README.md | 15 +++++---- 4 files changed, 34 insertions(+), 100 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f2be96..5ba18ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,7 +58,6 @@ jobs: node-compatibility: name: Node.js runtime compatibility runs-on: ubuntu-latest - continue-on-error: ${{ matrix.node-version == '18.19.1' }} strategy: fail-fast: false matrix: @@ -116,14 +115,11 @@ jobs: fail-on-severity: high container-build: - name: Build and smoke-test Ubuntu ${{ matrix.ubuntu }} ${{ matrix.platform }} + name: Build and smoke-test Ubuntu 26.04 ${{ matrix.platform }} runs-on: ubuntu-latest strategy: fail-fast: false matrix: - ubuntu: - - '24.04' - - '26.04' platform: - linux/amd64 - linux/arm64 @@ -146,9 +142,7 @@ jobs: platforms: ${{ matrix.platform }} push: false load: true - build-args: | - UBUNTU_VERSION=${{ matrix.ubuntu }} - tags: bzo:ci-${{ matrix.ubuntu }}-${{ strategy.job-index }} + tags: bzo:ci-ubuntu26.04-${{ strategy.job-index }} provenance: false sbom: false @@ -156,12 +150,7 @@ jobs: shell: bash run: | set -euo pipefail - image="bzo:ci-${{ matrix.ubuntu }}-${{ strategy.job-index }}" - case "${{ matrix.ubuntu }}" in - 24.04) expected_node="v18.19.1" ;; - 26.04) expected_node="v24.19.0" ;; - *) echo "Unsupported Ubuntu matrix value" >&2; exit 1 ;; - esac + image="bzo:ci-ubuntu26.04-${{ strategy.job-index }}" actual_node="$(docker run --rm --platform "${{ matrix.platform }}" "$image" node --version)" - test "$actual_node" = "$expected_node" + test "$actual_node" = "v24.19.0" docker run --rm --platform "${{ matrix.platform }}" "$image" node --check server.js diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fcde7e5..799e328 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -116,7 +116,6 @@ jobs: name: Release Node.js compatibility needs: gate runs-on: ubuntu-latest - continue-on-error: ${{ matrix.node-version == '18.19.1' }} strategy: fail-fast: false matrix: @@ -143,18 +142,12 @@ jobs: run: npm run check build: - name: Build Ubuntu ${{ matrix.ubuntu }} images + name: Build Ubuntu 26.04 images needs: - gate - codeql - node-compatibility runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - ubuntu: - - '24.04' - - '26.04' permissions: contents: read packages: write @@ -188,30 +181,22 @@ jobs: file: ./Dockerfile platforms: linux/amd64,linux/arm64 push: true - build-args: | - UBUNTU_VERSION=${{ matrix.ubuntu }} tags: | - ${{ needs.gate.outputs.image }}:ci-${{ needs.gate.outputs.version }}-ubuntu${{ matrix.ubuntu }}-${{ github.run_id }} + ${{ needs.gate.outputs.image }}:ci-${{ needs.gate.outputs.version }}-ubuntu26.04-${{ github.run_id }} labels: | org.opencontainers.image.title=BZFlag Battlezone Online org.opencontainers.image.version=${{ needs.gate.outputs.version }} org.opencontainers.image.revision=${{ github.sha }} - cache-from: type=gha,scope=release-ubuntu-${{ matrix.ubuntu }} + cache-from: type=gha,scope=release-ubuntu-26.04 provenance: mode=max sbom: true verify: - name: Verify Ubuntu ${{ matrix.ubuntu }} manifest and smoke test + name: Verify Ubuntu 26.04 manifest and smoke test needs: - gate - build runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - ubuntu: - - '24.04' - - '26.04' permissions: contents: read packages: read @@ -234,7 +219,7 @@ jobs: shell: bash env: IMAGE: ${{ needs.gate.outputs.image }} - STAGING_TAG: ci-${{ needs.gate.outputs.version }}-ubuntu${{ matrix.ubuntu }}-${{ github.run_id }} + STAGING_TAG: ci-${{ needs.gate.outputs.version }}-ubuntu26.04-${{ github.run_id }} run: | set -euo pipefail manifest="$(docker buildx imagetools inspect "${IMAGE}:${STAGING_TAG}")" @@ -246,17 +231,12 @@ jobs: shell: bash env: IMAGE: ${{ needs.gate.outputs.image }} - STAGING_TAG: ci-${{ needs.gate.outputs.version }}-ubuntu${{ matrix.ubuntu }}-${{ github.run_id }} + STAGING_TAG: ci-${{ needs.gate.outputs.version }}-ubuntu26.04-${{ github.run_id }} run: | set -euo pipefail - case "${{ matrix.ubuntu }}" in - 24.04) expected_node="v18.19.1" ;; - 26.04) expected_node="v24.19.0" ;; - *) echo "Unsupported Ubuntu matrix value" >&2; exit 1 ;; - esac for platform in linux/amd64 linux/arm64; do actual_node="$(docker run --rm --pull=always --platform "$platform" "${IMAGE}:${STAGING_TAG}" node --version)" - test "$actual_node" = "$expected_node" + test "$actual_node" = "v24.19.0" docker run --rm --pull=always --platform "$platform" "${IMAGE}:${STAGING_TAG}" node --check server.js done @@ -291,12 +271,9 @@ jobs: RUN_ID: ${{ github.run_id }} run: | set -euo pipefail - for ubuntu in 24.04 26.04; do - source="${IMAGE}:ci-${VERSION}-ubuntu${ubuntu}-${RUN_ID}" - docker buildx imagetools create --tag "${IMAGE}:${VERSION}-ubuntu${ubuntu}" "$source" - docker buildx imagetools create --tag "${IMAGE}:ubuntu${ubuntu}" "$source" - done - + source="${IMAGE}:ci-${VERSION}-ubuntu26.04-${RUN_ID}" + docker buildx imagetools create --tag "${IMAGE}:${VERSION}-ubuntu26.04" "$source" + docker buildx imagetools create --tag "${IMAGE}:ubuntu26.04" "$source" docker buildx imagetools create --tag "${IMAGE}:${VERSION}" "${IMAGE}:${VERSION}-ubuntu26.04" docker buildx imagetools create --tag "${IMAGE}:latest" "${IMAGE}:ubuntu26.04" diff --git a/Dockerfile b/Dockerfile index 9cd3812..e0e345f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,65 +3,34 @@ # Source: https://github.com/BZFlag-Dev/bzo # See LICENSE or https://www.gnu.org/licenses/agpl-3.0.html -# Pin the base-specific Node.js runtime and verify each architecture independently. -# Ubuntu 24.04 intentionally keeps Node.js 18.19.1 for compatibility; Ubuntu -# 26.04 uses the supported Node.js 24.19.0 runtime. Node.js 18 is EOL. -ARG UBUNTU_VERSION=24.04 -ARG NODE_VERSION_UBUNTU24=18.19.1 -ARG NODE_VERSION_UBUNTU26=24.19.0 -ARG NODE_SHA256_UBUNTU24_AMD64=724802c45237477dbe5777923743e6c77906830cae03a82b5653ebd75b301dda -ARG NODE_SHA256_UBUNTU24_ARM64=2913e8544d95c8be9e6034c539ec0584014532166a088bf742629756c3ec42e2 -ARG NODE_SHA256_UBUNTU26_AMD64=f625d97cd707df4ff96254916fbc5ff014f09c09effe5a1e0ca8f6d41a8789d4 -ARG NODE_SHA256_UBUNTU26_ARM64=d28c8a5bf0a808f0ed434a1dce8c54ae98f0371c0bd86ac58abc613f73e6643f - -FROM ubuntu:${UBUNTU_VERSION} - -ARG UBUNTU_VERSION -ARG NODE_VERSION_UBUNTU24 -ARG NODE_VERSION_UBUNTU26 -ARG NODE_SHA256_UBUNTU24_AMD64 -ARG NODE_SHA256_UBUNTU24_ARM64 -ARG NODE_SHA256_UBUNTU26_AMD64 -ARG NODE_SHA256_UBUNTU26_ARM64 -ARG TARGETARCH -RUN case "${UBUNTU_VERSION}" in \ - 24.04|26.04) ;; \ - *) echo "Unsupported Ubuntu version: ${UBUNTU_VERSION}" >&2; exit 1 ;; \ - esac +FROM ubuntu:26.04 ENV NODE_ENV=production \ PORT=3000 \ SERVER_CONFIG_PATH=/data/server-config.json \ DEBIAN_FRONTEND=noninteractive +ARG NODE_VERSION=24.19.0 +ARG NODE_SHA256_AMD64=f625d97cd707df4ff96254916fbc5ff014f09c09effe5a1e0ca8f6d41a8789d4 +ARG NODE_SHA256_ARM64=d28c8a5bf0a808f0ed434a1dce8c54ae98f0371c0bd86ac58abc613f73e6643f + RUN set -eu; \ apt-get update; \ apt-get install -y --no-install-recommends ca-certificates curl libstdc++6; \ - case "${UBUNTU_VERSION}" in \ - 24.04) node_version="${NODE_VERSION_UBUNTU24}" ;; \ - 26.04) node_version="${NODE_VERSION_UBUNTU26}" ;; \ - *) echo "Unsupported Ubuntu version: ${UBUNTU_VERSION}" >&2; exit 1 ;; \ - esac; \ - case "${TARGETARCH}" in \ - amd64) node_arch=x64 ;; \ - arm64) node_arch=arm64 ;; \ - *) echo "Unsupported target architecture: ${TARGETARCH}" >&2; exit 1 ;; \ - esac; \ - case "${UBUNTU_VERSION}:${TARGETARCH}" in \ - 24.04:amd64) node_checksum="${NODE_SHA256_UBUNTU24_AMD64}" ;; \ - 24.04:arm64) node_checksum="${NODE_SHA256_UBUNTU24_ARM64}" ;; \ - 26.04:amd64) node_checksum="${NODE_SHA256_UBUNTU26_AMD64}" ;; \ - 26.04:arm64) node_checksum="${NODE_SHA256_UBUNTU26_ARM64}" ;; \ - *) echo "Unsupported Ubuntu and architecture combination" >&2; exit 1 ;; \ + arch="$(dpkg --print-architecture)"; \ + case "$arch" in \ + amd64) node_arch="x64"; node_checksum="$NODE_SHA256_AMD64" ;; \ + arm64) node_arch="arm64"; node_checksum="$NODE_SHA256_ARM64" ;; \ + *) echo "Unsupported architecture: $arch" >&2; exit 1 ;; \ esac; \ - node_archive="node-v${node_version}-linux-${node_arch}.tar.gz"; \ + node_archive="node-v${NODE_VERSION}-linux-${node_arch}.tar.gz"; \ curl --fail --silent --show-error --location --retry 3 \ --output "/tmp/${node_archive}" \ - "https://nodejs.org/dist/v${node_version}/${node_archive}"; \ + "https://nodejs.org/dist/v${NODE_VERSION}/${node_archive}"; \ echo "${node_checksum} /tmp/${node_archive}" | sha256sum --check --strict; \ tar --extract --gzip --file "/tmp/${node_archive}" \ --strip-components=1 --directory /usr/local; \ - test "$(node --version)" = "v${node_version}"; \ + test "$(node --version)" = "v${NODE_VERSION}"; \ rm -f "/tmp/${node_archive}"; \ apt-get purge -y --auto-remove curl; \ rm -rf /var/lib/apt/lists/* diff --git a/README.md b/README.md index 81d1faf..22d7b1c 100644 --- a/README.md +++ b/README.md @@ -17,17 +17,16 @@ Each tagged release publishes: - a GitHub release with notes generated from [CHANGELOG.md](CHANGELOG.md) - a source tarball -- a versioned Ubuntu 24.04 image at `ghcr.io/bzflag-dev/bzo:-ubuntu24.04` - a versioned Ubuntu 26.04 image at `ghcr.io/bzflag-dev/bzo:-ubuntu26.04` -- moving `ubuntu24.04` and `ubuntu26.04` tags for the two base images +- a moving `ubuntu26.04` tag - `ghcr.io/bzflag-dev/bzo:` and `ghcr.io/bzflag-dev/bzo:latest`, both using Ubuntu 26.04 Every published image contains `linux/amd64` and `linux/arm64` variants. Release tags use stable `vX.Y.Z` SemVer only; prerelease and build-metadata tags are not -published. The Ubuntu 24.04 image intentionally keeps the pinned Node.js -`18.19.1` runtime for compatibility, while Ubuntu 26.04 uses the supported -Node.js `24.19.0` runtime. Node.js 18 is EOL, so new deployments should prefer -the Ubuntu 26.04 image. +published. Ubuntu 26.04 images use the pinned Node.js `24.19.0` runtime. + +Docker images are built on Ubuntu 26.04 with pinned Node.js `24.19.0`. +Runtime compatibility is validated in CI on Node.js `18.19.1` and `24.19.0`. ## Install with Docker @@ -63,7 +62,7 @@ The image defaults to `SERVER_CONFIG_PATH=/data/server-config.json`. ### Prerequisites -- Node.js 24 LTS or a newer supported LTS +- Node.js 18.19.1 or Node.js 24.19.0 - npm ### Setup @@ -218,7 +217,7 @@ The release workflow will: 2. install dependencies and run lint, validation, audit, and CodeQL checks 3. fail if `package.json` does not match the pushed tag 4. fail if [CHANGELOG.md](CHANGELOG.md) does not contain a matching non-placeholder section -5. build and smoke-test both Ubuntu LTS images, including their pinned Node.js runtimes, for `linux/amd64` and `linux/arm64` +5. build and smoke-test Ubuntu 26.04 images with pinned Node.js `24.19.0` for `linux/amd64` and `linux/arm64` 6. promote the verified versioned and moving Docker tags to GHCR 7. publish a GitHub release and attach a source tarball