fix: address apt install review round
All checks were successful
labels / labels (pull_request) Successful in 10s
ci / test (pull_request) Successful in 14s

This commit is contained in:
codex-bot-andresmgsl 2026-08-31 15:14:12 +00:00
parent 9cc9576116
commit 2efc76f23e
3 changed files with 61 additions and 20 deletions

View file

@ -13,10 +13,15 @@ A command-line interface for [Forgejo](https://forgejo.org/), built with [Comman
### With apt (Debian/Ubuntu — recommended)
The package is published to the Debian registry of the forge itself. The
`heavy-duty` organization is private, so installation requires a Forgejo user
that belongs to the organization and a personal access token that can read its
packages. One-time setup:
The package is published to the public Debian registry of the forge itself.
One-time setup:
```bash
curl -fsSL https://forgejo.heavyduty.builders/heavy-duty/stoke/raw/branch/main/scripts/install-apt.sh | bash
```
If a private registry or a `FORGE_URL=` override requires authentication,
download the installer and supply a Forgejo login and package-readable token:
```bash
export FORGE_USER=your-forgejo-login
@ -27,15 +32,16 @@ sudo --preserve-env=FORGE_USER,FORGE_TOKEN bash /tmp/stoke-install-apt.sh
unset FORGE_TOKEN
```
The installer keeps the credentials out of the source URL in a root-readable
apt auth file. To configure that file manually before adding the source:
The authenticated path keeps credentials out of the source URL in a
root-readable apt auth file. To configure that file manually before adding the
source:
```bash
sudo install -d -m 0755 /etc/apt/auth.conf.d
sudo install -m 0600 /dev/null /etc/apt/auth.conf.d/forgejo-heavy-duty.conf
printf 'machine forgejo.heavyduty.builders\nlogin %s\npassword %s\n' \
"$FORGE_USER" "$FORGE_TOKEN" \
| sudo tee /etc/apt/auth.conf.d/forgejo-heavy-duty.conf >/dev/null
sudo chmod 0600 /etc/apt/auth.conf.d/forgejo-heavy-duty.conf
```
Then add the forge's registry as an apt source:
@ -77,11 +83,13 @@ run:
```bash
export STOKE_ALLOW_UNVERIFIED_APT=1
sudo --preserve-env=FORGE_USER,FORGE_TOKEN,STOKE_ALLOW_UNVERIFIED_APT \
bash /tmp/stoke-install-apt.sh
curl -fsSL https://forgejo.heavyduty.builders/heavy-duty/stoke/raw/branch/main/scripts/install-apt.sh | bash
unset STOKE_ALLOW_UNVERIFIED_APT
```
For a private registry, re-run the downloaded installer with
`sudo --preserve-env=FORGE_USER,FORGE_TOKEN,STOKE_ALLOW_UNVERIFIED_APT` instead.
This exact opt-in is the only path in the installer that writes a
`[trusted=yes]` source. The installer prints the security trade-off again when
it takes that path.

View file

@ -52,10 +52,10 @@ if [ -n "$FORGE_USER" ] && [ -n "$FORGE_TOKEN" ]; then
forge_host="${FORGE_URL#*://}"
forge_host="${forge_host%%/*}"
$SUDO install -d -m 0755 "$APT_ETC/auth.conf.d"
$SUDO install -m 0600 /dev/null "$AUTH"
printf 'machine %s\nlogin %s\npassword %s\n' \
"$forge_host" "$FORGE_USER" "$FORGE_TOKEN" \
| $SUDO tee "$AUTH" >/dev/null
$SUDO chmod 0600 "$AUTH"
CURL_AUTH=(--netrc-file "$AUTH")
fi
@ -108,7 +108,7 @@ ensure_nodejs_source() {
}
echo "Adding APT source for $FORGE_URL/$OWNER ..."
$SUDO install -d -m 0755 "$APT_ETC/keyrings"
$SUDO install -d -m 0755 "$APT_ETC/keyrings" "$APT_ETC/sources.list.d"
$SUDO curl "${CURL_AUTH[@]}" -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
@ -134,19 +134,13 @@ fi
# is not the key algorithm, because the registry serves an RSA-2048 signing
# key (`gpg --list-packets` on repository.key reports `algo 1` with a
# 2048-bit pkey[0]). Try the properly signed source first so this heals
# automatically once the forge is fixed. Only that signature-error class, plus
# automatically once the forge is fixed. Only the exact live sqv failure, plus
# the user's exact opt-in, permits an unverified source; auth, network, and
# other failures must leave verification enabled and retain apt's diagnostic.
if update_output="$(update_only_source "$LIST" 2>&1)"; then
printf '%s\n' "$update_output"
else
update_status=$?
if ! grep -Eiq \
'NO_PUBKEY|EXPKEYSIG|BADSIG|signatures? (could not|couldn.t) be verified|signature (verification )?(failed|failure|error|invalid)|repository .*not signed|is not signed' \
<<<"$update_output"; then
printf '%s\n' "$update_output" >&2
exit "$update_status"
fi
if ! grep -Fqi '/usr/bin/sqv' <<<"$update_output" \
|| ! grep -Fqi 'Malformed MPI' <<<"$update_output"; then
printf '%s\n' "$update_output" >&2

View file

@ -16,12 +16,13 @@ const SCRIPT = path.join(__dirname, '..', 'scripts', 'install-apt.sh');
// sourceUpdateError stderr and exit 100 for the first signed stoke update
// forgeUser/token private-registry credentials
// allowUnverified explicit HTTPS-only integrity opt-in
// precreateSourcesDir whether the throwaway apt root already has sources.list.d
// 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, releaseStatus, sourceUpdateError, forgeUser, forgeToken, allowUnverified }) {
function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexistingNodesourceList, releaseStatus, sourceUpdateError, forgeUser, forgeToken, allowUnverified, precreateSourcesDir = true }) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-apt-test-'));
cleanups.push(root);
const bin = path.join(root, 'bin');
@ -29,7 +30,7 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi
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 });
if (precreateSourcesDir) 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);
@ -43,6 +44,20 @@ function runScenario({ candInitial, candAfterUpdate, candAfterNodesource, preexi
// Force the non-root path so every mutation goes through the sudo stub.
stub('id', 'echo 1000');
stub('sudo', 'SUDO_ACTIVE=1 exec "$@"');
stub('tee', [
'for destination in "$@"; do',
' case "$destination" in',
' */auth.conf.d/forgejo-*.conf)',
' mode="$(stat -c %a "$destination" 2>/dev/null || true)"',
' if [ "$mode" != 600 ]; then',
' echo "credential destination was not mode 0600 before write" >&2',
' exit 78',
' fi',
' ;;',
' esac',
'done',
'exec /usr/bin/tee "$@"',
].join('\n'));
// Registry Release-file probes (URLs under /dists/) answer with the
// scenario's HTTP status; everything else is a key fetch.
stub('curl', [
@ -196,6 +211,16 @@ test('registry Release file present: proceeds with the install', () => {
assert.match(s.aptGetLog, /install -y stoke/);
});
test('fresh apt root creates sources.list.d before writing the forge source', () => {
const s = runScenario({
candInitial: '22.23.1-1nodesource1',
precreateSourcesDir: false,
});
assert.equal(s.res.status, 0, s.res.stderr);
assert.match(s.forgeList, /\[signed-by=/);
assert.match(s.aptGetLog, /install -y stoke/);
});
test('signature verification failure refuses by default and removes the forge source', () => {
const s = runScenario({
candInitial: '22.23.1-1nodesource1',
@ -235,6 +260,20 @@ test('opt-in cannot bypass a missing signing key', () => {
assert.doesNotMatch(s.aptGetLog, /install -y stoke/);
});
test('opt-in cannot bypass sqv output without the known Malformed MPI failure', () => {
const failure = 'W: OpenPGP signature verification failed: Sub-process /usr/bin/sqv returned an error code (1): unexpected packet';
const s = runScenario({
candInitial: '22.23.1-1nodesource1',
sourceUpdateError: failure,
allowUnverified: '1',
});
assert.notEqual(s.res.status, 0);
assert.match(s.res.stderr, new RegExp(failure.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
assert.match(s.forgeList, /\[signed-by=/);
assert.doesNotMatch(s.forgeList, /trusted=yes/);
assert.doesNotMatch(s.aptGetLog, /install -y stoke/);
});
test('unrecognized opt-in value is rejected before configuring apt', () => {
const s = runScenario({
candInitial: '22.23.1-1nodesource1',