Some checks failed
CI / Unit & Component Tests (pull_request) Has been cancelled
CI / OCR Service Tests (pull_request) Has been cancelled
CI / Backend Unit Tests (pull_request) Has been cancelled
CI / fail2ban Regex (pull_request) Has been cancelled
CI / Semgrep Security Scan (pull_request) Has been cancelled
CI / Compose Bucket Idempotency (pull_request) Has been cancelled
CI / Unit & Component Tests (push) Successful in 3m23s
CI / OCR Service Tests (push) Successful in 22s
CI / Backend Unit Tests (push) Successful in 3m37s
CI / fail2ban Regex (push) Successful in 46s
CI / Semgrep Security Scan (push) Successful in 21s
CI / Compose Bucket Idempotency (push) Successful in 1m5s
nightly / deploy-staging (push) Successful in 2m1s
Convert the two bare failure echoes (gateway detection, /actuator status) to ::error:: so Gitea renders them as CI log annotations, consistent with the rest of the deploy steps. No behaviour change. Raised in review (Leonie). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
59 lines
3.2 KiB
YAML
59 lines
3.2 KiB
YAML
name: Smoke test
|
|
description: >-
|
|
Verify the deployed public surface (login reachable, HSTS pinned,
|
|
Permissions-Policy present, /actuator blocked) against a given vhost.
|
|
|
|
inputs:
|
|
host:
|
|
description: Public vhost to smoke-test, e.g. staging.raddatz.cloud
|
|
required: true
|
|
|
|
runs:
|
|
using: composite
|
|
steps:
|
|
- name: Smoke test deployed environment
|
|
shell: bash
|
|
# Healthchecks confirm containers are healthy; they do NOT confirm the
|
|
# public surface works. This step catches: Caddy not reloaded, HSTS
|
|
# header dropped, /actuator block bypassed.
|
|
#
|
|
# --resolve pins the public host to the Docker bridge gateway IP
|
|
# (the host) so we do NOT depend on hairpin NAT on the host router.
|
|
# 127.0.0.1 cannot be used: job containers run in bridge network mode
|
|
# (runner-config.yaml), so 127.0.0.1 is the container's loopback, not
|
|
# the host's. The bridge gateway IS the host; Caddy binds 0.0.0.0:443
|
|
# and is therefore reachable from the container via that IP.
|
|
# SNI still uses the public hostname so the TLS cert validates correctly.
|
|
#
|
|
# --resolve is stored as a Bash array so "${RESOLVE[@]}" expands to two
|
|
# separate arguments; a quoted string would pass the flag and its value
|
|
# as one token and curl would reject it as an unknown option.
|
|
#
|
|
# Gateway detection reads /proc/net/route (always present, no package
|
|
# required) instead of `ip route` to avoid a dependency on iproute2.
|
|
# Field $2=="00000000" is the default route; field $3 is the gateway as
|
|
# a little-endian 32-bit hex value which awk decodes to dotted-decimal.
|
|
env:
|
|
HOST: ${{ inputs.host }}
|
|
run: |
|
|
set -e
|
|
URL="https://$HOST"
|
|
HOST_IP=$(awk 'NR>1 && $2=="00000000"{h=$3;printf "%d.%d.%d.%d\n",strtonum("0x"substr(h,7,2)),strtonum("0x"substr(h,5,2)),strtonum("0x"substr(h,3,2)),strtonum("0x"substr(h,1,2));exit}' /proc/net/route)
|
|
[ -n "$HOST_IP" ] || { echo "::error::could not detect Docker bridge gateway via /proc/net/route"; exit 1; }
|
|
RESOLVE=(--resolve "$HOST:443:$HOST_IP")
|
|
echo "Smoke test: $URL (pinned to $HOST_IP via bridge gateway)"
|
|
curl -fsS "${RESOLVE[@]}" --max-time 10 "$URL/login" -o /dev/null
|
|
# Pin the preload-list-eligible HSTS value, not just header presence:
|
|
# a degraded `max-age=1` or a dropped `includeSubDomains; preload` must
|
|
# fail this check rather than pass it silently.
|
|
curl -fsS "${RESOLVE[@]}" --max-time 10 -I "$URL/" \
|
|
| grep -Eqi 'strict-transport-security:[[:space:]]*max-age=31536000.*includeSubDomains.*preload'
|
|
# Permissions-Policy denies APIs the app does not use (camera,
|
|
# microphone, geolocation). A regression that loosens or drops the
|
|
# header now fails the smoke step.
|
|
curl -fsS "${RESOLVE[@]}" --max-time 10 -I "$URL/" \
|
|
| grep -Eqi 'permissions-policy:[[:space:]]*camera=\(\),[[:space:]]*microphone=\(\),[[:space:]]*geolocation=\(\)'
|
|
status=$(curl -s "${RESOLVE[@]}" -o /dev/null -w "%{http_code}" --max-time 10 "$URL/actuator/health")
|
|
[ "$status" = "404" ] || { echo "::error::expected 404 from /actuator/health, got $status"; exit 1; }
|
|
echo "All smoke checks passed"
|