From 32c20ba55661ef186eb572e779429faae2eda6f7 Mon Sep 17 00:00:00 2001 From: whyour Date: Sun, 25 Oct 2026 17:27:38 +0800 Subject: [PATCH] build: unify container Node 24 and build ARM64 bundles natively Unify Alpine and Debian images on Node 24, remove Debian arm/v7 from the published matrix, and build ARM64 upgrade bundles on native runners to avoid QEMU SIGILL. Combines #3109 and #3110. --- .github/workflows/build-docker-image.yml | 19 +++++++++-------- .github/workflows/release-readiness.yml | 4 ++-- docker/Dockerfile | 5 +++-- docker/Dockerfile.310 | 5 +++-- docker/Dockerfile.debian | 12 ++++++++--- docker/Dockerfile.debian310 | 14 +++++++++---- docker/install-node-debian.sh | 22 ++++++++++++++++++++ docs/development/platform-upgrade-bundles.md | 6 +++--- scripts/publish-upgrade-bundles.cjs | 1 - test/back/platform-bundle.test.cjs | 4 +++- test/back/publish-upgrade-bundles.test.cjs | 14 ++++++++----- 11 files changed, 74 insertions(+), 32 deletions(-) create mode 100644 docker/install-node-debian.sh diff --git a/.github/workflows/build-docker-image.yml b/.github/workflows/build-docker-image.yml index 3b0fb1ba..11c0f2f3 100644 --- a/.github/workflows/build-docker-image.yml +++ b/.github/workflows/build-docker-image.yml @@ -247,7 +247,7 @@ jobs: version: "8.3.1" - uses: actions/setup-node@v6 with: - node-version: "20" + node-version: "24" cache: "pnpm" cache-dependency-path: pnpm-lock.yaml @@ -336,7 +336,7 @@ jobs: version: "8.3.1" - uses: actions/setup-node@v6 with: - node-version: "20" + node-version: "24" cache: "pnpm" cache-dependency-path: pnpm-lock.yaml @@ -394,7 +394,7 @@ jobs: SOURCE_COMMIT=${{ github.sha }} network: host # Keep s390x on Debian: npm can hang under QEMU with Alpine (nodejs/docker-node#1973). - platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x + platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x context: . file: ./docker/Dockerfile.debian push: true @@ -423,7 +423,7 @@ jobs: version: "8.3.1" - uses: actions/setup-node@v6 with: - node-version: "20" + node-version: "24" cache: "pnpm" cache-dependency-path: pnpm-lock.yaml @@ -495,7 +495,7 @@ jobs: version: "8.3.1" - uses: actions/setup-node@v6 with: - node-version: "20" + node-version: "24" cache: "pnpm" cache-dependency-path: pnpm-lock.yaml @@ -538,7 +538,7 @@ jobs: SOURCE_COMMIT=${{ github.sha }} network: host # Keep s390x on Debian: npm can hang under QEMU with Alpine (nodejs/docker-node#1973). - platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x + platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x context: . file: ./docker/Dockerfile.debian310 push: true @@ -662,7 +662,8 @@ jobs: # Both channels publish packages from this run's immutable image digests. if: ${{ !inputs.static_sync_only && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop') }} needs: [build-alpine, build-debian] - runs-on: ubuntu-22.04 + # Node 24 can SIGILL while archiving ARM64 musl under x64 QEMU. + runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-22.04' }} timeout-minutes: 90 permissions: contents: read @@ -679,13 +680,13 @@ jobs: - { platform: linux/s390x, libc: musl, name: s390x-musl } - { platform: linux/386, libc: musl, name: ia32-musl } - { platform: linux/amd64, libc: glibc, name: x64-glibc } - - { platform: linux/arm/v7, libc: glibc, name: armv7-glibc } - { platform: linux/arm64, libc: glibc, name: arm64-glibc } - { platform: linux/ppc64le, libc: glibc, name: ppc64-glibc } - { platform: linux/s390x, libc: glibc, name: s390x-glibc } steps: - uses: actions/checkout@v6 - uses: docker/setup-qemu-action@v4 + if: ${{ matrix.platform != 'linux/arm64' }} with: cache-image: false - uses: docker/setup-buildx-action@v4 @@ -721,7 +722,7 @@ jobs: - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: - node-version: '20' + node-version: '24' - uses: pnpm/action-setup@v6 with: version: '8.3.1' diff --git a/.github/workflows/release-readiness.yml b/.github/workflows/release-readiness.yml index facabafd..cf7a2535 100644 --- a/.github/workflows/release-readiness.yml +++ b/.github/workflows/release-readiness.yml @@ -69,7 +69,7 @@ jobs: - image: debian file: docker/Dockerfile.debian cache: debian - platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x + platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x - image: alpine-python3.10 file: docker/Dockerfile.310 cache: alpine-python3.10 @@ -77,7 +77,7 @@ jobs: - image: debian-python3.10 file: docker/Dockerfile.debian310 cache: debian-python3.10 - platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x + platforms: linux/amd64,linux/arm64,linux/ppc64le,linux/s390x steps: - uses: actions/checkout@v6 with: diff --git a/docker/Dockerfile b/docker/Dockerfile index 8d2a1c37..3fb2e840 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,6 +1,6 @@ # Run Node package installation natively on the builder. Node/npm can spin at # 100% CPU when Alpine s390x is emulated through QEMU. -FROM --platform=$BUILDPLATFORM node:18-alpine3.18 AS builder +FROM --platform=$BUILDPLATFORM node:24.18.1-alpine3.24 AS builder ARG TARGETARCH ARG TARGETVARIANT @@ -32,7 +32,7 @@ RUN set -x \ # Pin amd64 to upstream's musl runtime after native URL parser crashes with APK Node. FROM --platform=linux/amd64 node:24.18.1-alpine3.24@sha256:f70403e87646dc51b45295f4b8b70cdad0b63d2297c4c9899119b03f7af7a6b3 AS node-runtime-amd64 -FROM python:3.11-alpine +FROM python:3.11-alpine3.24 ARG TARGETARCH ARG TARGETVARIANT @@ -80,6 +80,7 @@ RUN --mount=type=bind,from=node-runtime-amd64,source=/usr/local/bin/node,target= cp /tmp/node-runtime-amd64 /usr/local/bin/node; \ ln -sf /usr/local/bin/node /usr/bin/node; \ fi \ + && node --version | grep -E '^v24\.' \ && rm -rf /var/cache/apk/* \ && apk update \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ diff --git a/docker/Dockerfile.310 b/docker/Dockerfile.310 index 22000149..2e366f77 100644 --- a/docker/Dockerfile.310 +++ b/docker/Dockerfile.310 @@ -1,6 +1,6 @@ # Run Node package installation natively on the builder. Node/npm can spin at # 100% CPU when Alpine s390x is emulated through QEMU. -FROM --platform=$BUILDPLATFORM node:18-alpine3.18 AS builder +FROM --platform=$BUILDPLATFORM node:24.18.1-alpine3.24 AS builder ARG TARGETARCH ARG TARGETVARIANT @@ -32,7 +32,7 @@ RUN set -x \ # Pin amd64 to upstream's musl runtime after native URL parser crashes with APK Node. FROM --platform=linux/amd64 node:24.18.1-alpine3.24@sha256:f70403e87646dc51b45295f4b8b70cdad0b63d2297c4c9899119b03f7af7a6b3 AS node-runtime-amd64 -FROM python:3.10-alpine +FROM python:3.10-alpine3.24 ARG TARGETARCH ARG TARGETVARIANT @@ -80,6 +80,7 @@ RUN --mount=type=bind,from=node-runtime-amd64,source=/usr/local/bin/node,target= cp /tmp/node-runtime-amd64 /usr/local/bin/node; \ ln -sf /usr/local/bin/node /usr/bin/node; \ fi \ + && node --version | grep -E '^v24\.' \ && rm -rf /var/cache/apk/* \ && apk update \ && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ diff --git a/docker/Dockerfile.debian b/docker/Dockerfile.debian index 871feb18..34fc65b8 100644 --- a/docker/Dockerfile.debian +++ b/docker/Dockerfile.debian @@ -1,6 +1,10 @@ -# Node 20 Bookworm is the latest official Node image variant that covers the -# full Debian build matrix, including arm/v7, ppc64le, and s390x. -FROM node:20-bookworm-slim AS nodebuilder +# Download and verify the target Node archive without running under QEMU. +FROM --platform=$BUILDPLATFORM debian:bookworm-slim AS nodebuilder +ARG TARGETARCH +COPY docker/install-node-debian.sh /tmp/install-node-debian.sh +RUN apt-get update && \ + apt-get install --no-install-recommends -y ca-certificates curl xz-utils && \ + sh /tmp/install-node-debian.sh "${TARGETARCH}" FROM python:3.11.14-slim-bookworm AS builder COPY package.json .npmrc pnpm-lock.yaml /tmp/build/ @@ -11,6 +15,7 @@ RUN set -x && \ apt-get update && \ apt-get install --no-install-recommends -y libatomic1 && \ npm i -g pnpm@8.3.1 && \ + test "$(node --version)" = "v24.18.1" && \ test "$(pnpm --version)" = "8.3.1" && \ cd /tmp/build && \ pnpm install --prod --frozen-lockfile @@ -71,6 +76,7 @@ RUN set -x && \ git config --global user.name "qinglong" && \ git config --global http.postBuffer 524288000 && \ npm install -g pnpm@8.3.1 pm2 ts-node typescript@5 && \ + test "$(node --version)" = "v24.18.1" && \ test "$(pnpm --version)" = "8.3.1" && \ npm cache clean --force && \ rm -rf /root/.cache && \ diff --git a/docker/Dockerfile.debian310 b/docker/Dockerfile.debian310 index 688d7dde..1b7c9af4 100644 --- a/docker/Dockerfile.debian310 +++ b/docker/Dockerfile.debian310 @@ -1,8 +1,12 @@ -# Node 20 Bookworm is the latest official Node image variant that covers the -# full Debian build matrix, including arm/v7, ppc64le, and s390x. -FROM node:20-bookworm-slim AS nodebuilder +# Download and verify the target Node archive without running under QEMU. +FROM --platform=$BUILDPLATFORM debian:bookworm-slim AS nodebuilder +ARG TARGETARCH +COPY docker/install-node-debian.sh /tmp/install-node-debian.sh +RUN apt-get update && \ + apt-get install --no-install-recommends -y ca-certificates curl xz-utils && \ + sh /tmp/install-node-debian.sh "${TARGETARCH}" -# Keep the full release matrix, including s390x, on the same immutable base. +# Keep s390x on the same immutable Python base as the other Debian targets. # The floating 3.10-slim-bookworm tag no longer includes s390x. FROM python:3.10.19-slim-bookworm@sha256:23f63358922e79a794f71be8f3723c84e5ccca9638af3f74456dc73d6184499e AS builder COPY package.json .npmrc pnpm-lock.yaml /tmp/build/ @@ -13,6 +17,7 @@ RUN set -x && \ apt-get update && \ apt-get install --no-install-recommends -y libatomic1 && \ npm i -g pnpm@8.3.1 && \ + test "$(node --version)" = "v24.18.1" && \ cd /tmp/build && \ pnpm install --prod --frozen-lockfile @@ -71,6 +76,7 @@ RUN set -x && \ git config --global user.name "qinglong" && \ git config --global http.postBuffer 524288000 && \ npm install -g pnpm@8.3.1 pm2 ts-node typescript@5 && \ + test "$(node --version)" = "v24.18.1" && \ npm cache clean --force && \ rm -rf /root/.cache && \ rm -rf /root/.npm && \ diff --git a/docker/install-node-debian.sh b/docker/install-node-debian.sh new file mode 100644 index 00000000..ca17eaa4 --- /dev/null +++ b/docker/install-node-debian.sh @@ -0,0 +1,22 @@ +#!/bin/sh +set -eu + +# The official Node 24 Bookworm image does not cover s390x. Use the same +# verified upstream release archive on every supported Debian architecture. +version=24.18.1 +case "${1:?Missing Docker TARGETARCH}" in + amd64) arch=x64; checksum=d6c664df3f3f61458e8c277585571328522d705166723a7c7823a9253a4d15a0 ;; + arm64) arch=arm64; checksum=7201e3a09dc825bac57867c81913e2b8f0ef87d04cb9082af4cda82f6ff3d88c ;; + ppc64le) arch=ppc64le; checksum=c47812c13862be7f6e71194c434c4d78a99096e72efb9c31a2627437e056d669 ;; + s390x) arch=s390x; checksum=acf57bd726f35afc2ba8475194e5a8bd532c00e1a3b2436295b2fbdde199d0e9 ;; + *) echo "Node ${version} has no supported Debian binary for TARGETARCH=$1" >&2; exit 1 ;; +esac + +archive="node-v${version}-linux-${arch}.tar.xz" +curl --fail --location --silent --show-error --retry 3 \ + "https://nodejs.org/dist/v${version}/${archive}" -o "/tmp/${archive}" +printf '%s %s\n' "${checksum}" "/tmp/${archive}" | sha256sum --check --status +tar -xJf "/tmp/${archive}" -C /usr/local --strip-components=1 --no-same-owner +rm "/tmp/${archive}" +# The target binary is executed and version-checked in the target builder/runtime stages. +test -x /usr/local/bin/node diff --git a/docs/development/platform-upgrade-bundles.md b/docs/development/platform-upgrade-bundles.md index d49f9a38..6b2f019f 100644 --- a/docs/development/platform-upgrade-bundles.md +++ b/docs/development/platform-upgrade-bundles.md @@ -18,7 +18,7 @@ gh workflow run build-docker-image.yml --ref develop 开发包只保留最新一个已验证批次:所有平台资产和渠道索引的大小、SHA256 校验成功后,删除其他名称匹配的开发构建 prerelease;不删除正式包、渠道索引或无法分类的旧 release。构建或上传失败不会删除上一套可用包。旧批次被删除后,已选中旧索引但尚未下载完的客户端可能需要重新发起下载;准备失败仍保留旧服务和数据。 -开发和正式包都必须在 12 个平台完成归档、解包后复验及依赖试运行,并且全部上传资产的大小和 SHA256 复验通过后,才更新各自索引。平台范围与现有 Alpine/Debian 镜像一致:ARMv6 仅提供 musl 包,不提供 ARMv6 glibc 包。 +开发和正式包都必须在 11 个平台完成归档、解包后复验及依赖试运行,并且全部上传资产的大小和 SHA256 复验通过后,才更新各自索引。平台范围与现有 Alpine/Debian 镜像一致:ARMv6 和 ARMv7 仅提供 musl 包,Debian 不再发布 ARMv7 镜像或 glibc 平台包。 版本入口为 GitHub prerelease `upgrade-v` 下的 `latest.json`;在线正式渠道保留 `upgrade-master/latest.json`,兼容现有客户端。二者指向同一套完整包,指定版本与 latest 不重复生成产物。索引记录渠道、版本、源码提交、构建批次、准确字节数、SHA256,以及 `upgrade-build-v--` 下的固定下载 URL。相同版本的镜像 tag 重新发布时,完整包也重新生成,版本和正式渠道索引同步覆盖到新批次;同一提交重跑也允许产生新内容。已选中旧索引的下载继续使用旧批次资产,避免覆盖文件导致哈希不匹配。master 的镜像构建和完整包发布整条流水线串行执行,避免镜像 tag 与完整包被并发重发交错覆盖;旧工作流明确重跑时也允许同版本、同提交重新发布。过期源码提交不能推进对应渠道或版本索引;平台包上传失败时不切换索引。 @@ -64,7 +64,7 @@ QL_DIR=/opt/qinglong QL_DATA_DIR=/opt/qinglong/data bash /opt/qinglong/shell/sta `--install` 拒绝非空目录,不覆盖既有配置。该包安装青龙及其 Node 生产依赖;首次启动仍需已有 Python、Bash、OpenSSL、PM2、nginx 等相应运行环境,不会在线安装或替换它们。手动启动脚本会在启动工作进程前完成首次 gRPC 证书准备,避免慢设备生成密钥耗尽工作进程的就绪期限;已有证书继续复用。服务启动和系统自启按原手动安装流程配置。 -ARMv6 小设备使用 `linux-armv6-musl.tar.gz`,要求已有兼容的 musl 系统及硬浮点 Node。ARMv6 glibc(例如 Raspbian/Raspberry Pi OS 32 位系统)不在平台包支持范围内,不能使用 ARMv7 或 musl 包替代;安装器仍会检查实际架构和 libc,拒绝不匹配的包。 +ARMv6 小设备使用 `linux-armv6-musl.tar.gz`,要求已有兼容的 musl 系统及硬浮点 Node。ARMv6 glibc(例如 Raspbian/Raspberry Pi OS 32 位系统)不在平台包支持范围内,不能使用其他架构或 musl 包替代;安装器仍会检查实际架构和 libc,拒绝不匹配的包。 ## npm 面板包 @@ -89,7 +89,7 @@ master 发布任务等待上述测试通过,直接发布已验证的 `.tgz`, - 最低 Node 为 20.18.1,Node-API 至少 6;安装器只检查,不替换 Node。 - 以 Node 实际运行架构、ARM 指令集和 libc 类型/版本选择包;不依据 Docker 架构别名猜测。 - glibc、musl 分包,并记录构建系统 libc 下限。系统库仍以目标设备加载测试作为最后检查。 -- ARMv6 与 ARMv7 使用不同包名。ARMv6 在目标镜像中从源码编译 SQLite,禁止复用 generic `arm` 的 ARMv7 预编译件;构建需通过加载和内存库读写,失败则不发布渠道。 +- ARMv6 保留独立包名。ARMv6 在目标镜像中从源码编译 SQLite,禁止复用 generic `arm` 的 ARMv7 预编译件;构建需通过加载和内存库读写,失败则不发布渠道。 - ARMv6 的归档构建、在线更新工作进程及独立离线安装器使用 `--jitless`,避开 ARM1176 上 V8 的优化编译崩溃;保留现有 Node 二进制,Node 24 及以上的 ARMv6 面板进程通过共享 `nodeRuntime` 使用 `--no-opt --no-sparkplug --no-maglev`,其他架构和较低 Node 版本保持原启动参数。ARMv6 的 gRPC 工作进程首次启动及重建最多等待 2 分钟;升级后的整体健康检查最多等待 5 分钟,覆盖慢设备按顺序启动数据库、调度与 HTTP 工作进程的耗时;其他平台仍为 60 秒,超时或版本校验失败仍执行回退。依赖加载与后端预启动使用现有 Node 的正常模式,使 Node 20.18.1 的内置 HTTP/WebAssembly 组件保持可用。预启动只有一个后端进程,按顺序初始化数据库、gRPC、HTTP,不启动 PM2/cluster;ARMv6 V8 堆上限 128 MiB、期限 5 分钟,其他平台为 192 MiB/60 秒。堆上限不是 RSS 总内存上限,系统仍需为现有服务和 OpenSSL 留出内存。依赖修复的包管理器及编译使用单并发、低网络并发和有界时长;ARMv6 包管理器采用 `--jitless`、256 MiB 堆上限,并优先复用版本匹配的现有 Node 头文件与实际运行时编译配置;超时、中断会终止整个进程组并清理临时目录。 - tar 中允许 pnpm 的包内符号链接;拒绝绝对路径、越界路径、包外链接、硬链接、设备条目和经符号链接写入的条目。重启前再次校验依赖,暂存失败保留上一次可用的就绪包。 diff --git a/scripts/publish-upgrade-bundles.cjs b/scripts/publish-upgrade-bundles.cjs index 3dc64fd7..28341124 100644 --- a/scripts/publish-upgrade-bundles.cjs +++ b/scripts/publish-upgrade-bundles.cjs @@ -13,7 +13,6 @@ const platforms = [ 'linux-s390x-musl', 'linux-ia32-musl', 'linux-x64-glibc', - 'linux-armv7-glibc', 'linux-arm64-glibc', 'linux-ppc64-glibc', 'linux-s390x-glibc', diff --git a/test/back/platform-bundle.test.cjs b/test/back/platform-bundle.test.cjs index 10b1def3..c8d941e2 100644 --- a/test/back/platform-bundle.test.cjs +++ b/test/back/platform-bundle.test.cjs @@ -591,7 +591,9 @@ test('publishing requires every platform and validates all immutable archive has ); } const index = releaseIndex(f.directory, sourceCommit, repository); - assert.equal(Object.keys(index.packages).length, 12); + assert.equal(Object.keys(index.packages).length, 11); + assert.ok(index.packages['linux-armv7-musl']); + assert.equal(index.packages['linux-armv7-glibc'], undefined); const armv6 = path.join( f.directory, `qinglong-${sourceCommit}-linux-armv6-musl.tar.gz`, diff --git a/test/back/publish-upgrade-bundles.test.cjs b/test/back/publish-upgrade-bundles.test.cjs index 53581a89..62ee74ec 100644 --- a/test/back/publish-upgrade-bundles.test.cjs +++ b/test/back/publish-upgrade-bundles.test.cjs @@ -162,10 +162,12 @@ test('publishes every platform and installer before advancing version and legacy assert.equal(index.version, '2.24.0'); assert.equal(index.buildId, '100-1'); assert.equal(index.sourceCommit, f.options.commit); - assert.equal(Object.keys(index.packages).length, 12); + assert.equal(Object.keys(index.packages).length, 11); + assert.ok(index.packages['linux-armv7-musl']); + assert.equal(index.packages['linux-armv7-glibc'], undefined); assert.deepEqual(index, f.index('upgrade-v2.24.0')); const tag = 'upgrade-build-v2.24.0-100-1'; - assert.equal(f.state.releases.get(tag).size, 26); + assert.equal(f.state.releases.get(tag).size, 24); for (const entry of [...Object.values(index.packages), index.installer]) { const url = new URL(entry.url), filename = path.basename(url.pathname); @@ -350,11 +352,13 @@ test('automatic development publication shares musl/glibc packages without chang const development = f.index('upgrade-develop'); assert.equal(development.channel, 'develop'); assert.equal(development.sourceCommit, f.state.developHead); - assert.equal(Object.keys(development.packages).length, 12); + assert.equal(Object.keys(development.packages).length, 11); assert.ok(development.packages['linux-x64-musl']); assert.ok(development.packages['linux-x64-glibc']); assert.ok(development.packages['linux-armv6-musl']); assert.equal(development.packages['linux-armv6-glibc'], undefined); + assert.ok(development.packages['linux-armv7-musl']); + assert.equal(development.packages['linux-armv7-glibc'], undefined); for (const entry of [ ...Object.values(development.packages), development.installer, @@ -362,10 +366,10 @@ test('automatic development publication shares musl/glibc packages without chang assert.ok(entry.url.includes('/upgrade-build-develop-v2.24.0-100-1/')); assert.deepEqual(f.index(), production); assert.deepEqual(f.index('upgrade-v2.24.0'), production); - assert.equal(f.state.releases.get('upgrade-build-v2.24.0-100-1').size, 26); + assert.equal(f.state.releases.get('upgrade-build-v2.24.0-100-1').size, 24); assert.equal( f.state.releases.get('upgrade-build-develop-v2.24.0-100-1').size, - 26, + 24, ); });