Pipeline #83
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: Pipeline | |
| on: | |
| push: | |
| branches: | |
| - "**" | |
| pull_request: | |
| branches: | |
| - "**" | |
| schedule: | |
| - cron: "0 6 * * *" # notify discoveries (daily) | |
| - cron: "0 0 * * *" # unlock solarhealth (daily) | |
| - cron: "0 14 * * 6" # delete anomaly log (weekly) | |
| - cron: "0 21 * * 0" # codeql (weekly) | |
| workflow_dispatch: | |
| inputs: | |
| run_notify_discoveries: | |
| description: "Run discovery notification maintenance job" | |
| required: false | |
| default: false | |
| type: boolean | |
| run_unlock_solarhealth: | |
| description: "Run solarhealth unlock maintenance job" | |
| required: false | |
| default: false | |
| type: boolean | |
| run_delete_anomaly_log: | |
| description: "Run anomaly log cleanup maintenance job" | |
| required: false | |
| default: false | |
| type: boolean | |
| run_community_event_notify: | |
| description: "Run community event push notification job" | |
| required: false | |
| default: false | |
| type: boolean | |
| community_event_title: | |
| description: "Community event notification title" | |
| required: false | |
| default: "Community event" | |
| type: string | |
| community_event_message: | |
| description: "Community event notification message" | |
| required: false | |
| default: "A new community event is live." | |
| type: string | |
| community_event_url: | |
| description: "URL to open from notification" | |
| required: false | |
| default: "/game" | |
| type: string | |
| community_event_dry_run: | |
| description: "If true, do not send notifications; only produce a report." | |
| required: false | |
| default: true | |
| type: boolean | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: pipeline-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| test_suite: | |
| if: github.event_name != 'schedule' | |
| runs-on: ubuntu-latest | |
| env: | |
| NODE_ENV: test | |
| SKIP_USER_CREATION_TESTS: "true" | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| cache: "yarn" | |
| - name: Install dependencies | |
| run: | | |
| yarn lock:check | |
| yarn install --frozen-lockfile | |
| - name: Setup Supabase CLI | |
| uses: supabase/setup-cli@v1 | |
| with: | |
| version: latest | |
| - name: Start local Supabase | |
| run: supabase start | |
| - name: Normalize local test env defaults | |
| run: | | |
| SUPABASE_URL="${SUPABASE_URL:-${{ vars.SUPABASE_URL }}}" | |
| NEXT_PUBLIC_SUPABASE_URL="${NEXT_PUBLIC_SUPABASE_URL:-${{ vars.NEXT_PUBLIC_SUPABASE_URL }}}" | |
| NEXT_PUBLIC_SUPABASE_ANON_KEY="${NEXT_PUBLIC_SUPABASE_ANON_KEY:-${{ vars.NEXT_PUBLIC_SUPABASE_ANON_KEY }}}" | |
| SUPABASE_SERVICE_ROLE_KEY="${SUPABASE_SERVICE_ROLE_KEY:-${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}}" | |
| DATABASE_URL="${DATABASE_URL:-}" | |
| SUPABASE_URL="${SUPABASE_URL:-http://127.0.0.1:54321}" | |
| NEXT_PUBLIC_SUPABASE_URL="${NEXT_PUBLIC_SUPABASE_URL:-http://127.0.0.1:54321}" | |
| STATUS_ENV="$(supabase status -o env 2>/dev/null || true)" | |
| if [ -n "${STATUS_ENV}" ]; then | |
| LOCAL_DB_URL="$(printf '%s\n' "${STATUS_ENV}" | sed -n 's/^DB_URL=//p' | tail -n1 | sed 's/^"//; s/"$//')" | |
| LOCAL_PUBLISHABLE="$(printf '%s\n' "${STATUS_ENV}" | sed -n 's/^ANON_KEY=//p' | tail -n1 | sed 's/^"//; s/"$//')" | |
| LOCAL_SECRET="$(printf '%s\n' "${STATUS_ENV}" | sed -n 's/^SERVICE_ROLE_KEY=//p' | tail -n1 | sed 's/^"//; s/"$//')" | |
| if [ -z "${LOCAL_PUBLISHABLE}" ]; then | |
| LOCAL_PUBLISHABLE="$(printf '%s\n' "${STATUS_ENV}" | sed -n 's/^PUBLISHABLE_KEY=//p' | tail -n1 | sed 's/^"//; s/"$//')" | |
| fi | |
| if [ -z "${LOCAL_SECRET}" ]; then | |
| LOCAL_SECRET="$(printf '%s\n' "${STATUS_ENV}" | sed -n 's/^SECRET_KEY=//p' | tail -n1 | sed 's/^"//; s/"$//')" | |
| fi | |
| fi | |
| if [ -z "${LOCAL_PUBLISHABLE}" ] || [ -z "${LOCAL_SECRET}" ]; then | |
| STATUS_TEXT="$(supabase status)" | |
| LOCAL_PUBLISHABLE="$(printf '%s\n' "${STATUS_TEXT}" | sed -n 's/^| Publishable | \(.*\) |$/\1/p' | tail -n1)" | |
| LOCAL_SECRET="$(printf '%s\n' "${STATUS_TEXT}" | sed -n 's/^| Secret | \(.*\) |$/\1/p' | tail -n1)" | |
| fi | |
| DATABASE_URL="${DATABASE_URL:-$LOCAL_DB_URL}" | |
| NEXT_PUBLIC_SUPABASE_ANON_KEY="${NEXT_PUBLIC_SUPABASE_ANON_KEY:-$LOCAL_PUBLISHABLE}" | |
| SUPABASE_SERVICE_ROLE_KEY="${SUPABASE_SERVICE_ROLE_KEY:-$LOCAL_SECRET}" | |
| if [ -z "${DATABASE_URL}" ] || [ -z "${NEXT_PUBLIC_SUPABASE_ANON_KEY}" ] || [ -z "${SUPABASE_SERVICE_ROLE_KEY}" ]; then | |
| echo "Unable to resolve local Supabase database/auth env from supabase status." >&2 | |
| exit 1 | |
| fi | |
| echo "DATABASE_URL=${DATABASE_URL}" >> "$GITHUB_ENV" | |
| echo "SUPABASE_URL=${SUPABASE_URL}" >> "$GITHUB_ENV" | |
| echo "NEXT_PUBLIC_SUPABASE_URL=${NEXT_PUBLIC_SUPABASE_URL}" >> "$GITHUB_ENV" | |
| echo "NEXT_PUBLIC_SUPABASE_ANON_KEY=${NEXT_PUBLIC_SUPABASE_ANON_KEY}" >> "$GITHUB_ENV" | |
| echo "SUPABASE_SERVICE_ROLE_KEY=${SUPABASE_SERVICE_ROLE_KEY}" >> "$GITHUB_ENV" | |
| - name: Run unit tests | |
| run: yarn test:unit | |
| - name: Run unit tests with coverage | |
| run: yarn test:unit --coverage | |
| - name: Run lint | |
| run: yarn lint | |
| - name: Run E2E tests (minimal smoke for regular pushes) | |
| if: github.event_name == 'push' && github.ref_name != 'main' | |
| run: yarn test:e2e:smoke:minimal | |
| - name: Run E2E tests (full suite for main branch pushes) | |
| if: github.event_name == 'push' && github.ref_name == 'main' | |
| run: yarn test:e2e | |
| - name: Run E2E tests (full suite for PR to main) | |
| if: > | |
| github.event_name == 'pull_request' && github.base_ref == 'main' | |
| run: yarn test:e2e | |
| - name: Upload e2e coverage | |
| if: > | |
| always() && ( | |
| (github.event_name == 'pull_request' && github.base_ref == 'main') | |
| ) | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: e2e-coverage | |
| path: | | |
| coverage-e2e | |
| .nyc_output | |
| - name: Upload Cypress artifacts | |
| if: > | |
| always() && ( | |
| (github.event_name == 'push' && github.ref_name == 'main') || | |
| (github.event_name == 'pull_request' && github.base_ref == 'main') | |
| ) | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: cypress-artifacts | |
| path: | | |
| cypress/videos | |
| cypress/screenshots | |
| build_app: | |
| if: github.event_name != 'schedule' | |
| needs: test_suite | |
| runs-on: ubuntu-latest | |
| env: | |
| NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} | |
| NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }} | |
| VAPID_PUBLIC_KEY: ${{ secrets.VAPID_PUBLIC_KEY }} | |
| VAPID_PRIVATE_KEY: ${{ secrets.VAPID_PRIVATE_KEY }} | |
| DATABASE_URL: "postgresql://placeholder:placeholder@localhost:5432/placeholder" | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| cache: "yarn" | |
| - name: Install dependencies | |
| run: | | |
| yarn lock:check | |
| yarn install --frozen-lockfile | |
| - name: Build app | |
| run: | | |
| rm -rf .next | |
| yarn build | |
| - name: Check bundle budgets | |
| run: yarn bundle:check | |
| deploy_preview: | |
| if: github.event_name == 'push' && github.ref_name != 'main' | |
| needs: build_app | |
| runs-on: ubuntu-latest | |
| env: | |
| VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} | |
| VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| cache: "yarn" | |
| - name: Pull Vercel Environment Information | |
| run: npx vercel@latest pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} --scope=${{ secrets.VERCEL_ORG_ID }} | |
| - name: Build Project Artifacts | |
| run: | | |
| ATTEMPTS=3 | |
| for ATTEMPT in $(seq 1 "$ATTEMPTS"); do | |
| echo "Vercel preview build attempt ${ATTEMPT}/${ATTEMPTS}" | |
| if npx vercel@latest build --yes --token=${{ secrets.VERCEL_TOKEN }} --scope=${{ secrets.VERCEL_ORG_ID }}; then | |
| exit 0 | |
| fi | |
| if [ "$ATTEMPT" -lt "$ATTEMPTS" ]; then | |
| echo "Build failed (likely transient). Retrying in 20s..." | |
| sleep 20 | |
| fi | |
| done | |
| echo "Vercel preview build failed after ${ATTEMPTS} attempts." | |
| exit 1 | |
| - name: Deploy Project Artifacts to Vercel Staging | |
| run: | | |
| ATTEMPTS=3 | |
| for ATTEMPT in $(seq 1 "$ATTEMPTS"); do | |
| echo "Vercel preview deploy attempt ${ATTEMPT}/${ATTEMPTS}" | |
| if npx vercel@latest deploy --prebuilt --yes --token=${{ secrets.VERCEL_TOKEN }} --scope=${{ secrets.VERCEL_ORG_ID }}; then | |
| exit 0 | |
| fi | |
| if [ "$ATTEMPT" -lt "$ATTEMPTS" ]; then | |
| echo "Deploy failed (likely transient). Retrying in 20s..." | |
| sleep 20 | |
| fi | |
| done | |
| echo "Vercel preview deploy failed after ${ATTEMPTS} attempts." | |
| exit 1 | |
| deploy_production: | |
| if: github.event_name == 'push' && github.ref_name == 'main' | |
| needs: build_app | |
| runs-on: ubuntu-latest | |
| env: | |
| VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} | |
| VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "22" | |
| cache: "yarn" | |
| - name: Pull Vercel Environment Information | |
| run: npx vercel@latest pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} --scope=${{ secrets.VERCEL_ORG_ID }} | |
| - name: Build Project Artifacts | |
| run: | | |
| ATTEMPTS=3 | |
| for ATTEMPT in $(seq 1 "$ATTEMPTS"); do | |
| echo "Vercel production build attempt ${ATTEMPT}/${ATTEMPTS}" | |
| if npx vercel@latest build --prod --yes --token=${{ secrets.VERCEL_TOKEN }} --scope=${{ secrets.VERCEL_ORG_ID }}; then | |
| exit 0 | |
| fi | |
| if [ "$ATTEMPT" -lt "$ATTEMPTS" ]; then | |
| echo "Build failed (likely transient). Retrying in 20s..." | |
| sleep 20 | |
| fi | |
| done | |
| echo "Vercel production build failed after ${ATTEMPTS} attempts." | |
| exit 1 | |
| - name: Deploy Project Artifacts to Vercel | |
| run: | | |
| ATTEMPTS=3 | |
| for ATTEMPT in $(seq 1 "$ATTEMPTS"); do | |
| echo "Vercel production deploy attempt ${ATTEMPT}/${ATTEMPTS}" | |
| if npx vercel@latest deploy --prebuilt --prod --yes --token=${{ secrets.VERCEL_TOKEN }} --scope=${{ secrets.VERCEL_ORG_ID }}; then | |
| exit 0 | |
| fi | |
| if [ "$ATTEMPT" -lt "$ATTEMPTS" ]; then | |
| echo "Deploy failed (likely transient). Retrying in 20s..." | |
| sleep 20 | |
| fi | |
| done | |
| echo "Vercel production deploy failed after ${ATTEMPTS} attempts." | |
| exit 1 | |
| notify_unclassified_discoveries: | |
| if: > | |
| (github.event_name == 'schedule' && github.event.schedule == '0 6 * * *') || | |
| (github.event_name == 'workflow_dispatch' && inputs.run_notify_discoveries == true) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: "yarn" | |
| - name: Install dependencies | |
| run: | | |
| yarn lock:check | |
| yarn install --frozen-lockfile | |
| - name: Send discovery notifications | |
| env: | |
| SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} | |
| SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} | |
| NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} | |
| NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }} | |
| NEXT_PUBLIC_VAPID_PUBLIC_KEY: ${{ secrets.VAPID_PUBLIC_KEY }} | |
| VAPID_PRIVATE_KEY: ${{ secrets.VAPID_PRIVATE_KEY }} | |
| SUPABASE_DB_URL: ${{ secrets.SUPABASE_DB_URL }} | |
| run: node --experimental-strip-types scripts/notify-unclassified-discoveries.ts | |
| unlock_solarhealth_anomalies: | |
| if: > | |
| (github.event_name == 'schedule' && github.event.schedule == '0 0 * * *') || | |
| (github.event_name == 'workflow_dispatch' && inputs.run_unlock_solarhealth == true) || | |
| (github.event_name == 'push' && contains(github.event.head_commit.message, 'SSM-257')) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: "yarn" | |
| - name: Install dependencies | |
| run: | | |
| yarn lock:check | |
| yarn install --frozen-lockfile | |
| - name: Unlock SolarHealth anomalies | |
| env: | |
| SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} | |
| SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} | |
| run: node --experimental-strip-types scripts/unlock-solarhealth-anomalies.ts | |
| delete_push_anomaly_log: | |
| if: > | |
| (github.event_name == 'schedule' && github.event.schedule == '0 14 * * 6') || | |
| (github.event_name == 'workflow_dispatch' && inputs.run_delete_anomaly_log == true) | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version: "1.21" | |
| - name: Install dependencies | |
| run: go mod tidy | |
| - name: Delete push anomaly log | |
| env: | |
| SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} | |
| SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} | |
| run: go run scripts/delete_push_anomaly_log.go | |
| community_event_notify: | |
| if: github.event_name == 'workflow_dispatch' && inputs.run_community_event_notify == true | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| cache: "yarn" | |
| - name: Install dependencies | |
| run: | | |
| yarn lock:check | |
| yarn install --frozen-lockfile | |
| - name: Send community event push notification (or dry run) | |
| env: | |
| SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }} | |
| SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} | |
| NEXT_PUBLIC_VAPID_PUBLIC_KEY: ${{ secrets.VAPID_PUBLIC_KEY }} | |
| VAPID_PRIVATE_KEY: ${{ secrets.VAPID_PRIVATE_KEY }} | |
| COMMUNITY_EVENT_TITLE: ${{ inputs.community_event_title }} | |
| COMMUNITY_EVENT_MESSAGE: ${{ inputs.community_event_message }} | |
| COMMUNITY_EVENT_URL: ${{ inputs.community_event_url }} | |
| DRY_RUN: ${{ inputs.community_event_dry_run }} | |
| REPORT_PATH: community-event-report.json | |
| run: node --experimental-strip-types scripts/notify-community-event.ts | |
| - name: Upload community event report | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: community-event-report | |
| path: community-event-report.json |