Skip to content

Merge design-diff and design-drift into one design check #3

Merge design-diff and design-drift into one design check

Merge design-diff and design-drift into one design check #3

Workflow file for this run

name: Design check
# Duas perguntas que antes eram dois workflows: o que mudou no design, e se o
# código acompanha o design. Um job, um agente, um comentário. O porquê de
# cada decisão está no §14 do DESIGN-SYSTEM.md; a lógica, em scripts/ — este
# arquivo só encadeia.
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- "design/pendev/**"
- "design/DESIGN-SYSTEM.md"
- "design/screen-routes.json"
- "app/**"
- "components/**"
- "scripts/**"
- ".github/workflows/design-check.yml"
permissions:
contents: read
pull-requests: write
concurrency:
group: design-check-${{ github.event.pull_request.number }}
cancel-in-progress: true
env:
PEN_FILE: design/pendev/youtube-channel.pen
WORK: /tmp/check
BASE_REF: ${{ github.base_ref }}
jobs:
check:
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
# O que roda depende do que o PR mudou, de ser rascunho e de ser fork
# (fork não recebe secret nem token de escrita). Ver scripts/check-gate.sh.
- name: Decidir o que roda
id: gate
env:
DRAFT: ${{ github.event.pull_request.draft }}
FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
run: ./scripts/check-gate.sh "origin/$BASE_REF" >> "$GITHUB_OUTPUT"
- uses: actions/setup-node@v4
with:
node-version: 22
# >= 0.3.5 obrigatório: em 0.3.2 um .pen com fills de imagem relativos
# carrega VAZIO sem erro fatal. scripts/pen-export.sh checa e falha.
- name: Instalar pen.dev CLI
if: steps.gate.outputs.pen == 'true'
run: npm install -g @pen.dev/cli && pen version
# O .pen referencia ./assets/* relativamente, então a versão da base
# precisa da árvore inteira — um worktree, não um `git show` solto.
- name: Worktree da base
if: steps.gate.outputs.design == 'true'
run: git worktree add --detach ../base "origin/$BASE_REF"
- name: Diff do design
id: design
if: steps.gate.outputs.design == 'true'
env:
PEN_CLI_KEY: ${{ secrets.PEN_CLI_KEY }}
RENDER: ${{ steps.gate.outputs.render }}
run: ./scripts/design-diff.sh "../base/$PEN_FILE" "$PEN_FILE" "$WORK/design"
# A evidência do agente, cada arquivo autoridade sobre uma coisa (§14).
# Os digests do head já saíram do passo anterior quando ele rodou.
- name: Evidência do design para a auditoria
id: evidence
if: ${{ !cancelled() && steps.gate.outputs.deep == 'true' }}
env:
PEN_CLI_KEY: ${{ secrets.PEN_CLI_KEY }}
run: |
mkdir -p "$WORK/audit"
if [ -f "$WORK/design/head/tokens.json" ]; then
cp "$WORK"/design/head/{tokens.json,components.json,inventory.txt,screens.tsv} "$WORK/audit/"
else
./scripts/pen-digest.sh "$PEN_FILE" "$WORK/audit"
./scripts/pen-screens.sh "$PEN_FILE" > "$WORK/audit/screens.tsv"
fi
PEN_NODES="$(paste -sd';' "$WORK/audit/inventory.txt")" \
./scripts/pen-export.sh "$PEN_FILE" "$WORK/audit/components.html" 1 html-tailwind
./scripts/pen-outline.py "$PEN_FILE" > "$WORK/audit/screens.json"
ls -la "$WORK/audit"
# Divergência é achado, não falha: o passo só falha se não conseguiu medir.
- name: Medir a app nas páginas do mapa
id: numeric
if: ${{ !cancelled() && steps.evidence.outcome == 'success' }}
continue-on-error: true
run: |
code=0
./scripts/measure-app.sh "$WORK/audit/components.html" "$WORK/audit/screens.tsv" > "$WORK/numeric.md" || code=$?
cat "$WORK/numeric.md"
exit $code
- name: Varredura mecânica
id: scan
if: ${{ !cancelled() && steps.gate.outputs.scan == 'true' }}
run: |
inventory="$WORK/audit/inventory.txt"
[ -f "$inventory" ] || inventory="$WORK/design/head/inventory.txt"
[ -f "$inventory" ] || inventory=""
INVENTORY="$inventory" ./scripts/drift-scan.sh "origin/$BASE_REF" > "$WORK/scan.md"
cat "$WORK/scan.md"
# O agente NÃO posta: escreve human.md e findings.json, e o passo
# "Publicar" publica. Assim a publicação é determinística, deduplica entre
# pushes, e o resultado mecânico sai mesmo se o agente falhar.
#
# github_token é obrigatório aqui. Sem ele a action troca OIDC pelo token
# do app do Claude, e essa troca exige o workflow idêntico ao do branch
# padrão: em PR que mexe neste arquivo, a action sai em 2s com "success"
# sem rodar nada. Como o agente não posta, o token do job basta — e ele já
# está disponível para qualquer workflow de PR do próprio repositório.
- uses: anthropics/claude-code-action@v1
id: agent
if: ${{ !cancelled() && steps.gate.outputs.deep == 'true' }}
continue-on-error: true
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
github_token: ${{ github.token }}
prompt: |
Audite este PR contra o design e descreva a mudança de design. NÃO
poste nada e não use ferramenta de comentário: escreva os dois
arquivos abaixo, e o workflow publica.
As regras estão em design/DESIGN-SYSTEM.md, já carregado via
CLAUDE.md. As dez regras auditadas estão numeradas no §12: use
exatamente esses números.
## Saída
1. /tmp/check/findings.json — SEMPRE, mesmo que vazio ([]):
[{"path": "components/chip.tsx", "line": 9, "rule": 10,
"title": "padding lateral 16px no design, 12px no código",
"body": "O Chip usa $space-4 no .pen. Troque `px-3` por `px-4` (§4)."}]
- path: relativo à raiz do repositório. line: a linha do arquivo
ATUAL onde a correção acontece — no código, nunca no .pen.
- rule: o número da regra do §12. title: uma linha. body: a
correção, citando a seção do doc.
- Arquivo que não mudou neste PR também vale: o workflow decide se
o achado vira comentário inline ou item do resumo. Não procure
uma linha do diff para encaixar o achado.
- Achado é só o que você verificou. Ocorrência do scan que é uso
correto não entra.
2. /tmp/check/human.md — SÓ se /tmp/check/design/summary.md existir.
No máximo 12 linhas, em português, dizendo o que mudou NO DESIGN,
em pixels: "o padding lateral do Chip vai de 12px para 16px", não
"$space-3 -> $space-4". Tela adicionada ou removida, e a rota que
isso cria ou apaga (design/screen-routes.json); composição que
mudou. NÃO diga o que mudar no código — isso é achado, e só existe
se o código de fato não acompanha. Se o design não mudou de
verdade, uma linha dizendo isso.
## Ordem — ela existe para você não gastar turnos
1. /tmp/check/scan.md primeiro. Cada ocorrência é candidata: abra só
aquele arquivo e decida se é violação ou uso correto. Seção que
não aparece passou: não reinvestigue.
2. /tmp/check/numeric.md. Divergência ali é medida, não inferida:
cada uma vira achado da regra 10, na linha que produz o valor.
3. Se /tmp/check/design/summary.md existe, o design mudou e o código
correspondente está defasado até prova em contrário (§13). Para
cada token, componente ou composição que mudou, confira o arquivo
correspondente, mesmo que ele não esteja no diff.
4. Regras 7 e 9, que exigem comparar com o design.
5. human.md, se couber.
PARE CEDO: sem ocorrência no scan, sem divergência no numeric e sem
design/summary.md, confira só a regra 9 nas páginas do diff e termine.
## Evidência, em /tmp/check — cada uma autoridade sobre uma coisa
- design/summary.md, design/screens.md: o que mudou no design
(tokens, componentes, composição; telas por render).
- audit/components.html: geometria e tipografia já em px, por
data-pencil-name. NUNCA use Read nele (~108KB): grep -A3
'data-pencil-name="Chip"'. Só tema claro. Não copie código dali.
- audit/components.json: qual token cada propriedade usa.
- audit/tokens.json: o valor de cada token em light e dark.
- audit/screens.json: a composição de cada tela. NUNCA use Read nele
(passa de 2000 linhas e trunca): jq '.[] | select(.node == "Sign In")'
/tmp/check/audit/screens.json. Liste com jq -r '.[].node'.
- design/screen-routes.json, no repositório: tela -> page.tsx -> URL.
## Escopo
git diff --name-only origin/${{ github.base_ref }}...HEAD
- Mudou código (app/, components/): audite os arquivos alterados.
- Mudou o design: compare com o código correspondente, mesmo fora do diff.
- Os dois: faça as duas coisas.
Não reporte estilo, nomes ou arquitetura fora das dez regras.
claude_args: |
--max-turns 60
--allowedTools "Read,Grep,Glob,Write,Bash(git diff:*),Bash(git log:*),Bash(git show:*),Bash(jq:*),Bash(grep:*),Bash(diff:*),Bash(comm:*),Bash(sed:*),Bash(head:*),Bash(tail:*),Bash(wc:*),Bash(ls:*)"
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() && steps.gate.outputs.publish == 'true' }}
with:
name: design-check-${{ github.event.pull_request.number }}
path: |
${{ env.WORK }}/design/artifact
${{ env.WORK }}/*.md
${{ env.WORK }}/findings.json
if-no-files-found: warn
# PR de fork: o token é só leitura, então o relatório vai para o job
# summary em vez de comentário.
- name: Publicar
id: publish
if: ${{ !cancelled() && steps.gate.outputs.publish == 'true' }}
continue-on-error: true
env:
GITHUB_TOKEN: ${{ github.token }}
CHECK_MODE: ${{ github.event.pull_request.head.repo.full_name != github.repository && 'summary' || 'pr' }}
CHECK_DRAFT: ${{ github.event.pull_request.draft }}
AGENT_OUTCOME: ${{ steps.agent.outcome }}
run: node scripts/publish.mjs "$WORK"
# Vermelho se houver achado, se a auditoria não terminou ou se algum passo
# quebrou. "Não auditou" nunca pode parecer "passou" (§14).
- name: Veredito
if: ${{ !cancelled() }}
env:
OUTCOMES: >-
design=${{ steps.design.outcome }}
evidence=${{ steps.evidence.outcome }}
numeric=${{ steps.numeric.outcome }}
scan=${{ steps.scan.outcome }}
agent=${{ steps.agent.outcome }}
publish=${{ steps.publish.outcome }}
PUBLISHED: ${{ steps.gate.outputs.publish }}
run: |
fail=0
for kv in $OUTCOMES; do
if [ "${kv#*=}" = failure ]; then echo "::error::o passo '${kv%%=*}' falhou"; fail=1; fi
done
if [ "$PUBLISHED" = true ]; then
if [ ! -f "$WORK/verdict.json" ]; then
echo "::error::a publicação não gravou verdict.json"; fail=1
else
cat "$WORK/verdict.json"
if [ "$(jq .findings "$WORK/verdict.json")" -gt 0 ]; then
echo "::error::$(jq .findings "$WORK/verdict.json") achado(s) — veja o comentário do PR"; fail=1
fi
if [ "$(jq .invalid "$WORK/verdict.json")" -gt 0 ]; then echo "::error::achados malformados do agente"; fail=1; fi
if [ "$(jq .agentMissing "$WORK/verdict.json")" = true ]; then echo "::error::o agente não gravou findings.json"; fail=1; fi
if [ "$(jq .postErrors "$WORK/verdict.json")" -gt 0 ]; then echo "::error::a API recusou comentários inline"; fail=1; fi
fi
fi
exit $fail