From 85a5091bd6b106ea20d6bf7bec5bf3b310e51056 Mon Sep 17 00:00:00 2001 From: Osvaldo Demo Date: Sun, 14 Jun 2026 16:02:08 -0300 Subject: [PATCH 1/4] Pin Node 24.1.0 in netlify.toml for the Netlify build path Adds a `[build.environment]` block setting `NODE_VERSION = "24.1.0"` so the Netlify-side build (now secondary to GitHub Actions deploy) uses the same Node we declare in .nvmrc and package.json#engines. This is defence-in-depth: even if the primary GH Actions deploy path is bypassed and a stray Netlify auto-build runs, it boots the right Node. Context: the June 2026 `noble` build-image rollout silently broke Netlify auto-deploys for ~7 weeks. Subsequent work in this branch moves production deploys to GitHub Actions so the noble incompatibility is no longer in the critical path. Co-authored-by: Cursor --- netlify.toml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/netlify.toml b/netlify.toml index 13f9f39..954d84a 100644 --- a/netlify.toml +++ b/netlify.toml @@ -3,6 +3,16 @@ publish = "dist" functions = "netlify/functions" +# Pin runtime versions for the (now-secondary) Netlify-side build path. +# The primary deploy path is the GitHub Actions `deploy-production` job in +# `.github/workflows/ci.yml`, which runs `netlify deploy --prod` from a +# pinned Node 24.1.0 runner. Keeping these here is defence-in-depth so a +# stray Netlify auto-build does not silently regress on a newer image +# (the June 2026 `noble` rollout took our auto-deploys offline for 7 weeks +# before this pipeline existed). +[build.environment] + NODE_VERSION = "24.1.0" + [[redirects]] from = "/api/*" # Function must not be named "api": it collides with the /api source directory From 049e006d5cd8152938050b4cca4699bbb55cda80 Mon Sep 17 00:00:00 2001 From: Osvaldo Demo Date: Sun, 14 Jun 2026 16:02:26 -0300 Subject: [PATCH 2/4] Add Netlify build pre-merge guard to CI Adds a `netlify-build-check` job that runs `netlify build --offline` on every PR after `verify` passes. This simulates Netlify Build's pipeline (executing netlify.toml's build command and bundling functions) so we catch Netlify-Build-specific regressions before they merge to main. Why we need this: the June 2026 `noble` image rollout silently broke auto-deploys for ~7 weeks because nothing failed in our PR pipeline. Running `netlify build` in CI surfaces that class of failure on the PR. Co-authored-by: Cursor --- .github/workflows/ci.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 459eefb..5a1a8e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,6 +47,30 @@ jobs: - name: Verify project run: npm run verify + # Pre-merge guard: simulate the Netlify Build pipeline so we catch + # Netlify-Build-specific failures (image incompatibilities, functions + # bundling, plugin issues) on PRs instead of in production. This is + # independent of where the actual deploy runs. + netlify-build-check: + name: Netlify build (pre-merge guard) + needs: verify + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Run netlify build (offline) + run: npx -y netlify-cli@latest build --offline + # secrets cannot appear in job-level `if`. Expose a boolean via env + step output. check-production-migrate: name: Check production database secret From 0223efe5158f2bf329359e7fefb06fa722f81d12 Mon Sep 17 00:00:00 2001 From: Osvaldo Demo Date: Sun, 14 Jun 2026 16:03:33 -0300 Subject: [PATCH 3/4] Deploy production to Netlify from GitHub Actions Adds a `deploy-production` job that becomes the source of truth for production deploys. The job: 1. Waits for `verify` and `netlify-build-check` to pass 2. Waits for `migrate-production` (when configured) so the schema is ready before the new Lambda boots 3. Builds with Node pinned via `.nvmrc` (`actions/setup-node@v4`) 4. Generates the Prisma client against the current schema 5. Runs `netlify deploy --prod --skip-functions-cache` so the function bundle is always fresh (no stale cached Prisma client) 6. Smoke-checks the live API and fails the job if it does not 200 Gated by NETLIFY_AUTH_TOKEN + NETLIFY_SITE_ID via a `check-production-deploy` job (secrets cannot be referenced in job-level `if:`). Background: Netlify's own git auto-deploy has been failing silently on the `noble` build image since the June 2026 rollout, leaving production on the April 28 Lambda for ~7 weeks. Moving the deploy into GitHub Actions gives us full logs, pinned Node, and a built-in smoke check, so a future Netlify image regression no longer takes the site down. Requires GitHub secrets: NETLIFY_AUTH_TOKEN, NETLIFY_SITE_ID (both set out-of-band when this commit lands). Co-authored-by: Cursor --- .github/workflows/ci.yml | 99 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a1a8e7..0d97573 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -129,3 +129,102 @@ jobs: env: DATABASE_URL: ${{ secrets.PRODUCTION_DATABASE_URL }} run: npm run db:seed + + # Mirror of `check-production-migrate`: secrets cannot be referenced in + # job-level `if`, so we gate `deploy-production` on a boolean derived + # here from the presence of NETLIFY_AUTH_TOKEN + NETLIFY_SITE_ID. + check-production-deploy: + name: Check production deploy secrets + needs: verify + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + outputs: + run_deploy: ${{ steps.gate.outputs.run_deploy }} + steps: + - id: gate + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + run: | + if [ -n "${NETLIFY_AUTH_TOKEN}" ] && [ -n "${NETLIFY_SITE_ID}" ]; then + echo "run_deploy=true" >> "$GITHUB_OUTPUT" + else + echo "run_deploy=false" >> "$GITHUB_OUTPUT" + fi + + # Production deploy: build with pinned Node, ship to Netlify directly, + # then verify the live endpoint. Replaces Netlify's git auto-deploy as + # the source of truth so noble-image / Prisma-binary regressions in + # Netlify Build no longer block production releases. + deploy-production: + name: Deploy to Netlify production + needs: [verify, netlify-build-check, migrate-production, check-production-deploy] + # `migrate-production` is conditional. `always()` + status checks let + # the deploy run when migrate was skipped (no production DB secret) + # but block it when migrate failed. + if: >- + always() && + github.event_name == 'push' && + github.ref == 'refs/heads/main' && + needs.verify.result == 'success' && + needs.netlify-build-check.result == 'success' && + needs.check-production-deploy.outputs.run_deploy == 'true' && + (needs.migrate-production.result == 'success' || needs.migrate-production.result == 'skipped') + runs-on: ubuntu-latest + timeout-minutes: 15 + concurrency: + group: production-deploy-main + cancel-in-progress: false + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version-file: .nvmrc + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Generate Prisma client + run: npm run db:generate + + - name: Build site + run: npm run build + + - name: Deploy to Netlify (prod) + # --skip-functions-cache forces a fresh function bundle so the + # deployed Prisma client always matches the current schema. The + # earlier deploy-race incident happened because Netlify reused a + # cached function bundle generated under a stale schema. + run: | + npx -y netlify-cli@latest deploy \ + --prod \ + --dir dist \ + --functions netlify/functions \ + --skip-functions-cache \ + --message "ci: ${GITHUB_SHA::7} (${GITHUB_REF_NAME})" + + - name: Verify live API + # Smoke-check: hit a public endpoint to make sure the new Lambda + # bundle actually serves traffic. Catches the class of failure + # where deploy reports success but the function 500s on cold + # start (e.g. Prisma client + DB column mismatch). + run: | + set -e + for attempt in 1 2 3 4 5; do + code=$(curl -s -o /tmp/body.json -w '%{http_code}' \ + "https://prode.grupoeglu.net/api/tournaments?status=active,upcoming") + echo "attempt $attempt: HTTP $code" + if [ "$code" = "200" ]; then exit 0; fi + sleep 5 + done + echo 'Live verification failed. Last body:' + head -c 500 /tmp/body.json + exit 1 From 7a9469aa70709101786dd8cde8539f3c4a92bd3c Mon Sep 17 00:00:00 2001 From: Osvaldo Demo Date: Sun, 14 Jun 2026 16:04:39 -0300 Subject: [PATCH 4/4] Document GH Actions deploy pipeline and ops rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ARCHITECTURE.md §15.4: new section describing the verify → netlify-build-check → migrate → deploy pipeline, the required GitHub secrets, the --skip-functions-cache requirement, and the live-API smoke check - ARCHITECTURE.md "Destructive Migrations And The Deploy Race": updated to reflect that migrate and deploy are now sequenced in the same workflow, noting the residual race only exists while Netlify's git auto-deploy stays on as a fallback - AGENTS.md §3 "Production Deploy Pipeline": ops rules for deploys, rollback guidance (prefer restoreSiteDeploy), and the required GitHub secrets contract Co-authored-by: Cursor --- AGENTS.md | 30 ++++++++++++++++++++++++ docs/ARCHITECTURE.md | 54 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 73 insertions(+), 11 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5df56b3..049ab43 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -96,6 +96,36 @@ migration first (column added as nullable or with a default), wait for the migrate job to finish, and only then ship the code that treats the column as required. +### Production Deploy Pipeline + +Production deploys are owned by GitHub Actions (`deploy-production` +job in `.github/workflows/ci.yml`), not Netlify's git auto-deploy. + +Rules: + +- never deploy by pushing a release branch through Netlify's UI +- the only supported deploy path on `main` is the GitHub Actions + workflow; if it is skipped (missing secrets, gated false), do not + shadow it with a manual `netlify deploy` from a workstation +- the deploy step uses `--skip-functions-cache` so the bundled + Prisma client always matches the current schema; do not remove + that flag +- the deploy step ends with a live-API smoke check; if it fails, + the workflow fails red — investigate before re-running +- if you need to roll back, prefer `netlify api restoreSiteDeploy` + via the Netlify CLI against a known-good deploy id rather than + pushing a revert commit, which would re-run the migrate job too + +Required GitHub secrets: + +- `PRODUCTION_DATABASE_URL` (existing, gates migrate) +- `NETLIFY_AUTH_TOKEN` (gates deploy) +- `NETLIFY_SITE_ID` (gates deploy) + +If `NETLIFY_AUTH_TOKEN` or `NETLIFY_SITE_ID` is missing, the deploy +job is skipped cleanly; the workflow stays green, and Netlify's own +git auto-deploy (still enabled as a fallback) takes over. + ## 4. API And Data Safety When changing API behavior: diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f802395..263f9f9 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -741,19 +741,51 @@ Current automation contract: This keeps the migration path explicit and checked-in while allowing repositories without configured production secrets to keep using the same CI workflow safely. +### 15.4 Production Deploy Pipeline + +Production code is shipped to Netlify from GitHub Actions, not via Netlify's git auto-deploy. + +Why: Netlify's `noble` build image rollout (June 2026) broke our +auto-deploy for ~7 weeks. We had no logs and no signal, so the live +Lambda silently stayed on the April 28 build while migrations kept +running on `main`. Owning the deploy from GitHub Actions gives us +pinned Node, full build logs, and a smoke check. + +Pipeline contract (`.github/workflows/ci.yml`): + +| Job | Trigger | Purpose | +|---|---|---| +| `verify` | every push + PR | lint, validate, tests, build | +| `netlify-build-check` | every push + PR (after `verify`) | runs `netlify build --offline` to catch Netlify-Build-specific failures on the PR, not in prod | +| `check-production-migrate` | push to `main` | gates the migrate job on `PRODUCTION_DATABASE_URL` | +| `migrate-production` | push to `main` (when gated true) | `npm run db:migrate:deploy` + `npm run db:seed` | +| `check-production-deploy` | push to `main` | gates the deploy job on `NETLIFY_AUTH_TOKEN` + `NETLIFY_SITE_ID` | +| `deploy-production` | push to `main` (when gated true) | builds, generates Prisma client, `netlify deploy --prod --skip-functions-cache`, then smoke-checks the live API | + +Required GitHub secrets: + +- `PRODUCTION_DATABASE_URL` — gates the migrate path +- `NETLIFY_AUTH_TOKEN` — personal access token with deploy scope +- `NETLIFY_SITE_ID` — Netlify project id + +Notes: + +- `--skip-functions-cache` is mandatory: the older "Deploying functions from cache" path can publish a function bundle whose Prisma client predates the current schema. +- The deploy job runs a 5-attempt curl smoke check against `/api/tournaments?status=active,upcoming` and fails the CI run if the live API does not 200. +- Netlify's git auto-deploy remains enabled as a fallback. With `[build.environment] NODE_VERSION = "24.1.0"` pinned in `netlify.toml`, future image rollouts have one fewer variable; but the GitHub Actions path is the source of truth. + ### Destructive Migrations And The Deploy Race -CI's `migrate-production` job and Netlify's build are independent -triggers on the same `main` push event. There is no ordering -guarantee between "schema is migrated" and "new Lambda bundle is -serving traffic." For backward-compatible changes (`ADD COLUMN` -nullable, new tables) the race is harmless. For destructive -changes it is not: an incompatible drop can land in the DB while -the live Lambda still selects the column, returning 500s until -Netlify catches up. - -Use a two-step pattern for destructive changes (see §3 of -`AGENTS.md`): +CI's `migrate-production` job and the deploy step are sequenced in the +same workflow (`deploy-production` `needs:` `migrate-production`), so +the deploy waits for the migration to finish before publishing the new +Lambda. That removes the historical race between Netlify's +out-of-band build and the migrate job. + +A residual race still exists when Netlify's git auto-deploy is left +on as a fallback (it runs in parallel with our pipeline). Until we +fully disable it, keep using the two-step pattern for destructive +changes (see §3 of `AGENTS.md`): 1. ship code that no longer references the column, merge, and confirm the new Lambda is live