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/3] 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/3] =?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/3] 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/); +});