Skip to content

chore(deps): bump actions/checkout from 6 to 7 in the actions group #357

chore(deps): bump actions/checkout from 6 to 7 in the actions group

chore(deps): bump actions/checkout from 6 to 7 in the actions group #357

# Layer-1 PR-gate: AUTH-004 CORS contract + magic-link round-trip
#
# Why: the 2026-05-29 → 2026-05-30 login outage had THREE stacked failures
# (web missing /auth/exchange POST, web sending Accept:application/json,
# api missing access-control-allow-credentials). The worker auth-probe
# catches it 5 min post-deploy; the OpenAPI contract CI catches schema
# drift at PR time. Neither catches it END-TO-END IN A BROWSER before
# merge. This workflow does — see e2e/auth-contract.spec.ts.
#
# What it runs:
# - Three Chromium-driven asserts against PROD api.instanode.dev +
# instanode.dev (override with E2E_API_URL / E2E_WEB_ORIGIN).
# - Bounded under 2 minutes (live tests are ~3s; lion's share is
# npm ci + Chromium download).
#
# Triggers:
# - pull_request to main (any path — auth surface is implicit to every PR).
# - push to main (post-merge canary).
# - workflow_dispatch (manual smoke).
# - repository_dispatch with type `auth-contract-e2e-from-api` (fired by
# the api repo's CI workflow when an api PR opens/pushes — closes the
# cross-repo gap, since an api-side CORS regression would otherwise
# ship without this gate firing).
name: Auth contract E2E (PR gate)
on:
pull_request:
branches: [main]
push:
branches: [main]
workflow_dispatch:
inputs:
api_url:
description: 'Override E2E_API_URL'
required: false
default: 'https://api.instanode.dev'
web_origin:
description: 'Override E2E_WEB_ORIGIN'
required: false
default: 'https://instanode.dev'
run_roundtrip:
description: 'Also run the cookie-exchange ROUND-TRIP spec (needs a NON-prod api + E2E_JWT_SECRET secret)'
type: boolean
required: false
default: false
repository_dispatch:
types: [auth-contract-e2e-from-api]
concurrency:
# Cancel an in-flight run when a new commit lands on the same PR.
# Different PRs / triggers still run in parallel.
group: auth-contract-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
auth-contract-e2e:
name: Auth contract smoke against prod
runs-on: ubuntu-latest
timeout-minutes: 5
env:
# SECURITY: never interpolate raw repository_dispatch client_payload
# values into `run:` scripts (workflow-injection sink). Pipe through
# env: and let the validate step sanitise. workflow_dispatch inputs
# are gated by repo write permission so are lower-risk but get the
# same treatment for consistency.
RAW_API_URL: ${{ github.event.inputs.api_url || github.event.client_payload.api_url || '' }}
RAW_WEB_ORIGIN: ${{ github.event.inputs.web_origin || github.event.client_payload.web_origin || '' }}
TRIGGER: ${{ github.event_name }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'
- name: Validate + resolve E2E targets
# Allowlist the only hosts this smoke is permitted to drive. An
# attacker who fires a repository_dispatch with a hostile api_url
# cannot redirect the test to an attacker-controlled origin and
# exfiltrate the GITHUB_TOKEN — the value is rejected here.
# `set -u` would NOT catch the injection; explicit validation does.
env:
DEFAULT_API_URL: https://api.instanode.dev
DEFAULT_WEB_ORIGIN: https://instanode.dev
run: |
set -euo pipefail
api="${RAW_API_URL:-$DEFAULT_API_URL}"
web="${RAW_WEB_ORIGIN:-$DEFAULT_WEB_ORIGIN}"
case "$api" in
https://api.instanode.dev|https://api.instanode.dev/) ;;
*) echo "::error::E2E_API_URL '$api' not in allowlist {https://api.instanode.dev}"; exit 1 ;;
esac
case "$web" in
https://instanode.dev|https://instanode.dev/|https://www.instanode.dev|https://www.instanode.dev/) ;;
*) echo "::error::E2E_WEB_ORIGIN '$web' not in allowlist {https://instanode.dev,https://www.instanode.dev}"; exit 1 ;;
esac
# Strip trailing slash for stable comparison in the spec.
api="${api%/}"
web="${web%/}"
echo "E2E_API_URL=$api" >> "$GITHUB_ENV"
echo "E2E_WEB_ORIGIN=$web" >> "$GITHUB_ENV"
echo "Resolved E2E_API_URL=$api E2E_WEB_ORIGIN=$web trigger=$TRIGGER"
- run: npm ci
# Only Chromium — this is a smoke, not a cross-browser matrix.
- run: npx playwright install --with-deps chromium
- name: Run auth-contract smoke
run: npx playwright test --config=playwright.auth-contract.config.ts
# Upload trace + screenshots on failure so the PR author can replay
# the exact browser session locally (`npx playwright show-trace ...`).
- name: Upload Playwright trace on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: auth-contract-trace-${{ github.run_id }}
path: |
test-results/
playwright-report-auth-contract/
if-no-files-found: ignore
retention-days: 14
# ── Cookie-exchange ROUND-TRIP (manual / staging only) ───────────────────
#
# The Layer-1 smoke above probes the CORS envelope with NO cookie. This job
# runs the full round-trip spec (e2e/auth-roundtrip.spec.ts): provision →
# claim → plant the bridge cookie → cross-origin exchange → Bearer
# /auth/me 200. That needs a NON-prod api it can mint a bridge cookie for
# (E2E_JWT_SECRET = the api's JWT_SECRET) + a provisioning backend.
#
# WHY workflow_dispatch-only (not on every PR): instanode-web CI cannot
# build/boot the api binary (cross-repo), and we never put the prod
# JWT_SECRET in this repo's CI. The AUTHORITATIVE pre-merge round-trip gate
# lives in the api repo's Layer-2 docker-compose workflow (which builds the
# api from PR source + has the stack-local secret). This job is the
# instanode-web-side companion an operator points at a staging/compose api
# on demand. The spec self-skips loudly when the secret is absent or the
# backend returns 503, so a misfire reports as skipped, never a false red.
auth-roundtrip:
name: Cookie-exchange round-trip (staging/compose, on demand)
if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.run_roundtrip == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 8
env:
# Route dispatch inputs through env: + validate (workflow-injection
# hygiene — same pattern as the smoke job above). The round-trip MUST
# NOT target prod (planting a bridge cookie needs the stack JWT_SECRET).
RAW_API_URL: ${{ github.event.inputs.api_url || '' }}
RAW_WEB_ORIGIN: ${{ github.event.inputs.web_origin || '' }}
# E2E_JWT_SECRET is the api's JWT_SECRET for the TARGET (staging/compose)
# api — provided as a repo/environment secret by the operator who runs
# this. Never the prod secret.
E2E_JWT_SECRET: ${{ secrets.E2E_JWT_SECRET }}
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v6
with:
node-version: '22'
cache: 'npm'
- name: Validate + resolve round-trip targets
run: |
set -euo pipefail
api="${RAW_API_URL:-}"
web="${RAW_WEB_ORIGIN:-http://localhost:5173}"
if [ -z "$api" ]; then
echo "::error::run_roundtrip requires a non-empty api_url pointing at a staging/compose api (NOT prod)."
exit 1
fi
# Hard refuse the prod api — the round-trip would need the prod
# JWT_SECRET, which must never enter this repo's CI.
case "$api" in
https://api.instanode.dev|https://api.instanode.dev/)
echo "::error::round-trip cannot target prod api (api.instanode.dev). Point it at a staging/compose api."
exit 1 ;;
esac
api="${api%/}"
web="${web%/}"
echo "E2E_API_URL=$api" >> "$GITHUB_ENV"
echo "E2E_WEB_ORIGIN=$web" >> "$GITHUB_ENV"
echo "Resolved round-trip E2E_API_URL=$api E2E_WEB_ORIGIN=$web"
- run: npm ci
- run: npx playwright install --with-deps chromium
- name: Run cookie-exchange round-trip
# Spec self-skips (loudly) when E2E_JWT_SECRET is empty or the
# provisioning backend returns 503 — so a missing secret reports as
# skipped, not a hard failure.
run: npx playwright test --config=playwright.auth-roundtrip.config.ts
- name: Upload round-trip trace on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: auth-roundtrip-trace-${{ github.run_id }}
path: |
test-results/
playwright-report-auth-roundtrip/
if-no-files-found: ignore
retention-days: 14