diff --git a/.github/docker/Dockerfile.ci b/.github/docker/Dockerfile.ci index 9bb7a6151..efe5aca83 100644 --- a/.github/docker/Dockerfile.ci +++ b/.github/docker/Dockerfile.ci @@ -90,8 +90,13 @@ RUN curl --retry 5 --retry-delay 5 --retry-connrefused -fsSL https://cli.github. # and "gnupg" was renamed to "gpg" on Ubuntu 24.04 anyway, so NodeSource's script # fails before it can add its own repo. Direct tarball download is network-simpler # (one host: nodejs.org) and doesn't touch apt at all. +# The hash comes from the release's signed SHASUMS256.txt, and the archive is +# checked before it is unpacked. scripts/ubicloud/setup-free-suite.sh pins the +# same version and hash (test/ci-image-cli-pin.test.ts keeps them equal). ENV NODE_VERSION=22.23.4 +ARG NODE_SHA256=1805bcd35e6815559bd2eb97db438d3fdb0eb3a5e6a977b7c1badca76337e9ce RUN curl --retry 5 --retry-delay 5 --retry-connrefused -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-x64.tar.xz" -o /tmp/node.tar.xz \ + && echo "${NODE_SHA256} /tmp/node.tar.xz" | sha256sum -c - \ && tar -xJ -C /usr/local --strip-components=1 --no-same-owner -f /tmp/node.tar.xz \ && rm -f /tmp/node.tar.xz \ && node --version \ diff --git a/scripts/ubicloud/setup-free-suite.sh b/scripts/ubicloud/setup-free-suite.sh index dba34076d..aed51c2c9 100755 --- a/scripts/ubicloud/setup-free-suite.sh +++ b/scripts/ubicloud/setup-free-suite.sh @@ -5,7 +5,8 @@ set -euo pipefail BUN_VERSION=1.4.2 -NODE_VERSION=22.20.0 +NODE_VERSION=22.23.4 +NODE_SHA256=1805bcd35e6815559bd2eb97db438d3fdb0eb3a5e6a977b7c1badca76337e9ce export DEBIAN_FRONTEND=noninteractive sudo -E apt-get update -qq @@ -17,9 +18,13 @@ sudo -E apt-get install -y -qq --no-install-recommends \ # needs; GitHub-hosted runners ship with this relaxed. sudo sysctl -qw kernel.apparmor_restrict_unprivileged_userns=0 +# The same verified release archive as .github/docker/Dockerfile.ci. if ! command -v node >/dev/null; then - curl -fsSL "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \ - | sudo tar -xJ -C /usr/local --strip-components=1 + node_tmp=$(mktemp -d) + curl -fsSL "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" -o "$node_tmp/node.tar.xz" + echo "$NODE_SHA256 $node_tmp/node.tar.xz" | sha256sum -c - >/dev/null + sudo tar -xJ -C /usr/local --strip-components=1 -f "$node_tmp/node.tar.xz" + rm -rf "$node_tmp" fi # The exact release archive, verified by SHA-256 before it is unpacked, as in diff --git a/test/ci-image-cli-pin.test.ts b/test/ci-image-cli-pin.test.ts index de3fb649a..5a8968765 100644 --- a/test/ci-image-cli-pin.test.ts +++ b/test/ci-image-cli-pin.test.ts @@ -193,7 +193,7 @@ describe('other CI Bun installs match the image (#1713, #1706)', () => { test(`${rel} downloads the verified linux-x64 archive instead of piping an installer into a shell`, () => { expect(curlPipedToShell(source)).toEqual([]); const download = source.indexOf('/releases/download/bun-v$BUN_VERSION/bun-linux-x64.zip'); - const verify = source.indexOf('sha256sum -c'); + const verify = source.indexOf('sha256sum -c', download); const unpack = source.indexOf('unzip -q'); expect(download).toBeGreaterThan(-1); expect(verify).toBeGreaterThan(download); @@ -216,3 +216,42 @@ describe('other CI Bun installs match the image (#1713, #1706)', () => { }); } }); + +describe('CI Node.js installs match the image and are verified (#3354)', () => { + const docker = fs.readFileSync(DOCKERFILE, 'utf-8'); + const imageVersion = docker.match(/^ENV NODE_VERSION=(\S+)$/m)?.[1]; + const imageSha = docker.match(/^ARG NODE_SHA256=(\S+)$/m)?.[1]; + const nodeRun = logicalLines(docker).find((line) => line.includes('nodejs.org/dist/v${NODE_VERSION}/')) ?? ''; + + test('Dockerfile.ci pins an exact Node.js version and the SHA-256 of its linux-x64 archive', () => { + expect(imageVersion).toMatch(/^\d+\.\d+\.\d+$/); + expect(imageSha).toMatch(/^[0-9a-f]{64}$/); + }); + + test('Dockerfile.ci checks the Node.js archive before it is unpacked', () => { + expect(nodeRun, 'Node.js must come from the nodejs.org release archive').not.toBe(''); + const download = nodeRun.indexOf('-o /tmp/node.tar.xz'); + const verify = nodeRun.indexOf('echo "${NODE_SHA256} /tmp/node.tar.xz" | sha256sum -c -'); + const unpack = nodeRun.indexOf('tar -xJ'); + expect(download).toBeGreaterThan(-1); + expect(verify).toBeGreaterThan(download); + expect(unpack).toBeGreaterThan(verify); + }); + + const rel = 'scripts/ubicloud/setup-free-suite.sh'; + const source = fs.readFileSync(path.join(ROOT, rel), 'utf-8'); + + test(`${rel} pins the same Node.js version and SHA-256 as Dockerfile.ci`, () => { + expect(source.match(/^NODE_VERSION=(\S+)$/m)?.[1]).toBe(imageVersion!); + expect(source.match(/^NODE_SHA256=(\S+)$/m)?.[1]).toBe(imageSha!); + }); + + test(`${rel} checks the Node.js archive before it is unpacked`, () => { + const download = source.indexOf('/node-v$NODE_VERSION-linux-x64.tar.xz" -o "$node_tmp/node.tar.xz"'); + const verify = source.indexOf('echo "$NODE_SHA256 $node_tmp/node.tar.xz" | sha256sum -c -'); + const unpack = source.indexOf('tar -xJ'); + expect(download).toBeGreaterThan(-1); + expect(verify).toBeGreaterThan(download); + expect(unpack).toBeGreaterThan(verify); + }); +});