ci(nightly): surface a clear error when the Gitea API rejects the audit token
All checks were successful
CI / Unit & Component Tests (push) Successful in 3m51s
CI / OCR Service Tests (push) Successful in 23s
CI / Unit & Component Tests (pull_request) Successful in 4m43s
CI / Backend Unit Tests (push) Successful in 5m6s
CI / fail2ban Regex (push) Successful in 48s
CI / OCR Service Tests (pull_request) Successful in 23s
CI / Backend Unit Tests (pull_request) Successful in 5m10s
CI / Semgrep Security Scan (push) Successful in 23s
CI / fail2ban Regex (pull_request) Successful in 47s
CI / Compose Bucket Idempotency (push) Successful in 1m5s
CI / Semgrep Security Scan (pull_request) Successful in 23s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m6s
SDD Gate / RTM Check (pull_request) Successful in 13s
SDD Gate / Contract Validate (pull_request) Successful in 24s
SDD Gate / Constitution Impact (pull_request) Successful in 17s

The npm-audit job filed its tracking issue via `curl -sf`, which collapses
every HTTP >=400 into a bare "exit 22". When the NIGHTLY_AUDIT_TOKEN secret is
missing, expired, or under-scoped, the step failed with an opaque
`exitcode '22'` and no hint at the cause (run #6707).

Route all five API calls through an `api()` helper that reads the HTTP status
and, on >=400, emits an actionable `::error::` naming the status and the token
secret before failing — without ever echoing the token value. Extend the
in-workflow self-test (mocked curl) to cover both the success and HTTP-error
paths.

Closes #839
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit was merged in pull request #840.
This commit is contained in:
Marcel
2026-06-14 19:29:49 +02:00
parent 1cd6ffd5ca
commit 6dae4fe428

View File

@@ -192,17 +192,52 @@ jobs:
REPO="${{ github.repository }}" REPO="${{ github.repository }}"
RUN_URL="${GITEA_URL}/${REPO}/actions/runs/${{ github.run_id }}" RUN_URL="${GITEA_URL}/${REPO}/actions/runs/${{ github.run_id }}"
# --- Gitea API helper ---
# api METHOD URL [extra curl args...] — authenticated Gitea API call.
# `curl -sf` collapses every HTTP >=400 into a bare "exit 22", which
# surfaces as an opaque step failure (issue #839). Instead we read the
# status code and, on a >=400 response, print an actionable ::error::
# to stderr (so a calling command substitution does not swallow it) and
# return 1 — `set -e` then still fails the step. The token is never
# echoed (no set -x; never placed in the message).
api() {
local method="$1" url="$2"; shift 2
local resp http
resp=$(curl -s -w '\n%{http_code}' -X "$method" \
-H "Authorization: token $NIGHTLY_AUDIT_TOKEN" "$@" -- "$url")
http=${resp##*$'\n'}
printf '%s' "${resp%$'\n'*}"
case "$http" in
2*|3*) return 0 ;;
401|403)
echo "::error::Gitea returned HTTP $http for $method ${url%%\?*} — the NIGHTLY_AUDIT_TOKEN secret is missing, expired, or lacks issue read+write scope; recreate the renovate_bot PAT and update the secret." >&2
return 1 ;;
*)
echo "::error::Gitea returned HTTP ${http:-(none)} for $method ${url%%\?*}." >&2
return 1 ;;
esac
}
# --- Self-test (mirrors ci.yml §Assert pattern) --- # --- Self-test (mirrors ci.yml §Assert pattern) ---
# Tests the exact jq test() call used in the dedupe step, before any # Runs before any real API call so broken logic fails loudly early:
# API call, so a broken matcher fails loudly early rather than silently # (a) the jq title matcher used by the dedupe step — proves the regex
# opening duplicate issues. Proves the regex only — create-vs-update # only; the create-vs-update decision is exercised by the
# decision is exercised by the workflow_dispatch AC. # workflow_dispatch AC;
# (b) the api helper's HTTP-status handling, driven by a mocked curl so
# it needs no network — proves a 2xx returns the body and a >=400
# fails with an ::error:: instead of an opaque exit 22.
echo "{\"title\": \"${MARKER}\"}" \ echo "{\"title\": \"${MARKER}\"}" \
| jq -e --arg m "$MARKER" '.title | test($m; "i")' > /dev/null \ | jq -e --arg m "$MARKER" '.title | test($m; "i")' > /dev/null \
|| { echo "FAIL: self-test — jq test() missed tracking issue title"; exit 1; } || { echo "FAIL: self-test — jq test() missed tracking issue title"; exit 1; }
echo '{"title": "fix(deps): update dependency esbuild (CVE-2025-12345)"}' \ echo '{"title": "fix(deps): update dependency esbuild (CVE-2025-12345)"}' \
| jq -e --arg m "$MARKER" '.title | test($m; "i") | not' > /dev/null \ | jq -e --arg m "$MARKER" '.title | test($m; "i") | not' > /dev/null \
|| { echo "FAIL: self-test — jq test() incorrectly matched unrelated title"; exit 1; } || { echo "FAIL: self-test — jq test() incorrectly matched unrelated title"; exit 1; }
( curl() { printf 'OK\n200'; }; [ "$(api GET selftest)" = "OK" ] ) \
|| { echo "FAIL: self-test — api helper dropped body on HTTP 200"; exit 1; }
( curl() { printf 'nope\n401'; }
if api GET selftest >/dev/null 2>/tmp/api_selftest_err; then exit 1; fi
grep -q '::error::' /tmp/api_selftest_err ) \
|| { echo "FAIL: self-test — api helper did not emit ::error:: on HTTP 401"; exit 1; }
echo "Self-test passed." echo "Self-test passed."
# --- Run audit --- # --- Run audit ---
@@ -237,8 +272,7 @@ jobs:
# Renovate vuln PRs also carry the "security" label, so >1 open # Renovate vuln PRs also carry the "security" label, so >1 open
# "security" issue WILL occur. Title-match (not just label) ensures # "security" issue WILL occur. Title-match (not just label) ensures
# we deduplicate only our own tracking issue. # we deduplicate only our own tracking issue.
OPEN_ISSUES=$(curl -sf \ OPEN_ISSUES=$(api GET \
-H "Authorization: token $NIGHTLY_AUDIT_TOKEN" \
"${GITEA_URL}/api/v1/repos/${REPO}/issues?state=open&type=issues&labels=security&limit=50") "${GITEA_URL}/api/v1/repos/${REPO}/issues?state=open&type=issues&labels=security&limit=50")
MATCHED=$(echo "$OPEN_ISSUES" | jq \ MATCHED=$(echo "$OPEN_ISSUES" | jq \
@@ -255,11 +289,10 @@ jobs:
--arg run_url "$RUN_URL" \ --arg run_url "$RUN_URL" \
'$existing + "\n\n---\n\nUpdated by run: " + $run_url') '$existing + "\n\n---\n\nUpdated by run: " + $run_url')
PAYLOAD=$(jq -n --arg body "$NEW_BODY" '{"body": $body}') PAYLOAD=$(jq -n --arg body "$NEW_BODY" '{"body": $body}')
curl -sf -X PATCH \ api PATCH \
-H "Authorization: token $NIGHTLY_AUDIT_TOKEN" \ "${GITEA_URL}/api/v1/repos/${REPO}/issues/${ISSUE_NUMBER}" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "$PAYLOAD" \ -d "$PAYLOAD" > /dev/null
"${GITEA_URL}/api/v1/repos/${REPO}/issues/${ISSUE_NUMBER}" > /dev/null
echo "Updated tracking issue #${ISSUE_NUMBER}" echo "Updated tracking issue #${ISSUE_NUMBER}"
else else
# Closed prior issue that recurs → new issue (not reopened). # Closed prior issue that recurs → new issue (not reopened).
@@ -268,24 +301,21 @@ jobs:
--arg title "$MARKER" \ --arg title "$MARKER" \
--arg body "$ISSUE_BODY" \ --arg body "$ISSUE_BODY" \
'{"title": $title, "body": $body}') '{"title": $title, "body": $body}')
CREATED=$(curl -sf -X POST \ CREATED=$(api POST \
-H "Authorization: token $NIGHTLY_AUDIT_TOKEN" \ "${GITEA_URL}/api/v1/repos/${REPO}/issues" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "$PAYLOAD" \ -d "$PAYLOAD")
"${GITEA_URL}/api/v1/repos/${REPO}/issues")
NEW_NUMBER=$(echo "$CREATED" | jq -r '.number') NEW_NUMBER=$(echo "$CREATED" | jq -r '.number')
echo "Opened new tracking issue #${NEW_NUMBER}" echo "Opened new tracking issue #${NEW_NUMBER}"
# Labels are ignored on issue create in Gitea — add in a follow-up call. # Labels are ignored on issue create in Gitea — add in a follow-up call.
LABEL_IDS=$(curl -sf \ LABEL_IDS=$(api GET \
-H "Authorization: token $NIGHTLY_AUDIT_TOKEN" \
"${GITEA_URL}/api/v1/repos/${REPO}/labels?limit=50" \ "${GITEA_URL}/api/v1/repos/${REPO}/labels?limit=50" \
| jq '[.[] | select(.name == "security" or .name == "devops" or .name == "P1-high") | .id]') | jq '[.[] | select(.name == "security" or .name == "devops" or .name == "P1-high") | .id]')
curl -sf -X POST \ api POST \
-H "Authorization: token $NIGHTLY_AUDIT_TOKEN" \ "${GITEA_URL}/api/v1/repos/${REPO}/issues/${NEW_NUMBER}/labels" \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-d "{\"labels\": $LABEL_IDS}" \ -d "{\"labels\": $LABEL_IDS}" > /dev/null
"${GITEA_URL}/api/v1/repos/${REPO}/issues/${NEW_NUMBER}/labels" > /dev/null
fi fi
exit "$AUDIT_EXIT" exit "$AUDIT_EXIT"