diff --git a/changelog.d/57.md b/changelog.d/57.md new file mode 100644 index 0000000..3f6d6d0 --- /dev/null +++ b/changelog.d/57.md @@ -0,0 +1 @@ +- Clarified Debian publish authentication failures with the CI secret source and the local remedies. (#57). diff --git a/scripts/publish-deb.sh b/scripts/publish-deb.sh index 1e7e3a6..f3b7e66 100755 --- a/scripts/publish-deb.sh +++ b/scripts/publish-deb.sh @@ -9,7 +9,7 @@ # component APT component, default: main # # Authentication (first match wins): -# 1. STOKE_TOKEN environment variable +# 1. STOKE_TOKEN environment variable (set from secrets.RELEASE_TOKEN in CI) # 2. The token stored by `stoke auth login` # # The Forgejo URL defaults to the instance in the stoke config, falling back @@ -30,7 +30,15 @@ CONFIG_JSON="$(node -e "const c = require('$ROOT/src/config').loadConfig(); if ( TOKEN="${STOKE_TOKEN:-$(node -pe "(JSON.parse(process.argv[1] || '{}').token) || ''" "$CONFIG_JSON")}" FORGE_URL="${FORGE_URL:-$(node -pe "(JSON.parse(process.argv[1] || '{}').url) || 'https://forgejo.heavyduty.builders'" "$CONFIG_JSON")}" -[ -n "$TOKEN" ] || { echo "error: no token. Set STOKE_TOKEN or run: stoke auth login" >&2; exit 1; } +if [ -z "$TOKEN" ]; then + cat >&2 <<'EOF' +error: no token. + In CI, this step reads STOKE_TOKEN from secrets.RELEASE_TOKEN; an empty value + means the secret is unset or unreadable by this workflow, not that the tool is missing. + Locally: export STOKE_TOKEN, or run `stoke auth login`. +EOF + exit 1 +fi URL="$FORGE_URL/api/packages/$OWNER/debian/pool/$DISTRIBUTION/$COMPONENT/upload" echo "Uploading $(basename "$DEB") to $URL" diff --git a/test/publish-deb.test.js b/test/publish-deb.test.js new file mode 100644 index 0000000..04f84f2 --- /dev/null +++ b/test/publish-deb.test.js @@ -0,0 +1,57 @@ +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 ROOT = path.join(__dirname, '..'); +const SCRIPT = path.join(ROOT, 'scripts', 'publish-deb.sh'); +const TOKEN = 'deb-token-that-must-not-appear-in-output'; + +function runScenario({ token = '' } = {}) { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'stoke-publish-deb-test-')); + try { + const home = path.join(dir, 'home'); + const bin = path.join(dir, 'bin'); + const deb = path.join(dir, 'stoke_2.0.0_all.deb'); + fs.mkdirSync(home); + fs.mkdirSync(bin); + fs.writeFileSync(deb, 'package'); + fs.writeFileSync(path.join(bin, 'curl'), '#!/usr/bin/env bash\nprintf 201\n'); + fs.chmodSync(path.join(bin, 'curl'), 0o755); + + return spawnSync('bash', [SCRIPT, deb], { + encoding: 'utf8', + env: { + HOME: home, + PATH: `${bin}:${process.env.PATH}`, + STOKE_CONFIG_FILE: path.join(dir, 'missing-config.json'), + STOKE_TOKEN: token, + }, + }); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +test('empty token identifies the CI secret before offering the local remedy', () => { + const result = runScenario(); + + assert.equal(result.status, 1); + assert.equal(result.stdout, ''); + assert.match(result.stderr, /^error: no token\./); + assert.match(result.stderr, /STOKE_TOKEN/); + assert.match(result.stderr, /RELEASE_TOKEN/); + assert.match(result.stderr, /empty value.*secret/is); + assert.ok(result.stderr.indexOf('RELEASE_TOKEN') < result.stderr.indexOf('stoke auth login')); +}); + +test('non-empty environment token passes the guard without exposing the token', () => { + const result = runScenario({ token: TOKEN }); + + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /Published\./); + assert.doesNotMatch(result.stdout, new RegExp(TOKEN)); + assert.doesNotMatch(result.stderr, new RegExp(TOKEN)); +});