# Post-merge verifier on `main`: always the FULL suite (no affected skips). # When green, deploys production via `vercel deploy --prod` (Vercel Git builds # are ignored — see scripts/vercel-ignore-build.sh). If HEAD moved while this # run was in flight, `requeue` dispatches another run for the new tip. name: CI on: push: branches: [main] workflow_dispatch: concurrency: group: main-verify cancel-in-progress: false permissions: contents: read actions: write env: DATABASE_URL: postgresql://teqo:teqo@localhost:5432/teqo_test PAYLOAD_SECRET: test-only-secret-not-used-in-production NEXT_PUBLIC_SITE_URL: http://localhost:3000 jobs: static: runs-on: ubuntu-latest timeout-minutes: 20 steps: - uses: actions/checkout@v5 - uses: pnpm/action-setup@v6 - uses: actions/setup-node@v5 with: node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile - run: node scripts/check-test-locations.mjs - run: pnpm lint - run: pnpm format:check - run: pnpm typecheck - run: pnpm knip - run: pnpm check:cycles - run: pnpm test:unit - name: Cancel workflow on failure if: failure() uses: ./.github/actions/cancel-workflow-on-failure int: runs-on: ubuntu-latest timeout-minutes: 25 services: postgres: image: postgres:17-alpine env: POSTGRES_USER: teqo POSTGRES_PASSWORD: teqo POSTGRES_DB: teqo_test ports: - 5432:5432 options: >- --health-cmd "pg_isready -U teqo" --health-interval 5s --health-timeout 5s --health-retries 12 steps: - uses: actions/checkout@v5 - uses: pnpm/action-setup@v6 - uses: actions/setup-node@v5 with: node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile - run: pnpm migrate - run: pnpm db:seed:minimal - run: pnpm test:int - name: Cancel workflow on failure if: failure() uses: ./.github/actions/cancel-workflow-on-failure build: runs-on: ubuntu-latest timeout-minutes: 25 services: postgres: image: postgres:17-alpine env: POSTGRES_USER: teqo POSTGRES_PASSWORD: teqo POSTGRES_DB: teqo_test ports: - 5432:5432 options: >- --health-cmd "pg_isready -U teqo" --health-interval 5s --health-timeout 5s --health-retries 12 steps: - uses: actions/checkout@v5 - uses: pnpm/action-setup@v6 - uses: actions/setup-node@v5 with: node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile - name: Build (against the local service database) run: pnpm build - name: Cancel workflow on failure if: failure() uses: ./.github/actions/cancel-workflow-on-failure e2e: runs-on: ubuntu-latest timeout-minutes: 40 services: postgres: image: postgres:17-alpine env: POSTGRES_USER: teqo POSTGRES_PASSWORD: teqo POSTGRES_DB: teqo_test ports: - 5432:5432 options: >- --health-cmd "pg_isready -U teqo" --health-interval 5s --health-timeout 5s --health-retries 12 steps: - uses: actions/checkout@v5 - uses: pnpm/action-setup@v6 - uses: actions/setup-node@v5 with: node-version: 24 cache: pnpm - run: pnpm install --frozen-lockfile - name: Install Playwright chromium run: pnpm exec playwright install --with-deps chromium - run: pnpm migrate - run: pnpm db:seed:minimal - name: Build for e2e run: pnpm build env: NEXT_DIST_DIR: .next/e2e - name: E2E tests (full suite) run: pnpm test:e2e - name: Cancel workflow on failure if: failure() uses: ./.github/actions/cancel-workflow-on-failure checks: name: checks runs-on: ubuntu-latest needs: [static, int, build, e2e] if: always() steps: - name: All main verifier checks green env: NEEDS: ${{ toJSON(needs) }} run: | set -euo pipefail echo "$NEEDS" | jq -e ' to_entries | all(.value.result == "success" or .value.result == "skipped") ' deploy: runs-on: ubuntu-latest needs: [checks] if: needs.checks.result == 'success' # pull → local `vercel build` (pnpm install + Next/Payload build against prod env) → upload. timeout-minutes: 35 outputs: stale: ${{ steps.tip.outputs.stale || 'false' }} cooldown: ${{ steps.cooldown.outputs.active || 'false' }} wait_seconds: ${{ steps.cooldown.outputs.wait_seconds || '0' }} env: VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }} steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Guard stale HEAD id: tip run: | set -euo pipefail git fetch origin main --quiet tip=$(git rev-parse origin/main) if [ "$tip" != "$GITHUB_SHA" ]; then echo "Skip deploy: origin/main is $tip, this run verified $GITHUB_SHA" echo "stale=true" >> "$GITHUB_OUTPUT" exit 0 fi echo "stale=false" >> "$GITHUB_OUTPUT" # Cooldown script is plain Node — no pnpm. Disable package-manager-cache # here: setup-node@v5 defaults to true and looks for pnpm before action-setup. - uses: actions/setup-node@v5 if: steps.tip.outputs.stale != 'true' with: node-version: 24 package-manager-cache: false - name: Require Vercel credentials if: steps.tip.outputs.stale != 'true' run: | set -euo pipefail missing=() [ -z "${VERCEL_TOKEN:-}" ] && missing+=(VERCEL_TOKEN) [ -z "${VERCEL_ORG_ID:-}" ] && missing+=(VERCEL_ORG_ID) [ -z "${VERCEL_PROJECT_ID:-}" ] && missing+=(VERCEL_PROJECT_ID) if [ "${#missing[@]}" -gt 0 ]; then echo "::error::Missing repo secrets for gated deploy: ${missing[*]}. Add them under Settings → Secrets, then re-run this workflow." exit 1 fi # Fail closed on obvious paste mistakes (must be team_/prj_ from .vercel/project.json). case "$VERCEL_ORG_ID" in team_*) ;; *) echo "::error::VERCEL_ORG_ID must look like team_… (from .vercel/project.json after vercel link)." exit 1 ;; esac case "$VERCEL_PROJECT_ID" in prj_*) ;; *) echo "::error::VERCEL_PROJECT_ID must look like prj_… (from .vercel/project.json after vercel link)." exit 1 ;; esac - name: Check deploy cooldown id: cooldown if: steps.tip.outputs.stale != 'true' run: node scripts/vercel-deploy-cooldown.mjs - name: Log deploy deferral if: steps.tip.outputs.stale != 'true' && steps.cooldown.outputs.active == 'true' run: | echo "Deploy deferred: last production deploy is younger than 15 minutes." echo "Requeue will retry in ${{ steps.cooldown.outputs.wait_seconds }}s." # `vercel build` shells out to pnpm (packageManager in package.json); without # these steps the runner only has npm → spawn pnpm ENOENT. - uses: pnpm/action-setup@v6 if: steps.tip.outputs.stale != 'true' && steps.cooldown.outputs.active != 'true' - uses: actions/setup-node@v5 if: steps.tip.outputs.stale != 'true' && steps.cooldown.outputs.active != 'true' with: node-version: 24 cache: pnpm - name: Install Vercel CLI if: steps.tip.outputs.stale != 'true' && steps.cooldown.outputs.active != 'true' run: npm install --global vercel@55 - name: Deploy production if: steps.tip.outputs.stale != 'true' && steps.cooldown.outputs.active != 'true' run: | set -euo pipefail # Workflow-level env points at the CI teqo_test service. Drop it so # `vercel build` uses `.vercel/.env.production.local` from `vercel pull` # (otherwise Payload migrate hits 127.0.0.1:5432 → ECONNREFUSED). unset DATABASE_URL PAYLOAD_SECRET NEXT_PUBLIC_SITE_URL # Canonical CI link (Vercel KB): env VERCEL_*_ID + pull → build → deploy --prebuilt. # "Could not retrieve Project Settings" usually means bad/expired token or wrong IDs (403). vercel pull --yes --environment=production --token="$VERCEL_TOKEN" vercel build --prod --token="$VERCEL_TOKEN" # stdout is always the deployment URL (CLI contract). Capture it — do not # pipe the whole CLI log or the exit code / URL get lost. deploy_url=$(vercel deploy --prebuilt --prod --archive=tgz --token="$VERCEL_TOKEN") echo "Deployed: $deploy_url" # Git auto-deploy OFF is intentional. If "Auto-assign Custom Production # Domains" is also OFF, --prod only stages *.vercel.app and leaves # pt.jorgesolla.com.br on the previous Current — promote + verify here. node scripts/vercel-ensure-production-alias.mjs "$deploy_url" # Docs: promote staged production, then alias (Git Integration is OFF by design). vercel promote "$deploy_url" --token="$VERCEL_TOKEN" --scope="$VERCEL_ORG_ID" || true # `--yes` is only valid for `alias rm`, not `alias set` (CLI 55). vercel alias set "$deploy_url" pt.jorgesolla.com.br --token="$VERCEL_TOKEN" --scope="$VERCEL_ORG_ID" requeue: runs-on: ubuntu-latest needs: [checks, deploy] if: always() timeout-minutes: 40 permissions: actions: write contents: read steps: - uses: actions/checkout@v5 with: fetch-depth: 0 - name: Re-dispatch if main advanced or deploy deferred env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} DEPLOY_RESULT: ${{ needs.deploy.result }} DEPLOY_COOLDOWN: ${{ needs.deploy.outputs.cooldown }} DEPLOY_WAIT_SECONDS: ${{ needs.deploy.outputs.wait_seconds }} run: | set -euo pipefail git fetch origin main --quiet tip=$(git rev-parse origin/main) if [ "$tip" != "$GITHUB_SHA" ]; then echo "origin/main moved to $tip (verified $GITHUB_SHA) — requeue ci.yml" gh workflow run ci.yml --ref main exit 0 fi if [ "${DEPLOY_RESULT:-}" != "success" ]; then echo "Deploy job did not succeed (${DEPLOY_RESULT:-skipped}) — idle" exit 0 fi if [ "${DEPLOY_COOLDOWN:-}" = "true" ]; then wait="${DEPLOY_WAIT_SECONDS:-0}" echo "Deploy deferred by cooldown — waiting ${wait}s then requeue ci.yml" sleep "$wait" git fetch origin main --quiet tip=$(git rev-parse origin/main) if [ "$tip" != "$GITHUB_SHA" ]; then echo "main advanced during cooldown wait ($tip) — requeue for new tip" else echo "Cooldown elapsed for $GITHUB_SHA — requeue ci.yml" fi gh workflow run ci.yml --ref main exit 0 fi echo "HEAD still $GITHUB_SHA — idle"