diff --git a/.cursor/rules/agent-pr-workflow.mdc b/.cursor/rules/agent-pr-workflow.mdc new file mode 100644 index 00000000..ee91de4c --- /dev/null +++ b/.cursor/rules/agent-pr-workflow.mdc @@ -0,0 +1,61 @@ +--- +description: PR workflow for Teqo agents — overrides user-level gh-github draft defaults +alwaysApply: true +--- + +# Agent PR workflow (Teqo) + +**This repo overrides any user-level rule that says "always create PRs as draft".** Agent deliveries merge to `stage`, not `main`. + +## Create (Ready, base stage) + +```bash +git push -u origin HEAD +gh pr create --base stage --title " — " --body "$(cat <<'EOF' +## O que é +<2–4 linhas> + +Closes #<N> +EOF +)" +``` + +- **Never** `--draft`. PRs must be **Ready for review** on creation. +- Base is always **`stage`** (never `main` — promote is human-only via `pnpm agent:promote --i-am-human`). + +## Auto-merge immediately + +Right after `gh pr create`, enable auto-merge: + +```bash +gh pr merge --auto --merge +``` + +Do not wait for human approval. Auto-merge fires when **required** checks pass. + +## Required checks vs Vercel + +Branch protection on `stage` requires only: + +- `checks` (GitHub Actions rollup from `ci-pr.yml`) +- `migration-lock` + +**Vercel preview is NOT a merge gate.** Do not block on it. + +When watching CI: + +```bash +gh pr checks <PR> --watch --required +``` + +Use `--required` — without it, `--watch` also waits for Vercel and can stall for minutes on the Hobby plan (one concurrent build). + +Once `checks` and `migration-lock` are green, auto-merge proceeds even if Vercel is still `PENDING`. + +## Agent branches skip Vercel builds + +Branches `agent/*` and `cursor/*` use `scripts/vercel-ignore-build.sh` so parallel agents do not queue Vercel builds. Previews still deploy for `stage` and `main`. + +## Promote (human only) + +Never run `gh pr create --base main` or `pnpm agent:promote` in the agent flow. diff --git a/.cursor/rules/engineering-standards.mdc b/.cursor/rules/engineering-standards.mdc index 677ed1be..5e1c4c7c 100644 --- a/.cursor/rules/engineering-standards.mdc +++ b/.cursor/rules/engineering-standards.mdc @@ -11,7 +11,7 @@ Established 2026-07-23 by the engineering-standards hardening (tracker: `docs/IM 1. **Dead code dies immediately.** After your edits, check what your change orphaned and delete it in the same delivery: run `pnpm exec knip` (files/dependencies are CI-blocking errors; unused exports are warnings being driven to zero). Never leave commented-out code, unused exports, superseded components, or one-shot scripts behind — git history is the archive. 2. **Zero warnings.** `pnpm lint` runs with `--max-warnings=0`. Deliberate placeholders use a `_` prefix. Never silence a rule without an inline justification comment. -3. **Gate em duas velocidades (desde 2026-07-30, paradigma de agentes paralelos — `docs/AGENT-OPS.md`).** **Fast gate local do agente** (antes de cada push): `pnpm gate:fast` (lint + typecheck + test:unit); o pre-push hook roda `pnpm gate:push` (fast + format:check + check:cycles). Fonte única: scripts `gate:*` em `package.json`; escape para WIP: `git push --no-verify`. Se a entrega tocou schema: também `pnpm migrate && pnpm db:seed:minimal && pnpm test:int`. **Gate completo = CI do PR** (`ci-pr.yml`: jobs paralelos — estático + unit/int afetados pelo diff + e2e por manifesto + build, contra o DB mínimo). O handoff humano (promote `stage→main`) exige CI stage + ci-pr green — não o build local. **CI vermelho no teu PR é teu, mesmo pré-existente ou de infra** — "fora do escopo da feature" não é critério de parada (política "Dono do PR, dono do CI" em `docs/AGENT-OPS.md`). +3. **Gate em duas velocidades (desde 2026-07-30, paradigma de agentes paralelos — `docs/AGENT-OPS.md`).** **Fast gate local do agente** (iteração durante a sessão): `pnpm gate:fast` (lint + typecheck + test:unit). **Pre-push hook** roda `pnpm gate:push` → `pnpm gate:ci` — espelho serial do `ci-pr.yml`: estático sempre; unit/int/e2e no **mesmo blast radius** que o CI (`scripts/test-affected.mjs`, `scripts/e2e-affected.mjs` vs `origin/stage`); build sempre. Fonte única: scripts `gate:*` em `package.json`; escape para WIP: `git push --no-verify`. `migration-lock` só existe no GitHub (conta PRs abertos em schema). O handoff humano (promote `stage→main`) exige CI stage + ci-pr green — não o build local. **CI vermelho no teu PR é teu, mesmo pré-existente ou de infra** — "fora do escopo da feature" não é critério de parada (política "Dono do PR, dono do CI" em `docs/AGENT-OPS.md`). 4. **Run gate commands bare, never piped** (`pnpm test | tail` swallows the exit code). Never run `knip --fix` blind — verify each removal with `git grep -w <symbol>` first (knip still cannot load `payload.config.ts`, so its graph is incomplete; ledgered as P3). ## Type honesty diff --git a/.cursor/skills/work-issue/SKILL.md b/.cursor/skills/work-issue/SKILL.md index 8564088c..beb4009d 100644 --- a/.cursor/skills/work-issue/SKILL.md +++ b/.cursor/skills/work-issue/SKILL.md @@ -21,8 +21,8 @@ Esta skill conduz UMA Issue rastreável do claim ao merge em `stage`. Substitui - [ ] 3. Freshness audit (enxuto) do plano contra o repositório - [ ] 4. Executar as fases (schema/server → UI via /impeccable → gates de engenharia) - [ ] 5. /simplify no diff da sessão → capture-review-debts -- [ ] 6. Fechar em stage: fast gate → branch → commit → gh pr create --base stage (Closes #N) - → gh pr merge --auto --merge → gh pr checks --watch até o merge → consolidar pontas soltas +- [ ] 6. Fechar em stage: pnpm gate:push → branch → commit → gh pr create --base stage (Closes #N, sem --draft) + → gh pr merge --auto --merge → gh pr checks --watch --required até o merge → consolidar pontas soltas ``` ## Passo 0 — Prep (deps) @@ -81,7 +81,7 @@ Ordem fixa, fases pequenas e verificáveis, respeitando o appetite do plano: 1. **Schema e server** — migrations (`pnpm migrate:create`, seguir `payload-migrations`), collections, utilities, server actions, testes de domínio. Guardrails do repo: Local API com `user` → `overrideAccess: false`; escrita multi-collection → transação com `req: { transactionID }`; pessoa → join com `Contact`; opt-in/PII → `Consent` por chave estável falhando fechado. 2. **UI via /impeccable** (classes B/C/D — a classe está no plano; se A, declare "Impeccable: N/A" e siga só engenharia): shape conforme a classe → craft → critique → (harden/optimize **só sob gatilho**) → polish. Paleta = tokens `data-theme='campaign'`; reusar `src/components/ui` e shells existentes; shape obrigatório em C **para** para confirmação do brief antes do craft. -3. **Gates de engenharia** — fast gate local: `pnpm gate:fast` (lint + typecheck + test:unit) e, antes do push, `pnpm gate:push` (fast + format:check + check:cycles — o pre-push hook roda exatamente isso) (+ `pnpm migrate && pnpm db:seed:minimal && pnpm test:int` se tocou schema). Scan Aikido dos arquivos editados. Comandos bare, nunca piped. +3. **Gates de engenharia** — iteração: `pnpm gate:fast`. Antes do push o hook roda `pnpm gate:push` (= `gate:ci`: estático + build sempre; unit/int/e2e no blast radius do CI via `test-affected`/`e2e-affected`). Scan Aikido dos arquivos editados. Comandos bare, nunca piped. Escape WIP: `git push --no-verify`. Tracer bullet: se a Issue for grande, a primeira fatia vertical real (schema mínimo → uma action → uma superfície UI) vem cedo. @@ -91,11 +91,11 @@ Rode `/simplify` sobre o diff da sessão. Follow-ups maiores que o cleanup → ` ## Passo 6 — Fechar em stage -1. Fast gate verde (Passo 4.3). +1. `pnpm gate:push` verde (ou `pnpm gate:ci` — espelho do ci-pr; o hook pre-push roda na sequência do `git push`). 2. Branch `agent/<id>-<slug>` (worktrees do Cursor são donos da criação; commits lógicos). -3. `gh pr create --base stage` com `Closes #<N>` no body. -4. `gh pr merge --auto --merge <PR>`. -5. **Acompanhe os checks até o merge** (`gh pr checks <PR> --watch`): falha no ci-pr → corrige na mesma branch e reempurra (o auto-merge dispara de novo quando verde). **Qualquer falha é tua** — infra do workflow, teste pré-existente ou regressão da feature; "fora do escopo da feature" não é critério de parada (política "Dono do PR, dono do CI" em `docs/AGENT-OPS.md`; exceção de blast radius: migration/access/Consent → para e escala). +3. `gh pr create --base stage` com `Closes #<N>` no body — **nunca `--draft`** (`.cursor/rules/agent-pr-workflow.mdc`). +4. `gh pr merge --auto --merge <PR>` imediatamente após criar o PR. +5. **Acompanhe só os checks obrigatórios** (`gh pr checks <PR> --watch --required`): gate = `checks` + `migration-lock`; Vercel não bloqueia. Falha no ci-pr → corrige na mesma branch. 6. O flip `in-progress → done` é **determinístico no CI** (workflow `issue-done-on-stage-merge.yml` lê o `Closes #N` no merge em stage). O agente espera o merge apenas para verificar e **consolidar pontas soltas**: débitos não registrados, doc da sessão pendente (notebook do projeto + `docs/plans/<slug>.md` com Status/Atualizado em), comentário de fechamento na Issue com o que entrou. Se o agente falhar depois do merge, o status já está correto. ## Resumo final ao usuário diff --git a/.husky/pre-push b/.husky/pre-push index 3461751c..b56772da 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -1,3 +1,3 @@ -# Fast gate before push (source of truth: package.json `gate:push`). -# Full gate = CI do PR (docs/AGENT-OPS.md). Escape hatch para WIP: `git push --no-verify`. +# Full ci-pr mirror before push (source of truth: package.json `gate:push` → `gate:ci`). +# Escape hatch para WIP: `git push --no-verify`. pnpm gate:push diff --git a/docs/AGENT-OPS.md b/docs/AGENT-OPS.md index 8c284791..f3841baf 100644 --- a/docs/AGENT-OPS.md +++ b/docs/AGENT-OPS.md @@ -21,7 +21,7 @@ stage → CI stage green → (humano) pnpm agent:promote --i-am-human → main **Skills do fluxo (desde 2026-07-30):** `plan-issue` (ideia → plano completo + gate de confirmação + Issue via `agent:register --model`) → `work-issue` (claim → verificação de modelo → freshness audit → execução → /simplify → PR `--base stage` → acompanha CI até o merge) → `project-status` (overview/fila/mermaid/bloqueios/consolidação, read-only). `docs/roadmap.md` = **legado congelado** (stub); a fonte canônica são as GitHub Issues. -- **Agente faz sozinho:** `pnpm agent:claim` (pega a próxima Issue `ready` desbloqueada por `prio:P0..P3`, lock otimista ready→in-progress) → implementa → `pnpm gate:fast` → `gh pr create --base stage` com `Closes #N` → acompanha `gh pr checks --watch` até o merge (falha → corrige na mesma branch). O flip `in-progress→done` é determinístico no CI (workflow `issue-done-on-stage-merge.yml`). +- **Agente faz sozinho:** `pnpm agent:claim` → implementa → `pnpm gate:push` (hook pre-push = espelho ci-pr) → `gh pr create --base stage` com `Closes #N` (**Ready**, sem `--draft`) → `gh pr merge --auto --merge` → `gh pr checks --watch --required` até o merge. Regra PR: `.cursor/rules/agent-pr-workflow.mdc`. ### Dono do PR, dono do CI (desde 2026-07-30 — incidente PR #50) @@ -51,6 +51,7 @@ Labels: estado `ready|in-progress|blocked|done|in-prod`, `prio:P0..P3`, `kind:fe 1. Se toca schema (migration, collection, Consent key, dado de boot) → **atualizar `db:seed:minimal` no mesmo PR** (label `needs:migration`). CI PR falha se `migrate + seed:minimal + test:int` não passam. 2. ≤1 PR aberto tocando `src/migrations/` | `payload-types.ts` (job `migration-lock`). 3. `docs/plans/<slug>.md` novo só para Issue nova — nunca recriar plans históricos no merge. +4. **Ready + auto-merge:** ver `.cursor/rules/agent-pr-workflow.mdc` — `gh pr create --base stage` (sem `--draft`) → `gh pr merge --auto --merge`; acompanhar com `gh pr checks --watch --required` (Vercel não é gate). ## CI por alvo @@ -60,7 +61,7 @@ Labels: estado `ready|in-progress|blocked|done|in-prod`, `prio:P0..P3`, `kind:fe | `ci-stage.yml` | push em `stage` | `STAGE_DATABASE_URL` (Environment `stage`) | **só** `migrate` + smoke int (subset curado — a suíte completa é lenta demais contra Neon remoto, estourou timeout 2× em 2026-07-30; a full fica no ci-pr/ci.yml) — **NUNCA `pnpm build`** (build migra o banco que vê), nunca seed:minimal | | `ci.yml` | push em `main` | service Postgres | **gate completo sempre** (rede de segurança pós-promote): estático + unit + int full (com seed:minimal) + build + e2e full | -Fast gate local do agente antes do push: `pnpm gate:fast` (lint + typecheck + test:unit); o hook `.husky/pre-push` roda `pnpm gate:push` (fast + format:check + check:cycles). Fonte única: scripts `gate:*` em `package.json`. knip fica fora do hook (grafo incompleto — ledger P3; CI o roda bloqueante). Escape para WIP: `git push --no-verify` (o CI repete tudo). **E2e afetado localmente (mesmo critério do PR):** `pnpm test:e2e:affected`. int/build/e2e = CI. +Fast gate local do agente (iteração): `pnpm gate:fast` (lint + typecheck + test:unit). O hook `.husky/pre-push` roda `pnpm gate:push` → `scripts/gate-ci.mjs` — **espelho serial do `ci-pr.yml`**: estático + build sempre; unit/int/e2e no **mesmo blast radius** que o CI de feature (`test-affected.mjs` / `e2e-affected.mjs` vs `origin/stage`). Requer Postgres local com `teqo_test` (`pnpm db:start` ou `.cursor/ensure-postgres.sh` no Cloud). `migration-lock` só no GitHub. Escape WIP: `git push --no-verify`. Debug e2e isolado: `pnpm test:e2e:affected`. ## Stage DB — runbook de refresh (semanal, humano) diff --git a/docs/plans/gate-push-local.md b/docs/plans/gate-push-local.md index e5fc4f18..0b358b63 100644 --- a/docs/plans/gate-push-local.md +++ b/docs/plans/gate-push-local.md @@ -1,6 +1,6 @@ # Gate local DRY — gate:fast / gate:push + pre-push completo -Status: entregue (2026-07-30 — executado em sessão única, fora do fluxo agent:register, a pedido do humano) +Status: entregue (2026-07-30) — **ampliado 2026-07-30**: `gate:push` → `gate:ci` espelha `ci-pr.yml` com seleção affected (`test-affected` / `e2e-affected` vs `origin/stage`), não suíte full por default. Atualizado em: 2026-07-30 Issue: — Priority: P2 @@ -28,7 +28,7 @@ Decisão de produto (2026-07-30, brief do lote CI): scripts DRY (`gate:fast`, `g ## Decisões travadas -- **knip fora do pre-push.** O grafo do knip é incompleto (não carrega `payload.config.ts`, ledger P3) e é lento para um hook; CI já o roda como erro bloqueante. **Rejeitado:** knip no hook (custo por push + falsos positivos já fichados); knip no `gate:fast` documentado como obrigatório (mesma razão). +- **knip fora do pre-push.** _(Revertido 2026-07-30 — `gate:ci` inclui knip para espelhar ci-pr.)_ O grafo do knip é incompleto (não carrega `payload.config.ts`, ledger P3) e é lento para um hook; CI já o roda como erro bloqueante. **Rejeitado na v1:** knip no hook (custo por push + falsos positivos já fichados). - **Sem pre-commit hook.** A divisão fica: nada no commit (commits intermediários de WIP são legítimos), tudo no push. **Rejeitado:** lint-staged no commit (adiciona toolchain e atrito sem pegar classe de erro que o push não pegue segundos depois). - **Escape hatch = `--no-verify` nativo, documentado; sem variável mágica própria.** **Rejeitado:** `SKIP_GATE=1` (mais uma convenção para manter; `--no-verify` é o padrão git que todo agente/humano já conhece). - **Hook chama `pnpm gate:push`, nunca inline de comandos.** Uma fonte de verdade editável em `package.json`. **Rejeitado:** manter a linha de shell no hook (foi o que causou o drift atual). @@ -41,7 +41,7 @@ Decisão de produto (2026-07-30, brief do lote CI): scripts DRY (`gate:fast`, `g Componentes: -- **`package.json`** — scripts `gate:fast` e `gate:push` (comandos bare encadeados com `&&`, nunca piped — invariante do repo). +- **`package.json`** — scripts `gate:fast`, `gate:ci` (`scripts/gate-ci.mjs`) e `gate:push` (= `gate:ci`). - **`.husky/pre-push`** — `pnpm gate:push`. - **`.cursor/rules/engineering-standards.mdc`** — item 3 do "After every change" passa a citar `pnpm gate:fast` / `pnpm gate:push`. - **`docs/AGENT-OPS.md`** — linha do fast gate local atualizada para os scripts. diff --git a/package.json b/package.json index 757dd124..1dc9d0dc 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,8 @@ "format": "prettier --write .", "format:check": "prettier --check .", "gate:fast": "pnpm lint && pnpm typecheck && pnpm test:unit", - "gate:push": "pnpm gate:fast && pnpm format:check && pnpm check:cycles", + "gate:ci": "node scripts/gate-ci.mjs", + "gate:push": "pnpm gate:ci", "check:cycles": "madge --circular --extensions ts,tsx --ts-config tsconfig.json src", "generate:importmap": "cross-env NODE_OPTIONS=\"--no-deprecation --conditions=react-server\" payload generate:importmap", "generate:types": "cross-env NODE_OPTIONS=\"--no-deprecation --conditions=react-server\" payload generate:types", diff --git a/scripts/gate-ci.mjs b/scripts/gate-ci.mjs new file mode 100644 index 00000000..f59a3294 --- /dev/null +++ b/scripts/gate-ci.mjs @@ -0,0 +1,147 @@ +#!/usr/bin/env node +/** + * Local mirror of `.github/workflows/ci-pr.yml` (serial): static checks always; + * unit/int/e2e follow the same blast-radius selection as CI (`test-affected.mjs`, + * `e2e-affected.mjs` vs `origin/stage`). Build always runs. `migration-lock` is + * GitHub-only. + * + * Preflight checks only `teqo_test` (ci-pr never touches the dev DB). Escape: + * `git push --no-verify`. + */ +import { execFileSync, spawnSync } from 'node:child_process' +import { readFileSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +import { parse as parseEnv } from 'dotenv' + +import { diagnoseDatabaseTarget } from './db-doctor.mjs' + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') + +const readTestEnv = () => { + try { + return parseEnv(readFileSync(path.join(repoRoot, '.env.test'), 'utf8')) + } catch { + return {} + } +} + +const testEnv = readTestEnv() + +/** Env for migrate / seed / build — matches ci-pr.yml service Postgres. */ +const dbEnv = { + ...process.env, + DATABASE_URL: testEnv.DATABASE_URL ?? 'postgresql://teqo:teqo@localhost:5432/teqo_test', + PAYLOAD_SECRET: testEnv.PAYLOAD_SECRET ?? 'test-only-secret-not-used-in-production', + NEXT_PUBLIC_SITE_URL: testEnv.NEXT_PUBLIC_SITE_URL ?? 'http://localhost:3000', +} + +const readJsonScript = (script) => { + const raw = execFileSync('node', [script], { + cwd: repoRoot, + encoding: 'utf8', + env: process.env, + }) + return JSON.parse(raw.trim()) +} + +const run = (label, command, args, env = process.env) => { + console.log(`\n[gate:ci] ▶ ${label}`) + const result = spawnSync(command, args, { + cwd: repoRoot, + stdio: 'inherit', + env, + }) + if (result.status !== 0) { + console.error(`\n[gate:ci] ✗ ${label} failed (exit ${result.status ?? 1})`) + process.exit(result.status ?? 1) + } +} + +const main = async () => { + const testScope = readJsonScript('scripts/test-affected.mjs') + const e2eScope = readJsonScript('scripts/e2e-affected.mjs') + + console.log(`[gate:ci] vitest: ${testScope.mode} — ${testScope.reason} (base ${testScope.base})`) + console.log(`[gate:ci] e2e: ${e2eScope.mode} — ${e2eScope.reason} (base ${e2eScope.base})`) + if (e2eScope.unmapped?.length > 0) { + console.error(`[gate:ci] e2e unmapped src/ paths:\n ${e2eScope.unmapped.join('\n ')}`) + } + + console.log('\n[gate:ci] ▶ preflight (teqo_test)') + const dbOk = await diagnoseDatabaseTarget({ + label: 'test', + databaseUrl: dbEnv.DATABASE_URL, + }) + if (!dbOk) { + console.error('\n[gate:ci] ✗ preflight (teqo_test) failed — run `pnpm db:start`') + process.exit(1) + } + + run('lint', 'pnpm', ['lint']) + run('format:check', 'pnpm', ['format:check']) + run('typecheck', 'pnpm', ['typecheck']) + run('knip', 'pnpm', ['exec', 'knip']) + run('check:cycles', 'pnpm', ['check:cycles']) + + if (testScope.mode === 'full') { + run('test:unit (full)', 'pnpm', ['test:unit']) + } else if (testScope.mode === 'changed') { + run('test:unit (changed)', 'pnpm', [ + 'test:unit', + '--', + '--changed', + testScope.base, + '--passWithNoTests', + ]) + } else { + console.log('\n[gate:ci] ⊘ test:unit skipped (no src/tests blast radius)') + } + + if (testScope.mode !== 'none') { + run('migrate', 'pnpm', ['migrate'], dbEnv) + run('db:seed:minimal', 'pnpm', ['db:seed:minimal'], dbEnv) + if (testScope.mode === 'full') { + run('test:int (full)', 'pnpm', ['test:int']) + } else { + run('test:int (changed)', 'pnpm', [ + 'test:int', + '--', + '--changed', + testScope.base, + '--passWithNoTests', + ]) + } + } else { + console.log('\n[gate:ci] ⊘ test:int skipped (no src/tests blast radius)') + } + + run('build', 'pnpm', ['build'], dbEnv) + + if (e2eScope.mode === 'none') { + console.log('\n[gate:ci] ⊘ e2e skipped (no e2e blast radius)') + } else { + run('playwright install chromium', 'pnpm', ['exec', 'playwright', 'install', 'chromium']) + run('e2e migrate', 'pnpm', ['migrate'], dbEnv) + run('e2e seed:minimal', 'pnpm', ['db:seed:minimal'], dbEnv) + run('e2e build', 'pnpm', ['build'], { ...dbEnv, NEXT_DIST_DIR: '.next/e2e' }) + + const e2eEnv = { ...dbEnv, E2E_PROD: '1', CI: '1', NEXT_DIST_DIR: '.next/e2e' } + if (e2eScope.mode === 'full') { + run('test:e2e (full)', 'pnpm', ['test:e2e'], e2eEnv) + } else { + const specPaths = e2eScope.specs.map((name) => `tests/e2e/${name}.e2e.spec.ts`) + run( + `test:e2e (selected: ${e2eScope.specs.join(', ')})`, + 'pnpm', + ['test:e2e', '--', ...specPaths], + e2eEnv, + ) + } + } + + console.log('\n[gate:ci] ✓ all checks passed (ci-pr mirror, affected scope)') +} + +await main() diff --git a/scripts/vercel-ignore-build.sh b/scripts/vercel-ignore-build.sh new file mode 100755 index 00000000..f304f51a --- /dev/null +++ b/scripts/vercel-ignore-build.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Vercel "Ignored Build Step": exit 0 = skip deployment, exit 1 = build. +# Agent feature branches must not queue preview builds (Hobby = 1 concurrent build). +# stage/main always build (stage preview + prod). +set -euo pipefail + +ref="${VERCEL_GIT_COMMIT_REF:-}" + +if [ "$ref" = "main" ] || [ "$ref" = "stage" ]; then + exit 1 +fi + +if [[ "$ref" == agent/* ]] || [[ "$ref" == cursor/* ]]; then + echo "skip: agent/cursor branch ($ref) — merge gate is GitHub Actions, not Vercel" + exit 0 +fi + +exit 1 diff --git a/vercel.json b/vercel.json index 78436c5f..7c001ad8 100644 --- a/vercel.json +++ b/vercel.json @@ -1,4 +1,5 @@ { "$schema": "https://openapi.vercel.sh/vercel.json", - "regions": ["gru1"] + "regions": ["gru1"], + "ignoreCommand": "bash scripts/vercel-ignore-build.sh" }