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"
