mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 23:53:05 +00:00
chore(OPS34): e2e CI em 2 shards e cadeia de projetos dev-only (Closes #599)
This commit is contained in:
parent
549d3f8167
commit
f955c6a7fe
8 changed files with 267 additions and 14 deletions
22
.github/workflows/ci-pr.yml
vendored
22
.github/workflows/ci-pr.yml
vendored
|
|
@ -38,6 +38,8 @@ jobs:
|
|||
test_mode: ${{ steps.classify.outputs.test_mode }}
|
||||
e2e_mode: ${{ steps.classify.outputs.e2e_mode }}
|
||||
e2e_specs: ${{ steps.classify.outputs.e2e_specs }}
|
||||
e2e_matrix: ${{ steps.classify.outputs.e2e_matrix }}
|
||||
e2e_shards: ${{ steps.classify.outputs.e2e_shards }}
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
|
|
@ -60,6 +62,11 @@ jobs:
|
|||
echo "test_mode=$(echo "$result" | jq -r .test.mode)" >> "$GITHUB_OUTPUT"
|
||||
echo "e2e_mode=$(echo "$result" | jq -r .e2e.mode)" >> "$GITHUB_OUTPUT"
|
||||
echo "e2e_specs=$(echo "$result" | jq -r '[.e2e.specs[] | "tests/e2e/" + . + ".e2e.spec.ts"] | join(" ")')" >> "$GITHUB_OUTPUT"
|
||||
# OPS34: full runs split across shards (one runner per shard); selected
|
||||
# stays on a single runner (a second build for a handful of specs is
|
||||
# waste). Single source: e2eShardConfig in scripts/lib/test-affected-core.mjs.
|
||||
echo "e2e_matrix=$(echo "$result" | jq -c .e2e_shards.matrix)" >> "$GITHUB_OUTPUT"
|
||||
echo "e2e_shards=$(echo "$result" | jq -r .e2e_shards.total)" >> "$GITHUB_OUTPUT"
|
||||
- name: Cancel workflow on failure
|
||||
if: failure()
|
||||
uses: ./.github/actions/cancel-workflow-on-failure
|
||||
|
|
@ -287,6 +294,15 @@ jobs:
|
|||
&& (needs.knip.result == 'success' || needs.knip.result == 'skipped')
|
||||
&& (needs.cycles.result == 'success' || needs.cycles.result == 'skipped')
|
||||
&& (needs.unit.result == 'success' || needs.unit.result == 'skipped')
|
||||
# OPS34: full mode splits across 2 runners (matrix from the scope job);
|
||||
# selected mode stays on one. fail-fast: false keeps both shards' logs.
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: ${{ fromJson(needs.scope.outputs.e2e_matrix) }}
|
||||
env:
|
||||
# Explicit CI workers; the local default stays 2 (playwright.config.ts).
|
||||
PLAYWRIGHT_WORKERS: 2
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
|
|
@ -320,9 +336,11 @@ jobs:
|
|||
env:
|
||||
NEXT_DIST_DIR: .next/e2e
|
||||
|
||||
- name: E2E tests (full suite)
|
||||
- name: E2E tests (full suite, shard ${{ matrix.shard }})
|
||||
if: needs.scope.outputs.e2e_mode == 'full'
|
||||
run: pnpm test:e2e
|
||||
# No `--` separator: pnpm would forward it verbatim to playwright and
|
||||
# the flag would be parsed as a file pattern ("No tests found").
|
||||
run: pnpm test:e2e --shard=${{ matrix.shard }}/${{ needs.scope.outputs.e2e_shards }}
|
||||
|
||||
- name: E2E tests (manifest-selected specs)
|
||||
if: needs.scope.outputs.e2e_mode == 'selected'
|
||||
|
|
|
|||
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
|
|
@ -111,6 +111,19 @@ jobs:
|
|||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 40
|
||||
# Main always runs the full suite: split it across 2 runners (OPS34), each
|
||||
# building its own `.next/e2e` (measured 2026-08-10: build 172s + tests
|
||||
# 196s + ~54s fixed → ≈ 5.5 min per shard vs 435s serial). `fail-fast:
|
||||
# false` so one shard's failure keeps the other's diagnostics. Keep the
|
||||
# [1, 2] / 2 literal in sync with `e2eShardConfig` in
|
||||
# scripts/lib/test-affected-core.mjs (ci-pr reads it via the scope job).
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2]
|
||||
env:
|
||||
# Explicit CI workers; the local default stays 2 (playwright.config.ts).
|
||||
PLAYWRIGHT_WORKERS: 2
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:17-alpine
|
||||
|
|
@ -141,8 +154,10 @@ jobs:
|
|||
run: pnpm build
|
||||
env:
|
||||
NEXT_DIST_DIR: .next/e2e
|
||||
- name: E2E tests (full suite)
|
||||
run: pnpm test:e2e
|
||||
- name: E2E tests (full suite, shard ${{ matrix.shard }})
|
||||
# No `--` separator: pnpm would forward it verbatim to playwright and
|
||||
# the flag would be parsed as a file pattern ("No tests found").
|
||||
run: pnpm test:e2e --shard=${{ matrix.shard }}/2
|
||||
- name: Cancel workflow on failure
|
||||
if: failure()
|
||||
uses: ./.github/actions/cancel-workflow-on-failure
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
|
|||
|
||||
---
|
||||
|
||||
**Recently resolved (2026-08-10):** **OPS34** (Issue #599) — o job `e2e` do CI (o mais longo da cascata, 435 s medidos no run 31412425553: ~54 s fixos + build 172 s + testes 196 s) roda agora **em 2 shards paralelos** (`--shard=N/2`, matrix `fail-fast: false`, build por shard — opção A da intenção validada pela medição: por shard ≈ 5–5,5 min; artefato de build compartilhado rejeitado porque os shards só começam após o build → wall maior). **Cadeia de projetos do Playwright (`setup → campaign → frontend → admin`) + prewarm do `setup` viraram dev-only:** `playwright.config.ts` ganhou `isProdMode = CI || E2E_PROD=1` (mesma condição do `webServer.command`, agora reusada) — em prod o projeto `setup` sai do array (arquivo não casado = não coletado) e as `dependencies` zeram (famílias em paralelo contra o build de produção, onde não há compile a preaquecer); dev intocado (provado: `--list` mostra `[setup]` só sem CI; smoke e2e dev rodou setup→campaignAuth). `ci-pr.yml`: o job `scope` emite `e2e_matrix`/`e2e_shards` via novo `e2eShardConfig(mode)` em `scripts/lib/test-affected-core.mjs` (fonte única, unit-pinada: full → `[1,2]`/2, selected/none → `[1]`/1 — pagar 2 builds por 3 specs é desperdício); `ci.yml` hardcodeia `[1,2]`/2 com comentário apontando o single-source. `PLAYWRIGHT_WORKERS: 2` explícito no env dos jobs e2e (aceite: explícito, default local intocado). Armadilha registrada nos workflows: flags do playwright vão **sem** o separador `--` — pnpm repassa o `--` literal ao playwright e a flag vira file pattern ("No tests found"). Specs intactas (o `setup` não foi removido — virou dev-only, já documentado como prewarm dev-only); sem migration/access/Consent/UI. Medição pós-merge do job e2e do ci.yml a registrar aqui. Intenção [`ci-e2e-paralelizar-job.md`](plans/ci-e2e-paralelizar-job.md); impl [`ci-e2e-paralelizar-job-impl.md`](plans/ci-e2e-paralelizar-job-impl.md).
|
||||
|
||||
**Recently resolved (2026-08-10):** **OPS22** (Issue #562) — CI de `main` vermelho determinístico desde o merge do B188 (05:54): os 3 e2e da agenda mobile (`campaignAgendaMobile` :96/:242/:295) falhavam com todas as retries → deploy bloqueado. **Root cause (reproduzida no prod build local + error-contexts com o drawer aberto):** o login e2e roda em **viewport desktop** → o settle de B167 abre o chat (`open=true`) → o **efeito persist do B188** (status `ready` já nasce) grava `{open:true}` no `sessionStorage` → o spec muda para 390px na **mesma aba** → o **restore do B188** reabre o **drawer mobile sozinho** → o modal Radix **aria-hide + inert** a página e cobre o viewport (combobox fora da a11y tree no :242, swipe cai no drawer no :96, clique do seletor bloqueado no :295; teclado/:173 passam por não usarem hit-testing). **Defeito real de produto** (aba desktop → mobile na mesma aba faz o chat "saltar"), não só timing de teste. **Fix na origem:** `SollinhaChatSession` ganhou **`openBy: 'user' | 'settle'`** (opcional — sessão legada sem o campo = `'settle'`, fail-closed; sem bump de versão) e o provider agora distingue **intenção do usuário** (`setOpen`/`toggle` do context value marcam `userToggledOpenRef`; FAB/header/drawer já passam por eles) de **verdade de layout** (settle B167 usa o setter cru): o **restore espera o `measured`** (viewport real) e honra `open:true` em mobile **só quando `openBy==='user'`**; o persist grava `openBy` conforme a intenção. Todos os contratos B188/resize mantidos (reload restaura conversa; drawer aberto por usuário volta aberto; visita mobile nova fechada; crossing ao vivo B167 intocado). Testes: unit (round-trip/invalidez/legado de `openBy`) + **e2e pin OPS22** no spec B188 (login desktop → poll do persist `open:true` → viewport 390 → `/campanha/agenda` → sem dialog + botão do Sollinha visível); spec da agenda intacto (4/4 no prod build). Sem migration/access/Consent/schema. Intenção [`ci-e2e-agenda-mobile-regressao-b188.md`](plans/ci-e2e-agenda-mobile-regressao-b188.md); impl [`ci-e2e-agenda-mobile-regressao-b188-impl.md`](plans/ci-e2e-agenda-mobile-regressao-b188-impl.md).
|
||||
|
||||
**Recently resolved (2026-08-10):** **OPS26** (Issue #571) — `worktree next`/`plan` **no terminal** terminam com o TUI do opencode aberto no worktree: a função `worktree()` de `.agents/shell/worktree.sh` marca `TEQO_WORKTREE_TERMINAL=1` e o script imprime a diretiva `launch opencode <dir> --model deepseek/deepseek-v4-flash --auto [--prompt /work-issue]` **antes** do `cd` (que segue sendo a última linha — contrato OPS24 intacto); a função aplica o cd e executa a linha (sem `exec`: sair do TUI volta ao shell no worktree; sem auto-`kill`). `next` envia `/work-issue` (o comando OPS25 roda o ciclo completo); `plan` abre **sem prompt** — a CLI do opencode não tem pre-fill sem submit (`--prompt` sempre auto-envia; fallback acordado: `/plan-` + autocomplete; gap registrado para o repo do opencode). Presets são constantes em `scripts/lib/worktree.mjs` (`OPENCODE_PRESET_MODEL`, `OPENCODE_SKILL_COMMAND_BY_PURPOSE`, `opencodeLaunchDirective` — pura, unit-testada; trocar de modelo é editar a constante). **Sem o marcador (comando `/worktree` do opencode, automação) a diretiva não é impressa** — nunca abre TUI aninhado; `--stay` suprime cd e launch; `kill` intocado. Docs: AGENTS.md ("Per-worktree environments"), header/uso do script, `.opencode/commands/worktree.md`, skill `worktree-next-issue`. Sem migration/dados/access/UI. Intenção [`worktree-go-abre-opencode-com-presets.md`](plans/worktree-go-abre-opencode-com-presets.md); impl [`worktree-go-abre-opencode-com-presets-impl.md`](plans/worktree-go-abre-opencode-com-presets-impl.md).
|
||||
|
|
|
|||
168
docs/plans/ci-e2e-paralelizar-job-impl.md
Normal file
168
docs/plans/ci-e2e-paralelizar-job-impl.md
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
# Impl: CI e2e — paralelizar o job (cadeia serial só em dev + sharding do e2e)
|
||||
|
||||
Status: aprovado
|
||||
Atualizado em: 2026-08-10
|
||||
Issue: #599
|
||||
Intenção: docs/plans/ci-e2e-paralelizar-job.md
|
||||
Appetite restante: herdado (~1 dia eng; entrega é ~5 arquivos, sem migration/UI)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** job `e2e` do `ci.yml` cai de ~10 min para ≤ ~6 min de wall time
|
||||
(mesmo conjunto de testes, mesma confiança); cadeia de projetos + prewarm do
|
||||
`setup` continuam existindo **só em dev**; workers do CI explícitos, default
|
||||
local intocado; sem classe nova de flake.
|
||||
- **O que NÃO negociar:** nenhuma spec de produto removida/alterada (campaign/
|
||||
frontend/admin); dev local (`pnpm test:e2e` sem CI/E2E_PROD) idêntico; main
|
||||
e PR full rodam a suite completa; cobertura inalterada.
|
||||
- **O que reavaliar:** a hipótese listava `scripts/gate-ci.mjs` e
|
||||
`scripts/run-e2e-affected.mjs` como áreas. **Não serão tocados**: ambos
|
||||
delegam a `pnpm test:e2e`, e a cadeia condicional mora no config — a
|
||||
paridade local com CI vem de graça (gate-ci já seta `CI=1 E2E_PROD=1` → modo
|
||||
prod → sem cadeia). Colocar sharding nesses scripts duplicaria política de
|
||||
CI na máquina local sem ganho (shard é paralelismo de _runners_ de CI).
|
||||
|
||||
### Medição de base (real, run 31412425553 do ci.yml, 2026-08-10)
|
||||
|
||||
Job e2e completo: **435 s** (17:07:16 → 17:14:31). Decomposição por step:
|
||||
|
||||
| Fase | Tempo |
|
||||
| ---------------------------------- | --------- |
|
||||
| setup/checkout/tooling | ~17 s |
|
||||
| pnpm install (cache) | 5 s |
|
||||
| install playwright chromium | 19 s |
|
||||
| migrate + seed:minimal | 13 s |
|
||||
| **build (`.next/e2e`)** | **172 s** |
|
||||
| **testes (suite full, 2 workers)** | **196 s** |
|
||||
|
||||
(A intenção citava 617–635 s — runs mais lentos do mesmo dia; a decomposição
|
||||
acima é a mais recente e é a linha de base usada aqui.)
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph ci.yml[ci.yml (main) — e2e sempre full]
|
||||
E2E1[e2e shard 1<br/>build + test --shard=1/2] --> C1[checks]
|
||||
E2E2[e2e shard 2<br/>build + test --shard=2/2] --> C1
|
||||
end
|
||||
subgraph ci-pr.yml[ci-pr.yml (PR)]
|
||||
S[scope job<br/>e2e_mode + e2e_matrix] --> P1[e2e full: matrix [1,2]<br/>--shard=N/2]
|
||||
S --> P2[e2e selected: matrix [1]<br/>specs sem shard]
|
||||
P1 --> CP[checks]
|
||||
P2 --> CP
|
||||
end
|
||||
subgraph config[playwright.config.ts]
|
||||
C[isProdMode = CI ou E2E_PROD=1<br/>→ sem projeto setup, sem dependencies<br/>dev → cadeia + prewarm atuais]
|
||||
end
|
||||
P1 -. roda sobre .-> C
|
||||
E2E1 -. roda sobre .-> C
|
||||
```
|
||||
|
||||
**Opções consideradas:** A) build por shard (cada runner compila) · B) job de
|
||||
build + artefato `.next/e2e` compartilhado · C) 3 shards já de início
|
||||
**Recomendação:** **A com 2 shards** — porque a medição real decide: por shard
|
||||
= 54 s (fixo) + 172 s (build) + ~98 s (196/2) ≈ **324 s ≈ 5,4 min**; e com a
|
||||
cadeia removida os testes encolhem mais (~120–150 s full → ~75 s por shard),
|
||||
chegando a ~5 min. O caminho crítico do pipeline (max entre e2e/build/int)
|
||||
passa de 435 s para ~300–330 s.
|
||||
**Rejeitadas:** **B** porque os shards só podem começar depois do build
|
||||
terminar: wall = build 172 + upload + download + teste/2 ≫ A (a transferência
|
||||
de um `.next` inteiro supera o build paralelo de cada shard); o `build` job do
|
||||
ci.yml **não** serve (compila `.next` default e o servidor e2e usa
|
||||
`.next/e2e` via `NEXT_DIST_DIR` — mudar isso é mexer no contrato do webServer
|
||||
para nada). **C** porque o ganho é marginal (~30 s a mais) ao custo de um
|
||||
runner e de mais pressão de quota; fica como gatilho de revisitação se o wall
|
||||
por shard passar de ~6,5 min ou a distribuição de testes ficar desbalanceada.
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`playwright.config.ts`**: hoist de `isProdMode = Boolean(process.env.CI)
|
||||
|| process.env.E2E_PROD === '1'` (mesma condição do `webServer.command`
|
||||
atual — agora reusada lá e nos projects). Em prod mode: projeto `setup`
|
||||
**removido** do array de projects e `dependencies` de campaign/frontend/
|
||||
admin viram `[]` (o `setup.e2e.spec.ts` fica sem project → Playwright ignora
|
||||
arquivo não casado, sem erro). Em dev: estrutura idêntica à de hoje
|
||||
(cadeia `setup → campaign → frontend → admin`, `fullyParallel: false` do
|
||||
admin preservado nos dois modos). Comentário no config explica o porquê.
|
||||
- **`scripts/lib/test-affected-core.mjs`**: novo `e2eShardConfig(mode)` puro →
|
||||
`'full'` retorna `{ matrix: [1, 2], total: 2 }`; `'selected'`/`'none'`
|
||||
retornam `{ matrix: [1], total: 1 }` — com comentário da medição e do
|
||||
gatilho de 3 shards. (Módulo já é o dono da seleção e2e da CI, já unitado.)
|
||||
- **`scripts/ci-scope.mjs`**: output JSON ganha `e2e_shards` (resultado do
|
||||
helper acima).
|
||||
- **`.github/workflows/ci.yml`**: job `e2e` vira matrix `shard: [1, 2]`
|
||||
(`fail-fast: false`; comentário apontando o single-source em
|
||||
test-affected-core); step de teste vira `pnpm test:e2e -- --shard=${{ matrix.shard }}/2`;
|
||||
env de job `PLAYWRIGHT_WORKERS: 2` explícito (aceite #4).
|
||||
- **`.github/workflows/ci-pr.yml`**: job `scope` emite `e2e_matrix` +
|
||||
`e2e_shards`; job `e2e` usa `strategy.matrix.shard: ${{ fromJson(...) }}`
|
||||
(full → `[1,2]`, selected → `[1]` — selected continua num runner só: pagar
|
||||
2 builds por 3 specs é desperdício); step full com
|
||||
`--shard=${{ matrix.shard }}/${{ needs.scope.outputs.e2e_shards }}`; step
|
||||
selected inalterado; env de job `PLAYWRIGHT_WORKERS: 2`.
|
||||
- **Migration:** nenhuma (sem schema).
|
||||
- **Access / Consent:** N/A.
|
||||
- **UI:** N/A (Impeccable A — infra de CI/testes).
|
||||
|
||||
### Nota de aceite — o spec `setup` e "nenhuma spec removida"
|
||||
|
||||
O `setup.e2e.spec.ts` não é removido nem alterado: ele vira **dev-only** (já é
|
||||
documentado como prewarm dev-only no próprio config e no issue). Em CI ele é
|
||||
custo puro (25 requisições de prewarm contra um build de produção, sem
|
||||
compile), e nenhum spec de produto depende dele em prod. A suite completa de
|
||||
produto (campaign/frontend/admin) roda em main e PR full, intacta.
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Infra (config + workflows + helper)** — as 5 mudanças acima, num único
|
||||
commit de CI (sem migration, sem UI).
|
||||
2. **Verificação local** —
|
||||
- `pnpm exec playwright test --list` (dev): setup presente, deps `[setup]`/
|
||||
`[campaign]`/`[frontend]`; `CI=1 pnpm exec playwright test --list`: sem
|
||||
projeto setup, sem deps.
|
||||
- `pnpm test:unit` (pin novo do `e2eShardConfig`) + `pnpm gate:fast`.
|
||||
- Smoke e2e dev de uma spec `campaign*` + uma `admin` (prova que o modo dev
|
||||
segue com cadeia e prewarm).
|
||||
3. **Verificação CI** — o próprio PR exercita o novo matrix em full
|
||||
(`playwright.config.ts` está em `HIGH_RISK_EXACT` → e2e_mode full):
|
||||
conferir 2 shards rodando em paralelo e `checks` verde. Pós-merge: medir o
|
||||
job e2e do ci.yml e registrar antes/depois no changelog.
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- Subir workers do CI além de 2 (aceite pede explícito, não maior).
|
||||
- Shard por família/afinidade (corte da intenção — `--shard` padrão).
|
||||
- Sharding do modo `selected` do PR (gatilho: wall selected recorrente > 10
|
||||
min).
|
||||
- Otimizar o job `build`/`int` (fora do escopo da intenção).
|
||||
- Mexer em `gate-ci.mjs`/`run-e2e-affected.mjs` (paridade herda do config).
|
||||
- Reduzir `timeout-minutes` dos jobs (sem ganho de aceite, risco de
|
||||
falso-positivo em run lento).
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Desbalanceamento de shard** (Playwright distribui por test ID): aceitável
|
||||
com fullyParallel; gatilho de 3 shards se o wall passar de ~6,5 min.
|
||||
- **Famílias interleaved em prod** (antes: campaign → frontend → admin
|
||||
seriadas): servidor é build de produção (sem compile race); specs são
|
||||
auto-contidas (fixtures por spec + advisory locks do testDatabaseLease); o
|
||||
número de workers por run não muda (2) — a classe de flake por carga não
|
||||
cresce. A run do PR full valida.
|
||||
- **`fromJson` de matrix com output vazio no ci-pr**: o job `scope` sempre
|
||||
emite `e2e_matrix` válido (`[1]` no mínimo) antes do `e2e` avaliar o matrix
|
||||
(que já depende de `scope`); job com `e2e_mode == 'none'` é skipado pelo
|
||||
`if:` existente — o matrix de `[1]` nunca dispara run extra.
|
||||
- **Drift do número 2 entre ci.yml (literal) e `e2eShardConfig`**: comentários
|
||||
cruzados nos dois arquivos + entrada do changelog; mudar para 3 é editar os
|
||||
dois pontos apontados.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [x] Aceite de produto da intenção ainda coberto (mapa: wall ≤ ~6 min ✓,
|
||||
suite completa em main/PR-full ✓, dev intocado ✓, workers explícitos ✓,
|
||||
flakes: mesma carga por run)
|
||||
- [x] Invariantes AGENTS/engineering-standards (sem migration, sem access,
|
||||
sem UI; gate:fast + gate de CI no fechamento)
|
||||
- [x] Testes previstos: unit do `e2eShardConfig` + `--list` nos dois modos +
|
||||
smoke e2e dev + run do PR full (2 shards)
|
||||
|
|
@ -27,6 +27,17 @@ const workers = workersOverride === undefined ? 2 : Number(workersOverride)
|
|||
if (!Number.isInteger(workers) || workers < 2) {
|
||||
throw new Error('PLAYWRIGHT_WORKERS must be an integer of at least 2.')
|
||||
}
|
||||
/*
|
||||
* Prod mode (CI or E2E_PROD=1) serves the PRODUCTION build, where no dev-mode
|
||||
* compile exists. The `setup` project (route prewarm) and the project chain
|
||||
* (setup → campaign → frontend → admin) exist to stabilize dev-mode cold
|
||||
* compiles; against a production build they are pure cost — measured 2026-08-10
|
||||
* (OPS34): the 25 prewarm requests plus the 4-test frontend/admin tail behind
|
||||
* the ~30-file campaign family. Prod mode therefore drops the setup project and
|
||||
* the dependencies so all families run in parallel; CI shards the suite across
|
||||
* runners instead (ci.yml / ci-pr.yml pass `--shard`). Dev mode is unchanged.
|
||||
*/
|
||||
const isProdMode = Boolean(process.env.CI) || process.env.E2E_PROD === '1'
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
|
|
@ -67,27 +78,37 @@ export default defineConfig({
|
|||
timeout: 10_000,
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'setup',
|
||||
testMatch: /setup\.e2e\.spec\.ts/,
|
||||
use: { ...devices['Desktop Chrome'], channel: 'chromium' },
|
||||
},
|
||||
/*
|
||||
* `setup` prewarms shared Next route bundles against the dev server's cold
|
||||
* compile cache. In prod mode there is no compile to prewarm and the whole
|
||||
* chain is pure cost (see `isProdMode` above), so the setup project only
|
||||
* exists in dev; an unmatched spec file is simply not collected.
|
||||
*/
|
||||
...(isProdMode
|
||||
? []
|
||||
: [
|
||||
{
|
||||
name: 'setup',
|
||||
testMatch: /setup\.e2e\.spec\.ts/,
|
||||
use: { ...devices['Desktop Chrome'], channel: 'chromium' },
|
||||
},
|
||||
]),
|
||||
{
|
||||
name: 'campaign',
|
||||
testMatch: /campaign.*\.e2e\.spec\.ts/,
|
||||
dependencies: ['setup'],
|
||||
dependencies: isProdMode ? [] : ['setup'],
|
||||
use: { ...devices['Desktop Chrome'], channel: 'chromium' },
|
||||
},
|
||||
{
|
||||
name: 'frontend',
|
||||
testMatch: /frontend\.e2e\.spec\.ts/,
|
||||
dependencies: ['campaign'],
|
||||
dependencies: isProdMode ? [] : ['campaign'],
|
||||
use: { ...devices['Desktop Chrome'], channel: 'chromium' },
|
||||
},
|
||||
{
|
||||
name: 'admin',
|
||||
testMatch: /admin\.e2e\.spec\.ts/,
|
||||
dependencies: ['frontend'],
|
||||
dependencies: isProdMode ? [] : ['frontend'],
|
||||
fullyParallel: false,
|
||||
use: { ...devices['Desktop Chrome'], channel: 'chromium' },
|
||||
},
|
||||
|
|
@ -102,7 +123,7 @@ export default defineConfig({
|
|||
* (CampaignListSheetProvider, 2026-07-30). Locally `pnpm dev` stays the
|
||||
* default: no build wait and hot reload while writing specs.
|
||||
*/
|
||||
command: process.env.CI || process.env.E2E_PROD === '1' ? 'pnpm start' : 'pnpm dev',
|
||||
command: isProdMode ? 'pnpm start' : 'pnpm dev',
|
||||
/*
|
||||
* Never reuse a dev server a developer may already have running against the
|
||||
* production database — always start a fresh one bound to the test DB.
|
||||
|
|
@ -111,7 +132,7 @@ export default defineConfig({
|
|||
url: baseURL,
|
||||
// Local-only tolerance for parallel-worktree load (machine load ~60); CI
|
||||
// boots the production build and keeps the usual 60s budget.
|
||||
timeout: process.env.CI ? undefined : 240_000,
|
||||
timeout: isProdMode ? undefined : 240_000,
|
||||
/* Force the isolated test database into the server process. */
|
||||
env: {
|
||||
DATABASE_URL: process.env.DATABASE_URL as string,
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
classifyBuildScope,
|
||||
classifyStaticScope,
|
||||
classifyTestScope,
|
||||
e2eShardConfig,
|
||||
selectE2eSpecs,
|
||||
} from './lib/test-affected-core.mjs'
|
||||
|
||||
|
|
@ -63,5 +64,6 @@ console.log(
|
|||
build: classifyBuildScope(files),
|
||||
test: classifyTestScope(files),
|
||||
e2e,
|
||||
e2e_shards: e2eShardConfig(e2e.mode),
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -236,6 +236,21 @@ export function selectE2eSpecs(files, manifest) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* CI job-level parallelism for the e2e suite (OPS34). The full run splits
|
||||
* across 2 shards (`--shard=N/total`); selected and none stay on a single
|
||||
* runner — a second build for a handful of specs is pure waste. Measured
|
||||
* 2026-08-10 (ci.yml run 31412425553): build 172s + tests 196s + ~54s fixed,
|
||||
* so shard 1 and 2 each land ≈ 5–5.5 min. Revisit at 3 shards if the sharded
|
||||
* wall drifts past ~6.5 min or the test split gets lopsided; ci.yml hardcodes
|
||||
* the same [1, 2] / 2 literal with a comment pointing back here.
|
||||
* @param {'full' | 'selected' | 'none'} mode
|
||||
* @returns {{ matrix: number[], total: number }}
|
||||
*/
|
||||
export function e2eShardConfig(mode) {
|
||||
return mode === 'full' ? { matrix: [1, 2], total: 2 } : { matrix: [1], total: 1 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Walk a list of repo-relative paths and return misplaced spec/test files.
|
||||
* @param {string[]} paths
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {
|
|||
classifyBuildScope,
|
||||
classifyStaticScope,
|
||||
classifyTestScope,
|
||||
e2eShardConfig,
|
||||
HIGH_RISK_EXACT,
|
||||
HIGH_RISK_PREFIXES,
|
||||
selectE2eSpecs,
|
||||
|
|
@ -115,3 +116,14 @@ describe('selectE2eSpecs (OPS5)', () => {
|
|||
expect(result).toMatchObject({ mode: 'none', specs: [], unmapped: [] })
|
||||
})
|
||||
})
|
||||
|
||||
describe('e2eShardConfig (OPS34)', () => {
|
||||
it('splits the full suite across 2 shards', () => {
|
||||
expect(e2eShardConfig('full')).toEqual({ matrix: [1, 2], total: 2 })
|
||||
})
|
||||
|
||||
it('keeps selected and skipped runs on a single runner', () => {
|
||||
expect(e2eShardConfig('selected')).toEqual({ matrix: [1], total: 1 })
|
||||
expect(e2eShardConfig('none')).toEqual({ matrix: [1], total: 1 })
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue