fix(ci): use Bash array for curl --resolve to fix smoke tests #616
Reference in New Issue
Block a user
Delete Branch "fix/ci-smoke-curl-resolve-quoting"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
RESOLVEwas a plain string and expanded as"$RESOLVE"(double-quoted), so curl received--resolve host:443:ipas one argument instead of two — the embedded space made the entire string an unknown option name (exit 2).RESOLVEis now a Bash array (RESOLVE=(--resolve "$HOST:443:$HOST_IP"));"${RESOLVE[@]}"always expands to exactly two separate shell words.ip route(requires iproute2) to/proc/net/route— consistent with nightly.yml, no extra package needed in the job container.Test Plan
workflow_dispatch) and verify smoke test step passesv*tag pushQuoting RESOLVE as a string and expanding with "$RESOLVE" passes the flag and its value as a single token to curl; curl rejects the whole string as an unknown option (exit 2). Switching to a Bash array and "${RESOLVE[@]}" ensures the two words are always passed as separate arguments regardless of quoting context. Also aligns release.yml gateway detection with nightly.yml: replaces `ip route` (requires iproute2) with /proc/net/route (always available in the job container, no extra package needed). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>👨💻 Felix Brandt — Senior Fullstack Developer
Verdict: ✅ Approved
This is a clean, targeted fix with no application code involved. Reviewing from a correctness and readability standpoint.
What's correct
Bash array semantics are right.
RESOLVE=(--resolve "$HOST:443:$HOST_IP")followed by"${RESOLVE[@]}"is the canonical way to pass a multi-word option as a single logical unit in Bash. The old quoted-string approach ("$RESOLVE") caused curl to receive--resolve host:443:ipas one token — the embedded space made curl treat it as an unknown option name (exit 2). This is a real, non-obvious Bash pitfall that's easy to miss in review.Comments explain the WHY, not the WHAT. The updated comment block in
release.yml:This is exactly the right level of comment — a non-obvious constraint that a future developer could accidentally revert. Well done.
set -eis in place — failures propagate correctly. No silent error swallowing.One minor note
The
awkhex-decode for/proc/net/routeis dense:It works, it's consistent with
nightly.yml, and it avoids theiproute2dependency — so no change needed. But if this ever needs debugging, the logic parses little-endian hex from column 3. Worth keeping the existingnightly.ymlcomment nearby as the canonical reference.🏛️ Markus Keller — Senior Application Architect
Verdict: ✅ Approved
This PR touches only CI workflow files. No architectural changes, no layer boundaries affected, no new infrastructure components introduced.
Doc update check
Per the architecture doc-update matrix:
No documentation updates are required for this change. The fix is squarely in the CI platform layer.
Consistency improvement worth noting
The
release.ymlpreviously usedip route show default | awk '/default/ {print $3}'to detect the Docker bridge gateway IP, whilenightly.ymlalready used the/proc/net/routeparsing approach. This PR aligns them — one pattern, two files. That's the right call: divergent approaches to the same problem in parallel workflows are a future maintenance trap.No concerns.
The infrastructure topology hasn't changed. The smoke test strategy (HSTS pin, Permissions-Policy pin, actuator block verification) remains intact and correct.
🔧 Tobias Wendt — DevOps & Platform Engineer
Verdict: ✅ Approved
This is a solid, minimal CI fix. Reviewing the infrastructure and pipeline changes.
What's correct
1. Bash array for
--resolve— correct fix for the root cause.The bug:
curl "$RESOLVE" ...whereRESOLVE="--resolve host:443:ip"passes the entire string as a single argument. curl sees--resolve host:443:ip(with a space inside) as an unknown option name and exits 2. The fix —RESOLVE=(--resolve "$HOST:443:$HOST_IP")with"${RESOLVE[@]}"— ensures curl receives exactly two words: the flag and its value. This is the correct Bash idiom.2.
/proc/net/routeoverip route show default— better choice.ip route show defaultrequires theiproute2package to be present in the runner container./proc/net/routeis a kernel virtual file — always present on Linux, no package dependency. The Gitea runner image may or may not haveiproute2; this was likely the actual root cause of failures in the release job. Good catch.3.
nightly.ymlandrelease.ymlare now consistent.Both use the same gateway detection logic and the same curl invocation pattern. Divergent approaches to identical problems in parallel jobs are a maintenance hazard. This PR closes that gap.
4. Error guard is in place.
Explicit failure with a clear error message before any curl call. Clean.
Suggestions (non-blocking)
awkhex-decode one-liner is opaque to anyone who hasn't worked with/proc/net/route's little-endian hex format before. The existing comment innightly.ymlexplains it well;release.ymlnow referencesnightly.ymlfor the full explanation — that cross-reference is acceptable.workflow_dispatchand av*tag push. That's the right approach for CI infrastructure — there's no cheaper way to verify it. Worth noting in the test plan that the nightly dispatch should be tested before merging if the release tag isn't imminent.🔐 Nora "NullX" Steiner — Application Security Engineer
Verdict: ✅ Approved
This PR modifies only CI smoke test scripts. Security surface is limited, and the existing controls are intact.
What I checked
1. No user-controlled input in shell variables.
HOSTis hardcoded in both scripts (HOST="archiv.raddatz.cloud"/HOST="$FRONTEND_HOST").HOST_IPis derived from/proc/net/route— a kernel file, no network input, no user-controlled data. No injection vector exists here.2. Secrets are not introduced or referenced in a new way.
No credentials are added. The workflow files do not introduce new
${{ secrets.* }}references or hardcode any values that should be secret.3. The smoke tests verify security-critical headers.
The suite actively checks:
max-age=31536000; includeSubDomains; preload) — not just header presence, but the exact HSTS policy required for preload list eligibility. A degradedmax-age=1now fails this check.camera=(), microphone=(), geolocation=()) — a loosened or dropped header fails the check./actuator/healthreturning 404 confirms the Caddy reverse proxy is blocking it. This is the right verification: if someone accidentally routes/actuator/*to the backend, this test catches it.These are meaningful security regression gates. The fix ensures they actually run correctly.
4.
/proc/net/routevsip route show default.No security difference between these two approaches. Both read system state to discover the Docker bridge IP. No external input, no SSRF vector.
No findings.
🧪 Sara Holt — Senior QA Engineer
Verdict: ✅ Approved
This is a CI infrastructure fix, not application code — no test pyramid changes are required or expected. Reviewing the quality of the smoke test suite and the verification approach.
Smoke test coverage assessment
The smoke tests check 4 distinct behaviors:
curl -fsS ... "$URL/login" -o /dev/null(basic connectivity + TLS)max-age=31536000.*includeSubDomains.*preloadcamera=\(\),.*microphone=\(\),.*geolocation=\(\)/actuator/healthEach check fails loudly on its own failure mode. This is a reasonable smoke suite for a deployment gate.
Fix verification
The test plan calls for:
workflow_dispatchtrigger for nightlyv*tag push for releaseThat's the correct approach. Shell script correctness in a Gitea Actions context can't be unit-tested in isolation — the only meaningful test is running it in the actual runner environment. The plan is honest about this.
One observation (non-blocking)
The
status=$(curl -s "${RESOLVE[@]}" ...)subshell captures the exit code from curl but thehttp_codefrom-w "%{http_code}". If curl itself fails (network error, TLS error),$statuswill be empty rather than an HTTP code, and the check[ "$status" = "404" ]will fail with a misleading message. This is pre-existing behavior, not introduced by this PR, and the failure mode is loud (exit 1) so it doesn't silently pass. No action needed.📋 Elicit — Requirements Engineer
Verdict: ✅ Approved
This is a targeted bug fix for a CI infrastructure regression. Reviewing from a requirements completeness standpoint.
Requirements traceability
The PR description clearly documents the full requirement chain:
"$RESOLVE"passes one shell token where curl expects two"${RESOLVE[@]}"release.ymlgateway detection aligned withnightly.ymlThe test plan correctly focuses on manual trigger verification — the only meaningful acceptance test for this class of fix.
No requirements gaps.
This PR doesn't introduce new user-facing behavior, modify any domain concept, or change system NFRs. No acceptance criteria artifacts are missing.
🎨 Leonie Voss — UI/UX Designer & Accessibility Strategist
Verdict: ✅ Approved
This PR modifies only CI workflow shell scripts — no user interface, no Svelte components, no HTML, no CSS, no design tokens. Zero UI/UX impact.
Nothing to review from my domain. LGTM.