Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
291 changes: 291 additions & 0 deletions .github/workflows/e2e-nightly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
name: 🌙 E2E Nightly (latest ThunderID)

# Runs the same E2E suite as pr-builder.yml's `e2e` job, against the same always-latest ThunderID
# release. The two differ only in trigger: this one runs on a schedule regardless of PR activity,
# so a ThunderID release that breaks something surfaces even on a day with no relevant PR open —
# the per-PR job (see pr-builder.yml) is label-gated and opt-in, not scheduled.

on:
schedule:
- cron: "30 18 * * *" # 18:30 UTC daily
workflow_dispatch:

env:
NODE_VERSION: "lts/*"

jobs:
e2e-nightly:
name: 🌙 E2E (sample apps, latest ThunderID)
runs-on: ubuntu-latest
timeout-minutes: 40
# Least privilege: this job never pushes, comments, or writes to the repo — only the default
# GITHUB_TOKEN's read access is needed for checkout.
permissions:
contents: read
steps:
- name: 📥 Checkout Code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false

- name: ⚙️ Set up Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: ${{ env.NODE_VERSION }}

- name: 📦 Set up pnpm
uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0
with:
version: latest
run_install: false

- name: 🗄️ Cache pnpm store
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
with:
path: ~/.local/share/pnpm/store
key: ${{ runner.os }}-pnpm-${{ hashFiles('pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-

- name: 📦 Install Dependencies
run: pnpm install --frozen-lockfile

- name: 🔨 Build SDK packages
run: pnpm build

- name: 🚀 Install, set up, and start ThunderID (latest, via npx)
id: install
working-directory: tests/e2e
# See the identically-named step in pr-builder.yml's `e2e` job: `npx thunderid` resolves
# and downloads the latest release itself and runs setup.sh non-interactively, then we
# start it ourselves with `start.sh` since npx thunderid's own background process doesn't
# outlive its own (expected) REPL/TTY failure on this runner.
run: |
# setsid detaches into a new session with no controlling terminal — see the identical
# note in pr-builder.yml's `e2e` job / tests/e2e/run-e2e.sh.
THUNDERID_ADMIN_USERNAME=admin THUNDERID_ADMIN_PASSWORD=admin \
setsid npx --yes thunderid --verbose < /dev/null || true

DIST_HOME=$(find "$PWD/thunderid" -maxdepth 1 -type d -name 'v*' | head -1)
if [ -z "$DIST_HOME" ]; then
echo "ERROR: npx thunderid did not produce an installed ThunderID release" && exit 1
fi
echo "dist_home=$DIST_HOME" >> "$GITHUB_OUTPUT"

chmod +x "$DIST_HOME/start.sh"
(cd "$DIST_HOME" && setsid ./start.sh &)
for i in $(seq 1 60); do
curl -skf https://localhost:8090/health/liveness && exit 0
sleep 2
done
echo "ERROR: ThunderID server did not become ready" && exit 1

- name: 🔑 Obtain admin token

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since some of these steps are duplicated in 2 workflows, we can extract them out to a action and re-use.

id: admin-token
# See the identically-named step in pr-builder.yml's `e2e` job — the same bash sequence, no
# reference to thunder-id/thunderid's branch or a pinned commit.
run: |
REDIRECT_URI="https://localhost:8090/console"
CODE_VERIFIER=$(openssl rand -hex 32 | cut -c1-43)
CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')

HEADERS_FILE=$(mktemp)
curl -sk -o /dev/null -D "$HEADERS_FILE" \
-G "https://localhost:8090/oauth2/authorize" \
--data-urlencode "client_id=CONSOLE" \
--data-urlencode "redirect_uri=$REDIRECT_URI" \
--data-urlencode "scope=system" \
--data-urlencode "resource=https://localhost:8090/mcp" \
--data-urlencode "response_type=code" \
--data-urlencode "code_challenge=$CODE_CHALLENGE" \
--data-urlencode "code_challenge_method=S256"

LOCATION=$(grep -i "^location:" "$HEADERS_FILE" | tr -d '\r' | sed 's/^[Ll]ocation: //')
rm -f "$HEADERS_FILE"
AUTH_ID=$(echo "$LOCATION" | sed -n 's/.*[?&]authId=\([^&]*\).*/\1/p')
EXEC_ID=$(echo "$LOCATION" | sed -n 's/.*[?&]executionId=\([^&]*\).*/\1/p')
[ -n "$AUTH_ID" ] && [ -n "$EXEC_ID" ] || { echo "ERROR: Failed to parse authId/executionId from authorize redirect."; exit 1; }

PROMPT_RESP=$(curl -sk -X POST "https://localhost:8090/flow/execute" \
-H "Content-Type: application/json" -d "{\"executionId\": \"$EXEC_ID\"}")
CHALLENGE_TOKEN=$(echo "$PROMPT_RESP" | jq -r '.challengeToken // empty')
[ -n "$CHALLENGE_TOKEN" ] || { echo "ERROR: Flow execution did not return a challenge token: $PROMPT_RESP"; exit 1; }

FLOW_RESP=$(curl -sk -X POST "https://localhost:8090/flow/execute" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg executionId "$EXEC_ID" --arg challengeToken "$CHALLENGE_TOKEN" \
--arg username "admin" --arg password "admin" --arg action "action_001" \
'{executionId: $executionId, challengeToken: $challengeToken, inputs: {username: $username, password: $password}, action: $action}')")
ASSERTION=$(echo "$FLOW_RESP" | jq -r '.assertion // empty')
[ -n "$ASSERTION" ] || { echo "ERROR: Admin authentication failed: $FLOW_RESP"; exit 1; }

CALLBACK_RESP=$(curl -sk -X POST "https://localhost:8090/oauth2/auth/callback" \
-H "Content-Type: application/json" -d "{\"authId\": \"$AUTH_ID\", \"assertion\": \"$ASSERTION\"}")
AUTH_CODE=$(echo "$CALLBACK_RESP" | jq -r '.redirect_uri // empty' | sed 's/.*[?&]code=\([^&]*\).*/\1/')
[ -n "$AUTH_CODE" ] || { echo "ERROR: OAuth2 callback did not return an authorization code: $CALLBACK_RESP"; exit 1; }

TOKEN_RESP=$(curl -sk -X POST "https://localhost:8090/oauth2/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=$AUTH_CODE" \
--data-urlencode "redirect_uri=$REDIRECT_URI" \
--data-urlencode "client_id=CONSOLE" \
--data-urlencode "resource=https://localhost:8090/mcp" \
--data-urlencode "code_verifier=$CODE_VERIFIER")
TOKEN=$(echo "$TOKEN_RESP" | jq -r '.access_token // empty')
[ -n "$TOKEN" ] || { echo "ERROR: Failed to obtain admin access token: $TOKEN_RESP"; exit 1; }
echo "::add-mask::$TOKEN"
echo "token=$TOKEN" >> "$GITHUB_OUTPUT"

- name: 📝 Import sample app OAuth2 clients
# See the identically-named step in pr-builder.yml's `e2e` job — the same bash sequence, no
# reference to thunder-id/thunderid's branch or a pinned commit.
env:
ADMIN_TOKEN: ${{ steps.admin-token.outputs.token }}
run: |
CONTENT=$(jq -Rs . < tests/e2e/thunderid-config/sample-apps.yaml)
VARIABLES=$(jq -n '{
BROWSER_CLIENT_ID: "JS_SDK_E2E_BROWSER", BROWSER_REDIRECT_URIS: ["http://localhost:5173"],
REACT_CLIENT_ID: "JS_SDK_E2E_REACT", REACT_REDIRECT_URIS: ["http://localhost:5174"],
VUE_CLIENT_ID: "JS_SDK_E2E_VUE", VUE_REDIRECT_URIS: ["http://localhost:5175"],
NEXTJS_CLIENT_ID: "JS_SDK_E2E_NEXTJS", NEXTJS_CLIENT_SECRET: "e2e-nextjs-secret", NEXTJS_REDIRECT_URIS: ["http://localhost:3001"],
NUXT_CLIENT_ID: "JS_SDK_E2E_NUXT", NUXT_CLIENT_SECRET: "e2e-nuxt-secret", NUXT_REDIRECT_URIS: ["http://localhost:3002/api/auth/callback"], NUXT_POST_LOGOUT_REDIRECT_URIS: ["http://localhost:3002/"],
EXPRESS_CLIENT_ID: "JS_SDK_E2E_EXPRESS", EXPRESS_CLIENT_SECRET: "e2e-express-secret", EXPRESS_REDIRECT_URIS: ["http://localhost:3000/login"], EXPRESS_POST_LOGOUT_REDIRECT_URIS: ["http://localhost:3000/logout"],
NODE_CLIENT_ID: "JS_SDK_E2E_NODE", NODE_CLIENT_SECRET: "e2e-node-secret",
ALL_ORIGINS: ["http://localhost:5173", "http://localhost:5174", "http://localhost:5175", "http://localhost:3000", "http://localhost:3001", "http://localhost:3002"]
}')
RESPONSE_FILE=$(mktemp)
HTTP_STATUS=$(curl -sk -o "$RESPONSE_FILE" -w "%{http_code}" \
-X POST "https://localhost:8090/import" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-d "{\"content\": $CONTENT, \"variables\": $VARIABLES, \"options\": {\"upsert\": true}}")

if [ "$HTTP_STATUS" != "200" ]; then
echo "ERROR: import returned HTTP $HTTP_STATUS:"; cat "$RESPONSE_FILE"; echo ""; exit 1
fi
FAILED_COUNT=$(jq -r '.summary.failed // 0' "$RESPONSE_FILE")
if [ "$FAILED_COUNT" != "0" ]; then
echo "ERROR: import had $FAILED_COUNT failed resource(s):"; cat "$RESPONSE_FILE"; echo ""; exit 1
fi
rm -f "$RESPONSE_FILE"

- name: 🔄 Restart the server with security enabled
working-directory: ${{ steps.install.outputs.dist_home }}
run: |
# `pkill -f start.sh` only matches the wrapper script, not the server binary it launches
# in the foreground (a separate PID that SIGKILL to the wrapper never reaches) — kill by
# port instead, and confirm the old server is actually gone before starting a new one,
# since starting a replacement while the old process still holds the port would let the
# old (pre-import) server keep answering liveness checks and silently pass this step.
lsof -ti tcp:8090 | xargs -r kill -9
for i in $(seq 1 30); do
curl -skf https://localhost:8090/health/liveness > /dev/null 2>&1 || break
sleep 1
done
if curl -skf https://localhost:8090/health/liveness > /dev/null 2>&1; then
echo "ERROR: previous ThunderID server is still reachable after kill" && exit 1
fi

setsid ./start.sh &
for i in $(seq 1 60); do
curl -skf https://localhost:8090/health/liveness && exit 0
sleep 2
done
echo "ERROR: ThunderID server did not restart" && exit 1

- name: 📝 Write sample app .env files
# See the identically-named step in pr-builder.yml's `e2e` job for why this writes each
# .env directly instead of using the apps' own `prepare-dev.cjs --flow=redirect`.
run: |
NEXTJS_SECRET=$(openssl rand -base64 32)
NUXT_SECRET=$(openssl rand -base64 32)

write_env() {
local file=$1; shift
printf '%s\n' "$@" > "$file"
}

write_env samples/browser/quickstart/.env \
"VITE_THUNDERID_BASE_URL=https://localhost:8090" \
"VITE_THUNDERID_CLIENT_ID=JS_SDK_E2E_BROWSER"

write_env samples/react/quickstart/.env \
"VITE_THUNDERID_BASE_URL=https://localhost:8090" \
"VITE_THUNDERID_CLIENT_ID=JS_SDK_E2E_REACT"

write_env samples/vue/quickstart/.env \
"VITE_THUNDERID_BASE_URL=https://localhost:8090" \
"VITE_THUNDERID_CLIENT_ID=JS_SDK_E2E_VUE"

write_env samples/express/quickstart/.env \
"THUNDERID_BASE_URL=https://localhost:8090" \
"THUNDERID_CLIENT_ID=JS_SDK_E2E_EXPRESS" \
"THUNDERID_CLIENT_SECRET=e2e-express-secret" \
"NODE_TLS_REJECT_UNAUTHORIZED=0"

write_env samples/nextjs/quickstart/.env \
"NEXT_PUBLIC_THUNDERID_BASE_URL=https://localhost:8090" \
"NEXT_PUBLIC_THUNDERID_CLIENT_ID=JS_SDK_E2E_NEXTJS" \
"THUNDERID_CLIENT_SECRET=e2e-nextjs-secret" \
"THUNDERID_SECRET=${NEXTJS_SECRET}" \
"NODE_TLS_REJECT_UNAUTHORIZED=0"

write_env samples/nuxt/quickstart/.env \
"NUXT_PUBLIC_THUNDERID_BASE_URL=https://localhost:8090" \
"NUXT_PUBLIC_THUNDERID_CLIENT_ID=JS_SDK_E2E_NUXT" \
"THUNDERID_CLIENT_SECRET=e2e-nuxt-secret" \
"THUNDERID_SESSION_SECRET=${NUXT_SECRET}" \
"NODE_TLS_REJECT_UNAUTHORIZED=0"

- name: 🚀 Start sample apps
run: |
( cd samples/browser/quickstart && pnpm exec vite --port 5173 & )
( cd samples/react/quickstart && pnpm exec vite --port 5174 & )
( cd samples/vue/quickstart && pnpm exec vite --port 5175 & )
( cd samples/nextjs/quickstart && pnpm exec next dev -p 3001 & )
( cd samples/nuxt/quickstart && pnpm exec nuxt dev --port 3002 & )
( cd samples/express/quickstart && pnpm exec node --env-file-if-exists=.env index.mjs & )
for url in http://localhost:5173 http://localhost:5174 http://localhost:5175 http://localhost:3000 http://localhost:3001 http://localhost:3002; do
ready=0
for i in $(seq 1 60); do
curl -sf "$url" > /dev/null && ready=1 && break
sleep 2
done
if [ "$ready" != "1" ]; then
echo "ERROR: $url did not become ready after 120s" && exit 1
fi
done

- name: 🎭 Install Playwright browsers
working-directory: tests/e2e
run: npx playwright install --with-deps chromium

- name: 🧪 Run E2E tests
working-directory: tests/e2e
env:
SERVER_URL: https://localhost:8090
ADMIN_USERNAME: admin
ADMIN_PASSWORD: admin
ADMIN_TOKEN: ${{ steps.admin-token.outputs.token }}
TEST_USER_USERNAME: e2e-test-user
TEST_USER_PASSWORD: E2ePassword@123
BROWSER_APP_URL: http://localhost:5173
REACT_APP_URL: http://localhost:5174
VUE_APP_URL: http://localhost:5175
NEXTJS_APP_URL: http://localhost:3001
NUXT_APP_URL: http://localhost:3002
EXPRESS_APP_URL: http://localhost:3000
THUNDERID_BASE_URL: https://localhost:8090
THUNDERID_CLIENT_ID: JS_SDK_E2E_NODE
THUNDERID_CLIENT_SECRET: e2e-node-secret
NODE_TLS_REJECT_UNAUTHORIZED: "0"
run: npx playwright test

- name: 📤 Upload Playwright report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: playwright-report-nightly
path: tests/e2e/playwright-report/
retention-days: 30
Loading
Loading