Fullstack deploy smoke (real providers) #10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Fullstack deploy smoke (real providers) | |
| # #984 tier 3: deploys the Supabase × Cloudflare reference starter to a real | |
| # Cloudflare Worker and smokes the live URL. Requires maintainer-provided | |
| # secrets (see below). Manual + weekly only: never runs on push/PR. | |
| # | |
| # #997 scheduled-run semantics: schedule events carry no inputs, so the | |
| # job-level env below defaults ASYNC_MODE to `provision` — the weekly run | |
| # executes the full probe (Queue/DLQ verification, plus scanner redeploy when | |
| # the engine secrets exist), which is idempotent against the already-created | |
| # bounded async resources. The release freshness gate | |
| # (tools/check-evidence-freshness.ts) reads scheduled and workflow_dispatch | |
| # conclusions with newest-wins across triggers (ADR-0134): a weekly red run | |
| # blocks release exactly like a manual red, and a newer manual green | |
| # supersedes older scheduled reds. Manual runs intended as release evidence | |
| # must select the `provision` mode so they cover the full probe. | |
| # | |
| # #1070 / ADR-0139: real-provider qualification is a v0.44 gate. Absence is | |
| # recorded honestly without blocking the provider-neutral v0.43.1 release. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| async_mode: | |
| description: 'Keep the safe base deployment, or provision Queue/DLQ/Cron after Supabase migrations are current' | |
| required: true | |
| default: base | |
| type: choice | |
| options: | |
| - base | |
| - provision | |
| scanner_service: | |
| description: 'Existing private scanner Worker name used by the provision mode service binding' | |
| required: true | |
| default: openelement-attachment-scanner | |
| type: string | |
| schedule: | |
| # Tuesdays 06:00 UTC (staggered from the tier-2 Monday run) | |
| - cron: '0 6 * * 2' | |
| concurrency: | |
| group: fullstack-real-cloudflare | |
| cancel-in-progress: false | |
| permissions: | |
| contents: read | |
| jobs: | |
| deploy-smoke: | |
| name: Deploy reference starter to Workers and smoke the live URL | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| env: | |
| # Scheduled runs have no inputs; default them to the full provision | |
| # probe so the weekly evidence covers Queue/DLQ, not just HTTP. | |
| ASYNC_MODE: ${{ inputs.async_mode || 'provision' }} | |
| SCANNER_SERVICE: ${{ inputs.scanner_service || 'openelement-attachment-scanner' }} | |
| # Scanner steps run only when both private-engine secrets exist; | |
| # otherwise the artifact records not-configured instead of claiming a | |
| # positive-path qualification. | |
| SCANNER_CONFIGURED: ${{ secrets.METADEFENDER_CORE_URL != '' && secrets.METADEFENDER_API_KEY != '' }} | |
| steps: | |
| # v7.0.1 | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | |
| - uses: ./.github/actions/setup-deno-workspace | |
| - name: Build the Workers bundle | |
| working-directory: examples/supabase-cloudflare-starter | |
| run: | | |
| deno task build | |
| deno task nitro:build | |
| - name: Deploy the private attachment scanner | |
| if: env.ASYNC_MODE == 'provision' && env.SCANNER_CONFIGURED == 'true' | |
| working-directory: examples/supabase-cloudflare-starter | |
| env: | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| SUPABASE_URL: ${{ secrets.SUPABASE_URL }} | |
| SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} | |
| METADEFENDER_CORE_URL: ${{ secrets.METADEFENDER_CORE_URL }} | |
| METADEFENDER_API_KEY: ${{ secrets.METADEFENDER_API_KEY }} | |
| run: | | |
| set -euo pipefail | |
| for required in SUPABASE_URL SUPABASE_SERVICE_ROLE_KEY METADEFENDER_CORE_URL METADEFENDER_API_KEY SCANNER_SERVICE; do | |
| [ -n "${!required}" ] || { echo "$required is not configured" >&2; exit 1; } | |
| done | |
| deno run -A npm:wrangler@4.123.0 deploy --config scanner-wrangler.jsonc \ | |
| --name "$SCANNER_SERVICE" \ | |
| --var "SUPABASE_URL:$SUPABASE_URL" "METADEFENDER_CORE_URL:$METADEFENDER_CORE_URL" | |
| jq -n \ | |
| --arg supabase "$SUPABASE_SERVICE_ROLE_KEY" \ | |
| --arg metadefender "$METADEFENDER_API_KEY" \ | |
| '{SUPABASE_SERVICE_ROLE_KEY:$supabase,METADEFENDER_API_KEY:$metadefender}' | \ | |
| deno run -A npm:wrangler@4.123.0 secret bulk \ | |
| --config scanner-wrangler.jsonc --name "$SCANNER_SERVICE" | |
| - name: Record the scan engine as not configured | |
| if: env.ASYNC_MODE == 'provision' && env.SCANNER_CONFIGURED != 'true' | |
| run: | | |
| mkdir -p .smoke | |
| echo '{"check":"attachment-scanner-engine","result":"not-configured"}' >> .smoke/results.jsonl | |
| echo "scan engine is not configured (#1070): attachments stay pending_scan and undownloadable — fail-closed" | |
| echo "real benign/EICAR provider qualification is tracked for v0.44 (#1070)" | |
| - name: Render and provision bounded async resources | |
| if: env.ASYNC_MODE == 'provision' | |
| working-directory: examples/supabase-cloudflare-starter | |
| env: | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} | |
| STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }} | |
| STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }} | |
| STRIPE_PRICE_ID: ${{ secrets.STRIPE_PRICE_ID }} | |
| run: | | |
| set -euo pipefail | |
| [ -n "$SUPABASE_SERVICE_ROLE_KEY" ] || { echo "SUPABASE_SERVICE_ROLE_KEY is not configured" >&2; exit 1; } | |
| [ -n "$STRIPE_SECRET_KEY" ] || { echo "STRIPE_SECRET_KEY is not configured" >&2; exit 1; } | |
| [ -n "$STRIPE_WEBHOOK_SECRET" ] || { echo "STRIPE_WEBHOOK_SECRET is not configured" >&2; exit 1; } | |
| [ -n "$STRIPE_PRICE_ID" ] || { echo "STRIPE_PRICE_ID is not configured" >&2; exit 1; } | |
| [ -n "$SCANNER_SERVICE" ] || { echo "scanner_service is required" >&2; exit 1; } | |
| if [ "$SCANNER_CONFIGURED" = "true" ]; then | |
| deno run --allow-read --allow-write ../../tools/render-cloudflare-async-config.ts \ | |
| wrangler.jsonc .wrangler-async.generated.json "$SCANNER_SERVICE" | |
| else | |
| deno run --allow-read --allow-write ../../tools/render-cloudflare-async-config.ts \ | |
| wrangler.jsonc .wrangler-async.generated.json --omit-scanner | |
| fi | |
| # wrangler 4.x `queues list` has no --json flag; enumerate existing | |
| # queues through the Cloudflare API instead. | |
| queues=$(curl -sf --max-time 30 \ | |
| -H "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \ | |
| "https://api.cloudflare.com/client/v4/accounts/$CLOUDFLARE_ACCOUNT_ID/queues?per_page=100") | |
| # Queue names are hardcoded here; the canonical source is | |
| # examples/supabase-cloudflare-starter/lib/cloudflare-queues.ts | |
| # (parity covered by tools/render-cloudflare-async-config.test.ts). | |
| for queue in \ | |
| openelement-attachment-scan \ | |
| openelement-attachment-scan-dlq \ | |
| openelement-attachment-scan-persistence-failures \ | |
| openelement-payment-events \ | |
| openelement-payment-events-dlq \ | |
| openelement-payment-events-persistence-failures; do | |
| if echo "$queues" | jq -e --arg name "$queue" '.result[] | select(.queue_name == $name)' >/dev/null; then | |
| echo "$queue already exists" | |
| else | |
| deno run -A npm:wrangler@4.123.0 queues create "$queue" | |
| fi | |
| done | |
| printf '%s' "$SUPABASE_SERVICE_ROLE_KEY" | \ | |
| deno run -A npm:wrangler@4.123.0 secret put SUPABASE_SERVICE_ROLE_KEY \ | |
| --config .wrangler-async.generated.json | |
| for secret_name in STRIPE_SECRET_KEY STRIPE_WEBHOOK_SECRET STRIPE_PRICE_ID; do | |
| printf '%s' "${!secret_name}" | \ | |
| deno run -A npm:wrangler@4.123.0 secret put "$secret_name" \ | |
| --config .wrangler-async.generated.json | |
| done | |
| - name: Select deployment config | |
| working-directory: examples/supabase-cloudflare-starter | |
| run: | | |
| if [ "$ASYNC_MODE" = "provision" ]; then | |
| echo "WRANGLER_CONFIG=.wrangler-async.generated.json" >> "$GITHUB_ENV" | |
| else | |
| echo "WRANGLER_CONFIG=wrangler.jsonc" >> "$GITHUB_ENV" | |
| fi | |
| - name: Deploy to Cloudflare Workers | |
| working-directory: examples/supabase-cloudflare-starter | |
| env: | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| SUPABASE_URL: ${{ secrets.SUPABASE_URL }} | |
| SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }} | |
| run: >- | |
| deno run -A npm:wrangler@4.123.0 deploy --config "$WRANGLER_CONFIG" | |
| --var "SUPABASE_URL:$SUPABASE_URL" "SUPABASE_ANON_KEY:$SUPABASE_ANON_KEY" | |
| "STRIPE_LIVEMODE:false" | |
| "APP_ORIGIN:https://openelement-ref-starter.freemanzheng.workers.dev" | |
| "STRIPE_CHECKOUT_HOST:checkout.stripe.com" | |
| - name: Smoke the live worker | |
| env: | |
| WORKER_URL: https://openelement-ref-starter.freemanzheng.workers.dev | |
| run: | | |
| set -e | |
| mkdir -p .smoke | |
| record() { echo "{\"check\":\"$1\",\"result\":\"$2\"}" >> .smoke/results.jsonl; } | |
| for i in 1 2 3 4 5 6; do | |
| code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 "$WORKER_URL/" || true) | |
| [ "$code" = "200" ] && break | |
| sleep 10 | |
| done | |
| echo "GET / -> $code"; [ "$code" = "200" ] | |
| record worker-root-200 pass | |
| notes=$(curl -s --max-time 15 "$WORKER_URL/notes") | |
| echo "$notes" | grep -q "Sign-in is required" | |
| record anonymous-notes-denied pass | |
| csrf=$(curl -s -o /dev/null -w '%{http_code}' --max-time 15 -X POST "$WORKER_URL/login" -H 'Origin: https://evil.example.com' -H 'Content-Type: application/x-www-form-urlencoded' -d 'x=1') | |
| echo "cross-origin POST /login -> $csrf"; [ "$csrf" = "403" ] | |
| record cross-origin-login-denied pass | |
| - name: Probe cookie security floor and Host hygiene | |
| env: | |
| WORKER_URL: https://openelement-ref-starter.freemanzheng.workers.dev | |
| SUPABASE_URL: ${{ secrets.SUPABASE_URL }} | |
| SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} | |
| run: | | |
| set -euo pipefail | |
| # Short-lived smoke user; the random password is never logged. The | |
| # id goes to $GITHUB_ENV so the always() cleanup step below can | |
| # delete the user again. | |
| SMOKE_EMAIL="deploy-smoke-$(date +%s)@example.com" | |
| SMOKE_PASSWORD="Deploy-Smoke-$(date +%s)-$RANDOM$RANDOM" | |
| SMOKE_USER_ID=$(curl -sf -X POST "$SUPABASE_URL/auth/v1/admin/users" \ | |
| -H "apikey: $SUPABASE_SERVICE_ROLE_KEY" \ | |
| -H "Authorization: Bearer $SUPABASE_SERVICE_ROLE_KEY" \ | |
| -H 'Content-Type: application/json' \ | |
| -d "{\"email\":\"$SMOKE_EMAIL\",\"password\":\"$SMOKE_PASSWORD\",\"email_confirm\":true}" \ | |
| | jq -r .id) | |
| echo "SMOKE_USER_ID=$SMOKE_USER_ID" >> "$GITHUB_ENV" | |
| ref=$(printf '%s' "$SUPABASE_URL" | sed -E 's|https?://([^.]+)\..*|\1|') | |
| headers=$(mktemp) | |
| # Plant stale chunk cookies: the session write must delete them too, | |
| # so the response carries several Set-Cookie headers and proves the | |
| # response path preserves multi Set-Cookie instead of collapsing it. | |
| curl -s -D "$headers" -o /dev/null --max-time 20 -X POST "$WORKER_URL/login" \ | |
| -H 'Content-Type: application/x-www-form-urlencoded' \ | |
| -H "Cookie: sb-$ref-auth-token.0=stale0; sb-$ref-auth-token.1=stale1" \ | |
| --data-urlencode "email=$SMOKE_EMAIL" \ | |
| --data-urlencode "password=$SMOKE_PASSWORD" | |
| tr -d '\r' < "$headers" > "$headers.clean" | |
| cookies=$(grep -c '^set-cookie:' "$headers.clean" || true) | |
| echo "POST /login Set-Cookie count -> $cookies" | |
| [ "$cookies" -ge 2 ] | |
| grep '^set-cookie:' "$headers.clean" | while IFS= read -r line; do | |
| for attribute in 'httponly' 'samesite=lax' 'path=/' 'secure'; do | |
| printf '%s' "$line" | grep -qi "$attribute" || { | |
| echo "Set-Cookie missing $attribute: $line"; exit 1; | |
| } | |
| done | |
| done | |
| echo "all $cookies Set-Cookie headers carry HttpOnly; SameSite=Lax; Path=/; Secure -> OK" | |
| location=$(grep -i '^location:' "$headers.clean" | head -1 | cut -d' ' -f2-) | |
| echo "POST /login Location -> $location" | |
| case "$location" in | |
| /*|"$WORKER_URL"*) : ;; | |
| *) echo "login redirect leaked a non-application host: $location"; exit 1 ;; | |
| esac | |
| if grep -iE 'localhost|127\.0\.0\.1|\.internal|\.supabase\.co' "$headers.clean"; then | |
| echo "internal/provider hostname leaked in login response headers"; exit 1 | |
| fi | |
| echo "Host hygiene: no internal/provider hostname in response headers -> OK" | |
| rm -f "$headers" "$headers.clean" | |
| - name: Verify provisioned Queue and DLQ | |
| if: env.ASYNC_MODE == 'provision' | |
| working-directory: examples/supabase-cloudflare-starter | |
| env: | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$SCANNER_CONFIGURED" = "true" ]; then | |
| deno run -A npm:wrangler@4.123.0 deployments list \ | |
| --name "$SCANNER_SERVICE" --json | jq -e 'length > 0' >/dev/null | |
| else | |
| echo "scan engine not configured (#1070); skipping scanner deployment check" | |
| fi | |
| # Queue names mirror the provisioning loop above; canonical source: | |
| # examples/supabase-cloudflare-starter/lib/cloudflare-queues.ts. | |
| deno run -A npm:wrangler@4.123.0 queues info openelement-attachment-scan >/dev/null | |
| deno run -A npm:wrangler@4.123.0 queues info openelement-attachment-scan-dlq >/dev/null | |
| deno run -A npm:wrangler@4.123.0 queues info \ | |
| openelement-attachment-scan-persistence-failures >/dev/null | |
| deno run -A npm:wrangler@4.123.0 queues info openelement-payment-events >/dev/null | |
| deno run -A npm:wrangler@4.123.0 queues info openelement-payment-events-dlq >/dev/null | |
| deno run -A npm:wrangler@4.123.0 queues info \ | |
| openelement-payment-events-persistence-failures >/dev/null | |
| echo "Attachment/payment Queue + DLQ + persistence-failure provider lookup -> OK" | |
| echo '{"check":"attachment-payment-queues","result":"pass"}' \ | |
| >> "$GITHUB_WORKSPACE/.smoke/results.jsonl" | |
| - name: Qualify real scanner clean and EICAR paths | |
| if: env.ASYNC_MODE == 'provision' && env.SCANNER_CONFIGURED == 'true' | |
| env: | |
| WORKER_URL: https://openelement-ref-starter.freemanzheng.workers.dev | |
| SUPABASE_URL: ${{ secrets.SUPABASE_URL }} | |
| SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} | |
| run: | | |
| set -euo pipefail | |
| cookie_jar=$(mktemp) | |
| clean_file=$(mktemp) | |
| eicar_file=$(mktemp) | |
| user_id='' | |
| cleanup() { | |
| if [ -n "$user_id" ]; then | |
| # Remove private objects before deleting the Auth owner. Never | |
| # print keys, user ids, credentials, or fixture contents. | |
| rows=$(curl -sf --max-time 20 \ | |
| "$SUPABASE_URL/rest/v1/attachment_reservations?select=object_key&user_id=eq.$user_id" \ | |
| -H "apikey: $SUPABASE_SERVICE_ROLE_KEY" \ | |
| -H "Authorization: Bearer $SUPABASE_SERVICE_ROLE_KEY" || echo '[]') | |
| echo "$rows" | jq -r '.[].object_key' | while IFS= read -r key; do | |
| encoded=$(printf '%s' "$key" | jq -sRr @uri | sed 's|%2F|/|g') | |
| curl -s -o /dev/null -X DELETE \ | |
| "$SUPABASE_URL/storage/v1/object/notes-attachments/$encoded" \ | |
| -H "apikey: $SUPABASE_SERVICE_ROLE_KEY" \ | |
| -H "Authorization: Bearer $SUPABASE_SERVICE_ROLE_KEY" || true | |
| done | |
| curl -s -o /dev/null -X DELETE \ | |
| "$SUPABASE_URL/auth/v1/admin/users/$user_id" \ | |
| -H "apikey: $SUPABASE_SERVICE_ROLE_KEY" \ | |
| -H "Authorization: Bearer $SUPABASE_SERVICE_ROLE_KEY" || true | |
| fi | |
| rm -f "$cookie_jar" "$clean_file" "$eicar_file" | |
| } | |
| trap cleanup EXIT | |
| suffix="$(date +%s)-$RANDOM" | |
| email="scanner-smoke-$suffix@example.com" | |
| password="Scanner-Smoke-$suffix-$RANDOM" | |
| user_id=$(curl -sf --max-time 20 -X POST "$SUPABASE_URL/auth/v1/admin/users" \ | |
| -H "apikey: $SUPABASE_SERVICE_ROLE_KEY" \ | |
| -H "Authorization: Bearer $SUPABASE_SERVICE_ROLE_KEY" \ | |
| -H 'Content-Type: application/json' \ | |
| -d "{\"email\":\"$email\",\"password\":\"$password\",\"email_confirm\":true}" \ | |
| | jq -er .id) | |
| curl -sf --max-time 20 -c "$cookie_jar" -o /dev/null -X POST "$WORKER_URL/login" \ | |
| -H "Origin: $WORKER_URL" \ | |
| -H 'Content-Type: application/x-www-form-urlencoded' \ | |
| --data-urlencode "email=$email" --data-urlencode "password=$password" | |
| clean_name="scanner-clean-$suffix.txt" | |
| eicar_name="scanner-eicar-$suffix.txt" | |
| printf 'OpenElement scanner qualification clean fixture.\n' > "$clean_file" | |
| # Standard EICAR test string, generated only in the ephemeral runner. | |
| printf '%s' 'X5O!P%@AP[4\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*' > "$eicar_file" | |
| for fixture in clean eicar; do | |
| name_var="${fixture}_name"; file_var="${fixture}_file" | |
| curl -sf --max-time 30 -b "$cookie_jar" -o /dev/null -X POST \ | |
| "$WORKER_URL/upload?/upload" -H "Origin: $WORKER_URL" \ | |
| -F "file=@${!file_var};filename=${!name_var};type=text/plain" | |
| done | |
| reservation_state() { | |
| local name="$1" | |
| curl -sf --max-time 20 \ | |
| "$SUPABASE_URL/rest/v1/attachment_reservations?select=state&user_id=eq.$user_id&display_name=eq.$name" \ | |
| -H "apikey: $SUPABASE_SERVICE_ROLE_KEY" \ | |
| -H "Authorization: Bearer $SUPABASE_SERVICE_ROLE_KEY" | jq -r '.[0].state // "missing"' | |
| } | |
| wait_for_state() { | |
| local name="$1" expected="$2" state='' | |
| for _ in $(seq 1 36); do | |
| state=$(reservation_state "$name") | |
| [ "$state" = "$expected" ] && return 0 | |
| case "$state" in pending_scan|reserved|missing) sleep 5 ;; *) return 1 ;; esac | |
| done | |
| return 1 | |
| } | |
| wait_for_state "$clean_name" clean | |
| wait_for_state "$eicar_name" quarantined | |
| owner_html=$(curl -sf --max-time 20 -b "$cookie_jar" "$WORKER_URL/upload") | |
| printf '%s' "$owner_html" | grep -q "$clean_name" | |
| if printf '%s' "$owner_html" | grep -q "$eicar_name"; then | |
| echo 'quarantined fixture became owner-downloadable' >&2; exit 1 | |
| fi | |
| signed_url=$(printf '%s' "$owner_html" | \ | |
| grep -oE '<a href="[^"]+">[^<]*</a>' | grep "$clean_name" | \ | |
| sed -n 's/.*href="\([^"]*\)".*/\1/p' | head -1 | sed 's/&/\&/g') | |
| [ -n "$signed_url" ] | |
| [ "$(curl -s -o /dev/null -w '%{http_code}' --max-time 20 "$signed_url")" = '200' ] | |
| echo '{"check":"attachment-scanner-real-clean-owner-download","result":"pass"}' >> .smoke/results.jsonl | |
| echo '{"check":"attachment-scanner-real-eicar-quarantined","result":"pass"}' >> .smoke/results.jsonl | |
| echo 'real scanner clean/EICAR owner-download qualification -> OK (redacted)' | |
| - name: Qualify scanner retry, DLQ, and authenticated replay | |
| if: env.ASYNC_MODE == 'provision' && env.SCANNER_CONFIGURED == 'true' | |
| env: | |
| WORKER_URL: https://openelement-ref-starter.freemanzheng.workers.dev | |
| SUPABASE_URL: ${{ secrets.SUPABASE_URL }} | |
| SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} | |
| METADEFENDER_CORE_URL: ${{ secrets.METADEFENDER_CORE_URL }} | |
| SCANNER_SERVICE: ${{ env.SCANNER_SERVICE }} | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| run: bash tools/qualify-scanner-retry-replay.sh | |
| - name: Delete the throwaway smoke user (Auth admin API) | |
| id: cleanup-user | |
| # Cleanup guarantee: runs even when the probes above fail, so the | |
| # project never accumulates deploy-smoke users. The report step | |
| # records this step's outcome as the artifact's cleanup field. | |
| if: always() | |
| env: | |
| SUPABASE_URL: ${{ secrets.SUPABASE_URL }} | |
| SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} | |
| run: | | |
| if [ -z "${SMOKE_USER_ID:-}" ]; then | |
| echo "no smoke user was created; nothing to clean up" | |
| exit 0 | |
| fi | |
| code=$(curl -s -o /dev/null -w '%{http_code}' -X DELETE \ | |
| "$SUPABASE_URL/auth/v1/admin/users/$SMOKE_USER_ID" \ | |
| -H "apikey: $SUPABASE_SERVICE_ROLE_KEY" \ | |
| -H "Authorization: Bearer $SUPABASE_SERVICE_ROLE_KEY") | |
| echo "DELETE admin user -> $code" | |
| [ "$code" = "200" ] | |
| - name: Write the smoke report | |
| if: always() | |
| env: | |
| WORKER_URL: https://openelement-ref-starter.freemanzheng.workers.dev | |
| run: | | |
| checks='[]' | |
| if [ -f .smoke/results.jsonl ]; then | |
| checks=$(jq -s '.' .smoke/results.jsonl) | |
| fi | |
| jq -n \ | |
| --arg version "$(git rev-parse --short HEAD)" \ | |
| --arg runId "${{ github.run_id }}" \ | |
| --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ | |
| --arg environment "cloudflare-workers-test" \ | |
| --arg workerUrl "$WORKER_URL" \ | |
| --arg asyncMode "$ASYNC_MODE" \ | |
| --arg scannerEngine "$SCANNER_CONFIGURED" \ | |
| --argjson asyncResourcesVerified "$([ "$ASYNC_MODE" = "provision" ] && echo true || echo false)" \ | |
| --arg status "${{ job.status }}" \ | |
| --arg cleanup "${{ steps.cleanup-user.outcome || 'not-run' }}" \ | |
| --argjson checks "$checks" \ | |
| '{ | |
| version: $version, | |
| runId: $runId, | |
| timestamp: $timestamp, | |
| environment: $environment, | |
| workerUrl: $workerUrl, | |
| asyncMode: $asyncMode, | |
| scannerEngine: (if $scannerEngine == "true" then "configured" else "not-configured" end), | |
| asyncResourcesVerified: $asyncResourcesVerified, | |
| jobStatus: $status, | |
| cleanup: $cleanup, | |
| checks: $checks, | |
| note: "tier-3 deploy smoke; redacted — no credentials in this report; the test worker and queues are persistent shared infrastructure, only the smoke user is cleaned up per run" | |
| }' > fullstack-deploy-smoke.json | |
| - name: Archive the smoke report | |
| if: always() | |
| # v7.0.1 | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a | |
| with: | |
| name: fullstack-deploy-smoke | |
| path: fullstack-deploy-smoke.json | |
| if-no-files-found: error |