From 0b4947b038fd42407a44abd968641a8a465358d2 Mon Sep 17 00:00:00 2001 From: cluade-reviewer-andresmgsl Date: Wed, 22 Jul 2026 21:39:51 +0000 Subject: [PATCH 1/4] install-apt: bootstrap Node 22 via NodeSource when distro nodejs is too old (#1) The package depends on nodejs (>= 22.12), but Debian 13 ships Node 20 and Ubuntu 24.04 ships Node 18, so a fresh container failed apt-get install with an unmet dependency. install-apt.sh now checks whether any configured apt source can satisfy the requirement and, if not, adds the NodeSource Node 22 repository before installing. README documents the behaviour and the manual equivalent. Verified on fresh debian:13 and ubuntu:24.04 containers: one-line setup, apt-get install stoke, stoke --version all succeed. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 9 +++++++++ scripts/install-apt.sh | 35 +++++++++++++++++++++++++++++++---- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2b840d6..bfea833 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,15 @@ sudo apt-get update && sudo apt-get install stoke Upgrades then arrive through regular `apt-get upgrade`. +The package depends on `nodejs (>= 22.12)`, which the distro archives of Debian 13 (Node 20) and Ubuntu 24.04 (Node 18) cannot satisfy. `install-apt.sh` handles this automatically by adding the [NodeSource](https://deb.nodesource.com) Node 22 repository when no configured apt source offers a new-enough nodejs. If you follow the manual steps instead, make sure such a source is available before `apt-get install stoke`: + +```bash +curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \ + | sudo tee /etc/apt/keyrings/nodesource.asc >/dev/null +echo "deb [signed-by=/etc/apt/keyrings/nodesource.asc] https://deb.nodesource.com/node_22.x nodistro main" \ + | sudo tee /etc/apt/sources.list.d/nodesource.list +``` + Note: apt releases that verify OpenPGP with `sqv` (Debian 13+, apt >= 2.9) currently reject the signature Forgejo generates for its Debian registry (an upstream signing bug). `install-apt.sh` detects this and falls back to a `[trusted=yes]` source — integrity then relies on HTTPS to the forge. The script prefers the signed source, so setups heal automatically once the forge is fixed. As a fallback, each release also has the `.deb` attached for direct install: `sudo dpkg -i stoke__all.deb`. diff --git a/scripts/install-apt.sh b/scripts/install-apt.sh index 7823cf6..90ceb35 100755 --- a/scripts/install-apt.sh +++ b/scripts/install-apt.sh @@ -28,13 +28,38 @@ if [ "$(id -u)" -ne 0 ]; then SUDO="sudo" fi -update_only_this_source() { +update_only_source() { $SUDO apt-get update \ - -o Dir::Etc::sourcelist="$LIST" \ + -o Dir::Etc::sourcelist="$1" \ -o Dir::Etc::sourceparts=/dev/null \ -o APT::Get::List-Cleanup=0 } +# stoke needs Node.js >= 22.12 (commander 15), but the distro archives of +# Debian 13 (nodejs 20.x) and Ubuntu 24.04 (nodejs 18.x) cannot satisfy +# that, which would make `apt-get install stoke` fail with an unmet +# dependency. When no configured source offers a new-enough nodejs, add the +# NodeSource repository for Node 22 so the dependency resolves. +NODE_MIN="22.12" +node_candidate_ok() { + local candidate + candidate="$(apt-cache policy nodejs 2>/dev/null | sed -n 's/^ Candidate: //p')" + [ -n "$candidate" ] && [ "$candidate" != "(none)" ] || return 1 + dpkg --compare-versions "${candidate#*:}" ge "$NODE_MIN" +} + +ensure_nodejs_source() { + node_candidate_ok && return 0 + echo "No apt source provides nodejs >= $NODE_MIN; adding NodeSource (Node 22) ..." + local ns_keyring="/etc/apt/keyrings/nodesource.asc" + local ns_list="/etc/apt/sources.list.d/nodesource.list" + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | $SUDO tee "$ns_keyring" >/dev/null + echo "deb [signed-by=$ns_keyring] https://deb.nodesource.com/node_22.x nodistro main" \ + | $SUDO tee "$ns_list" >/dev/null + update_only_source "$ns_list" + node_candidate_ok || { echo "error: still no nodejs >= $NODE_MIN available after adding NodeSource" >&2; exit 1; } +} + echo "Adding APT source for $FORGE_URL/$OWNER ..." $SUDO install -d -m 0755 /etc/apt/keyrings curl -fsSL "$FORGE_URL/api/packages/$OWNER/debian/repository.key" | $SUDO tee "$KEYRING" >/dev/null @@ -46,7 +71,7 @@ echo "deb [signed-by=$KEYRING] $FORGE_URL/api/packages/$OWNER/debian $DISTRIBUTI # in the upstream signing library). Try the properly signed source first so # this heals automatically once the forge is fixed; otherwise fall back to # [trusted=yes] — package integrity then relies on HTTPS to our own forge. -if ! update_only_this_source; then +if ! update_only_source "$LIST"; then echo echo "WARNING: signature verification failed (known Forgejo registry issue" >&2 echo "with sqv-based apt). Falling back to [trusted=yes]; transport" >&2 @@ -54,9 +79,11 @@ if ! update_only_this_source; then echo echo "deb [trusted=yes] $FORGE_URL/api/packages/$OWNER/debian $DISTRIBUTION $COMPONENT" \ | $SUDO tee "$LIST" >/dev/null - update_only_this_source + update_only_source "$LIST" fi +ensure_nodejs_source + $SUDO apt-get install -y stoke echo From 5e99006d04b53b18ff3d70bad4a9a1ede940f595 Mon Sep 17 00:00:00 2001 From: cluade-reviewer-andresmgsl Date: Wed, 22 Jul 2026 21:56:26 +0000 Subject: [PATCH 2/4] =?UTF-8?q?install-apt:=20address=20review=20=E2=80=94?= =?UTF-8?q?=20locale-safe=20parsing,=20metadata=20refresh,=20no=20list=20c?= =?UTF-8?q?lobber,=20README=20order?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Parse apt-cache policy under LC_ALL=C (Candidate: label is localized) - Refresh apt metadata (best effort) and re-check before concluding no suitable nodejs source exists - Refuse to overwrite an existing /etc/apt/sources.list.d/nodesource.list instead of silently replacing a user-managed entry - README: manual path now adds the forge source, then the Node 22 source, then runs apt-get update && install — in that order Verified on fresh debian:13: install, idempotent re-run (NodeSource not re-added), and the refusal branch with a pre-existing user list. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 15 ++++++++++----- scripts/install-apt.sh | 16 ++++++++++++++-- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index bfea833..e12a565 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ The package is published to the Debian registry of the forge itself. One-time se curl -fsSL https://forgejo.heavyduty.builders/heavy-duty/stoke/raw/branch/main/scripts/install-apt.sh | bash ``` -or manually: +or manually. First add the forge's registry as an apt source: ```bash sudo install -d /etc/apt/keyrings @@ -27,12 +27,9 @@ curl -fsSL https://forgejo.heavyduty.builders/api/packages/heavy-duty/debian/rep | sudo tee /etc/apt/keyrings/forgejo-heavy-duty.asc >/dev/null echo "deb [signed-by=/etc/apt/keyrings/forgejo-heavy-duty.asc] https://forgejo.heavyduty.builders/api/packages/heavy-duty/debian stable main" \ | sudo tee /etc/apt/sources.list.d/forgejo-heavy-duty.list -sudo apt-get update && sudo apt-get install stoke ``` -Upgrades then arrive through regular `apt-get upgrade`. - -The package depends on `nodejs (>= 22.12)`, which the distro archives of Debian 13 (Node 20) and Ubuntu 24.04 (Node 18) cannot satisfy. `install-apt.sh` handles this automatically by adding the [NodeSource](https://deb.nodesource.com) Node 22 repository when no configured apt source offers a new-enough nodejs. If you follow the manual steps instead, make sure such a source is available before `apt-get install stoke`: +The package depends on `nodejs (>= 22.12)`, which the distro archives of Debian 13 (Node 20) and Ubuntu 24.04 (Node 18) cannot satisfy — on those distros, also add a Node 22 source such as [NodeSource](https://deb.nodesource.com): ```bash curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key \ @@ -41,6 +38,14 @@ echo "deb [signed-by=/etc/apt/keyrings/nodesource.asc] https://deb.nodesource.co | sudo tee /etc/apt/sources.list.d/nodesource.list ``` +Then install: + +```bash +sudo apt-get update && sudo apt-get install stoke +``` + +Upgrades then arrive through regular `apt-get upgrade`. `install-apt.sh` performs all of the above, adding the NodeSource repository only when no already-configured apt source offers a new-enough nodejs. + Note: apt releases that verify OpenPGP with `sqv` (Debian 13+, apt >= 2.9) currently reject the signature Forgejo generates for its Debian registry (an upstream signing bug). `install-apt.sh` detects this and falls back to a `[trusted=yes]` source — integrity then relies on HTTPS to the forge. The script prefers the signed source, so setups heal automatically once the forge is fixed. As a fallback, each release also has the `.deb` attached for direct install: `sudo dpkg -i stoke__all.deb`. diff --git a/scripts/install-apt.sh b/scripts/install-apt.sh index 90ceb35..bf13ef5 100755 --- a/scripts/install-apt.sh +++ b/scripts/install-apt.sh @@ -43,16 +43,28 @@ update_only_source() { NODE_MIN="22.12" node_candidate_ok() { local candidate - candidate="$(apt-cache policy nodejs 2>/dev/null | sed -n 's/^ Candidate: //p')" + # LC_ALL=C: the "Candidate:" label is localized. + candidate="$(LC_ALL=C apt-cache policy nodejs 2>/dev/null | sed -n 's/^ Candidate: //p')" [ -n "$candidate" ] && [ "$candidate" != "(none)" ] || return 1 dpkg --compare-versions "${candidate#*:}" ge "$NODE_MIN" } ensure_nodejs_source() { node_candidate_ok && return 0 - echo "No apt source provides nodejs >= $NODE_MIN; adding NodeSource (Node 22) ..." + # The verdict may just be stale package lists — refresh (best effort, a + # transient failure of an unrelated source must not abort) and re-check + # before adding anything. + echo "No apt source seems to provide nodejs >= $NODE_MIN; refreshing apt metadata ..." + $SUDO apt-get update || true + node_candidate_ok && return 0 local ns_keyring="/etc/apt/keyrings/nodesource.asc" local ns_list="/etc/apt/sources.list.d/nodesource.list" + if [ -e "$ns_list" ]; then + echo "error: nodejs >= $NODE_MIN is unavailable and $ns_list already exists;" >&2 + echo "refusing to overwrite it. Point it at a Node >= 22 release and re-run." >&2 + exit 1 + fi + echo "Adding NodeSource (Node 22) ..." curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | $SUDO tee "$ns_keyring" >/dev/null echo "deb [signed-by=$ns_keyring] https://deb.nodesource.com/node_22.x nodistro main" \ | $SUDO tee "$ns_list" >/dev/null From 444470301c12487e54b7476316e6ec07ee7a5b76 Mon Sep 17 00:00:00 2001 From: cluade-reviewer-andresmgsl Date: Wed, 22 Jul 2026 22:01:51 +0000 Subject: [PATCH 3/4] test: stubbed shell tests for the NodeSource bootstrap logic Covers the scenarios codex-reviewer recommended: suitable candidate already present (incl. epoch stripping), missing metadata healed by a refresh, bootstrap on too-old distro nodejs, bootstrap failure, and the refuse-to-overwrite branch for a user-managed nodesource.list. Every scenario runs under a localized LC_ALL with an apt-cache stub that only emits the English Candidate: label under LC_ALL=C, so locale-safe parsing is regression-tested (mutation-checked: dropping LC_ALL=C fails 3 tests). install-apt.sh gains STOKE_APT_ETC to redirect /etc/apt to a throwaway directory under test, following the script's existing env-override pattern. Real-container flow re-verified on debian:13. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/install-apt.sh | 13 ++-- test/install-apt.test.js | 133 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 test/install-apt.test.js diff --git a/scripts/install-apt.sh b/scripts/install-apt.sh index bf13ef5..4ac6efe 100755 --- a/scripts/install-apt.sh +++ b/scripts/install-apt.sh @@ -18,9 +18,12 @@ FORGE_URL="${FORGE_URL:-https://forgejo.heavyduty.builders}" OWNER="${OWNER:-heavy-duty}" DISTRIBUTION="${DISTRIBUTION:-stable}" COMPONENT="${COMPONENT:-main}" +# Where apt configuration lives; overridable so tests can run against a +# throwaway directory instead of the real /etc/apt. +APT_ETC="${STOKE_APT_ETC:-/etc/apt}" -KEYRING="/etc/apt/keyrings/forgejo-$OWNER.asc" -LIST="/etc/apt/sources.list.d/forgejo-$OWNER.list" +KEYRING="$APT_ETC/keyrings/forgejo-$OWNER.asc" +LIST="$APT_ETC/sources.list.d/forgejo-$OWNER.list" SUDO="" if [ "$(id -u)" -ne 0 ]; then @@ -57,8 +60,8 @@ ensure_nodejs_source() { echo "No apt source seems to provide nodejs >= $NODE_MIN; refreshing apt metadata ..." $SUDO apt-get update || true node_candidate_ok && return 0 - local ns_keyring="/etc/apt/keyrings/nodesource.asc" - local ns_list="/etc/apt/sources.list.d/nodesource.list" + local ns_keyring="$APT_ETC/keyrings/nodesource.asc" + local ns_list="$APT_ETC/sources.list.d/nodesource.list" if [ -e "$ns_list" ]; then echo "error: nodejs >= $NODE_MIN is unavailable and $ns_list already exists;" >&2 echo "refusing to overwrite it. Point it at a Node >= 22 release and re-run." >&2 @@ -73,7 +76,7 @@ ensure_nodejs_source() { } echo "Adding APT source for $FORGE_URL/$OWNER ..." -$SUDO install -d -m 0755 /etc/apt/keyrings +$SUDO install -d -m 0755 "$APT_ETC/keyrings" curl -fsSL "$FORGE_URL/api/packages/$OWNER/debian/repository.key" | $SUDO tee "$KEYRING" >/dev/null echo "deb [signed-by=$KEYRING] $FORGE_URL/api/packages/$OWNER/debian $DISTRIBUTION $COMPONENT" \ | $SUDO tee "$LIST" >/dev/null diff --git a/test/install-apt.test.js b/test/install-apt.test.js new file mode 100644 index 0000000..c78b7dc --- /dev/null +++ b/test/install-apt.test.js @@ -0,0 +1,133 @@ +const { test } = require('node:test'); +const assert = require('node:assert/strict'); +const { spawnSync } = require('node:child_process'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +const SCRIPT = path.join(__dirname, '..', 'scripts', 'install-apt.sh'); + +// Runs install-apt.sh against a throwaway apt directory (STOKE_APT_ETC) with +// every external command stubbed via PATH. Scenario knobs: +// candInitial `apt-cache policy` Candidate before any update +// candAfterUpdate Candidate after any `apt-get update` +// candAfterNodesource Candidate after an update once nodesource.list exists +// The apt-cache stub localizes the "Candidate:" label unless LC_ALL=C is set, +// so every scenario doubles as a regression test for locale-safe parsing. +function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexistingNodesourceList }) { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-apt-test-')); + const bin = path.join(root, 'bin'); + const state = path.join(root, 'state'); + const aptEtc = path.join(root, 'etc', 'apt'); + fs.mkdirSync(bin, { recursive: true }); + fs.mkdirSync(state, { recursive: true }); + fs.mkdirSync(path.join(aptEtc, 'sources.list.d'), { recursive: true }); + fs.writeFileSync(path.join(state, 'candidate'), candInitial); + if (preexistingNodesourceList !== undefined) { + fs.writeFileSync(path.join(aptEtc, 'sources.list.d', 'nodesource.list'), preexistingNodesourceList); + } + + const stub = (name, body) => { + const p = path.join(bin, name); + fs.writeFileSync(p, `#!/usr/bin/env bash\n${body}\n`, { mode: 0o755 }); + }; + + // Force the non-root path so every mutation goes through the sudo stub. + stub('id', 'echo 1000'); + stub('sudo', 'exec "$@"'); + stub('curl', 'echo "FAKE-KEY"'); + stub('stoke', 'echo 1.2.0'); + stub('apt-cache', [ + 'cand="$(cat "$STATE_DIR/candidate")"', + '[ "$cand" = "absent" ] && exit 0', + 'label="Candidato"', + '[ "${LC_ALL:-}" = "C" ] && label="Candidate"', + 'printf "nodejs:\\n Installed: (none)\\n %s: %s\\n" "$label" "$cand"', + ].join('\n')); + stub('apt-get', [ + 'echo "apt-get $*" >> "$STATE_DIR/apt-get.log"', + 'for a in "$@"; do', + ' if [ "$a" = update ]; then', + ' if [ -e "$STOKE_APT_ETC/sources.list.d/nodesource.list" ] && [ -n "${CAND_AFTER_NODESOURCE:-}" ]; then', + ' echo "$CAND_AFTER_NODESOURCE" > "$STATE_DIR/candidate"', + ' elif [ -n "${CAND_AFTER_UPDATE:-}" ]; then', + ' echo "$CAND_AFTER_UPDATE" > "$STATE_DIR/candidate"', + ' fi', + ' fi', + 'done', + 'exit 0', + ].join('\n')); + + const res = spawnSync('bash', [SCRIPT], { + encoding: 'utf8', + env: { + ...process.env, + PATH: `${bin}:${process.env.PATH}`, + STOKE_APT_ETC: aptEtc, + STATE_DIR: state, + CAND_AFTER_UPDATE: candAfterUpdate || '', + CAND_AFTER_NODESOURCE: candAfterNodesource || '', + LC_ALL: 'es_ES.UTF-8', // localized environment; the script must force C + }, + }); + + const read = (p) => (fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : null); + return { + res, + aptEtc, + nodesourceList: read(path.join(aptEtc, 'sources.list.d', 'nodesource.list')), + nodesourceKey: read(path.join(aptEtc, 'keyrings', 'nodesource.asc')), + aptGetLog: read(path.join(state, 'apt-get.log')) || '', + }; +} + +test('suitable nodejs candidate already available: installs without touching NodeSource', () => { + // Epoch-prefixed version also covers the epoch-stripping in the comparison. + const s = runScenario({ candInitial: '1:22.23.1-1nodesource1' }); + assert.equal(s.res.status, 0, s.res.stderr); + assert.equal(s.nodesourceList, null); + assert.match(s.aptGetLog, /install -y stoke/); +}); + +test('no cached metadata: refreshes apt lists before deciding, no NodeSource needed', () => { + const s = runScenario({ candInitial: 'absent', candAfterUpdate: '22.23.1-1nodesource1' }); + assert.equal(s.res.status, 0, s.res.stderr); + assert.equal(s.nodesourceList, null); + assert.match(s.aptGetLog, /install -y stoke/); +}); + +test('distro nodejs too old: bootstraps NodeSource and installs', () => { + const s = runScenario({ + candInitial: '20.19.2+dfsg-1+deb13u2', + candAfterUpdate: '20.19.2+dfsg-1+deb13u2', + candAfterNodesource: '22.23.1-1nodesource1', + }); + assert.equal(s.res.status, 0, s.res.stderr); + assert.match(s.nodesourceList, /deb \[signed-by=.*nodesource\.asc\] https:\/\/deb\.nodesource\.com\/node_22\.x nodistro main/); + assert.equal(s.nodesourceKey, 'FAKE-KEY\n'); + assert.match(s.aptGetLog, /install -y stoke/); +}); + +test('bootstrap failure: NodeSource still lacks a suitable nodejs, exits with error', () => { + const s = runScenario({ + candInitial: '20.19.2+dfsg-1+deb13u2', + candAfterUpdate: '20.19.2+dfsg-1+deb13u2', + candAfterNodesource: '20.19.2+dfsg-1+deb13u2', + }); + assert.notEqual(s.res.status, 0); + assert.match(s.res.stderr, /still no nodejs >= 22\.12/); + assert.doesNotMatch(s.aptGetLog, /install -y stoke/); +}); + +test('pre-existing user-managed nodesource.list is never overwritten', () => { + const marker = '# user-managed entry\n'; + const s = runScenario({ + candInitial: '18.19.1+dfsg-6ubuntu5', + candAfterUpdate: '18.19.1+dfsg-6ubuntu5', + preexistingNodesourceList: marker, + }); + assert.notEqual(s.res.status, 0); + assert.match(s.res.stderr, /refusing to overwrite/); + assert.equal(s.nodesourceList, marker); + assert.doesNotMatch(s.aptGetLog, /install -y stoke/); +}); From 0ecd935528a4eda1947d237c4dc965c38adc35cd Mon Sep 17 00:00:00 2001 From: cluade-reviewer-andresmgsl Date: Wed, 22 Jul 2026 23:17:20 +0000 Subject: [PATCH 4/4] =?UTF-8?q?install-apt:=20address=20review=20nits=20?= =?UTF-8?q?=E2=80=94=20apt-readable=20file=20modes,=20test=20cleanup,=20cl?= =?UTF-8?q?earer=20refusal=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - chmod 0644 every keyring and sources.list entry after writing: tee inherits the caller's umask, and under e.g. umask 077 apt's unprivileged _apt user could not read the keyring - Tests now run the script under umask 077 and assert the 0644 modes (mutation-checked: dropping the chmod fails a test), and remove their temp directories on exit - Refusal error now states that metadata was already refreshed before concluding the existing nodesource.list is unsuitable Verified on fresh debian:13 under umask 077: all four files 0644, install succeeds. npm test 32/32. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/install-apt.sh | 12 ++++++++++-- test/install-apt.test.js | 15 ++++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/scripts/install-apt.sh b/scripts/install-apt.sh index 4ac6efe..9e68c5d 100755 --- a/scripts/install-apt.sh +++ b/scripts/install-apt.sh @@ -63,14 +63,18 @@ ensure_nodejs_source() { local ns_keyring="$APT_ETC/keyrings/nodesource.asc" local ns_list="$APT_ETC/sources.list.d/nodesource.list" if [ -e "$ns_list" ]; then - echo "error: nodejs >= $NODE_MIN is unavailable and $ns_list already exists;" >&2 - echo "refusing to overwrite it. Point it at a Node >= 22 release and re-run." >&2 + echo "error: even after refreshing apt metadata, no source provides nodejs >= $NODE_MIN," >&2 + echo "and $ns_list already exists; refusing to overwrite it." >&2 + echo "Point it at a Node >= 22 release (e.g. node_22.x) and re-run." >&2 exit 1 fi echo "Adding NodeSource (Node 22) ..." curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | $SUDO tee "$ns_keyring" >/dev/null echo "deb [signed-by=$ns_keyring] https://deb.nodesource.com/node_22.x nodistro main" \ | $SUDO tee "$ns_list" >/dev/null + # tee inherits our umask; apt's unprivileged _apt user must be able to + # read these. + $SUDO chmod 0644 "$ns_keyring" "$ns_list" update_only_source "$ns_list" node_candidate_ok || { echo "error: still no nodejs >= $NODE_MIN available after adding NodeSource" >&2; exit 1; } } @@ -80,6 +84,9 @@ $SUDO install -d -m 0755 "$APT_ETC/keyrings" curl -fsSL "$FORGE_URL/api/packages/$OWNER/debian/repository.key" | $SUDO tee "$KEYRING" >/dev/null echo "deb [signed-by=$KEYRING] $FORGE_URL/api/packages/$OWNER/debian $DISTRIBUTION $COMPONENT" \ | $SUDO tee "$LIST" >/dev/null +# tee inherits our umask; apt's unprivileged _apt user must be able to +# read these. +$SUDO chmod 0644 "$KEYRING" "$LIST" # Newer apt verifies with sqv (Sequoia), which rejects the signature Forgejo # currently produces for its Debian registry (malformed Ed25519 MPI encoding @@ -94,6 +101,7 @@ if ! update_only_source "$LIST"; then echo echo "deb [trusted=yes] $FORGE_URL/api/packages/$OWNER/debian $DISTRIBUTION $COMPONENT" \ | $SUDO tee "$LIST" >/dev/null + $SUDO chmod 0644 "$LIST" update_only_source "$LIST" fi diff --git a/test/install-apt.test.js b/test/install-apt.test.js index c78b7dc..51dd362 100644 --- a/test/install-apt.test.js +++ b/test/install-apt.test.js @@ -14,8 +14,12 @@ const SCRIPT = path.join(__dirname, '..', 'scripts', 'install-apt.sh'); // candAfterNodesource Candidate after an update once nodesource.list exists // The apt-cache stub localizes the "Candidate:" label unless LC_ALL=C is set, // so every scenario doubles as a regression test for locale-safe parsing. +const cleanups = []; +process.on('exit', () => { for (const dir of cleanups) fs.rmSync(dir, { recursive: true, force: true }); }); + function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexistingNodesourceList }) { const root = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-apt-test-')); + cleanups.push(root); const bin = path.join(root, 'bin'); const state = path.join(root, 'state'); const aptEtc = path.join(root, 'etc', 'apt'); @@ -58,7 +62,9 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi 'exit 0', ].join('\n')); - const res = spawnSync('bash', [SCRIPT], { + // Restrictive umask: apt-readable 0644 files must come from the script's + // explicit chmod, not from a lucky default. + const res = spawnSync('bash', ['-c', 'umask 077 && exec bash "$1"', 'bash', SCRIPT], { encoding: 'utf8', env: { ...process.env, @@ -72,11 +78,15 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi }); const read = (p) => (fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : null); + const mode = (p) => (fs.existsSync(p) ? fs.statSync(p).mode & 0o777 : null); return { res, aptEtc, nodesourceList: read(path.join(aptEtc, 'sources.list.d', 'nodesource.list')), + nodesourceListMode: mode(path.join(aptEtc, 'sources.list.d', 'nodesource.list')), nodesourceKey: read(path.join(aptEtc, 'keyrings', 'nodesource.asc')), + nodesourceKeyMode: mode(path.join(aptEtc, 'keyrings', 'nodesource.asc')), + forgeKeyMode: mode(path.join(aptEtc, 'keyrings', 'forgejo-heavy-duty.asc')), aptGetLog: read(path.join(state, 'apt-get.log')) || '', }; } @@ -87,6 +97,7 @@ test('suitable nodejs candidate already available: installs without touching Nod assert.equal(s.res.status, 0, s.res.stderr); assert.equal(s.nodesourceList, null); assert.match(s.aptGetLog, /install -y stoke/); + assert.equal(s.forgeKeyMode, 0o644, 'forge keyring must be readable by _apt'); }); test('no cached metadata: refreshes apt lists before deciding, no NodeSource needed', () => { @@ -105,6 +116,8 @@ test('distro nodejs too old: bootstraps NodeSource and installs', () => { assert.equal(s.res.status, 0, s.res.stderr); assert.match(s.nodesourceList, /deb \[signed-by=.*nodesource\.asc\] https:\/\/deb\.nodesource\.com\/node_22\.x nodistro main/); assert.equal(s.nodesourceKey, 'FAKE-KEY\n'); + assert.equal(s.nodesourceKeyMode, 0o644, 'NodeSource keyring must be readable by _apt'); + assert.equal(s.nodesourceListMode, 0o644, 'NodeSource list must be readable by _apt'); assert.match(s.aptGetLog, /install -y stoke/); });