teqo-backup/.github/workflows/ci.yml
Cursor Agent baa8f14f82
fix(ci): promote Vercel deploy to pt.jorgesolla.com.br
Disabling Git auto-deploy is correct for Actions-gated production. The
separate project toggle "Auto-assign Custom Production Domains" must stay
ON — when OFF, `vercel deploy --prod` only aliases *.vercel.app and leaves
the custom domain on an older Current (exactly what we saw after #122).

After each deploy, ensure-production-alias re-enables auto-assign if needed,
promotes the deployment, and verifies pt.jorgesolla.com.br is attached.
Also adds workflow_dispatch "Vercel promote production" for promote-without-rebuild.
2026-08-01 15:26:31 +00:00

314 lines
11 KiB
YAML

# 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 exec 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 30 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"
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"