Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 55 additions & 11 deletions .github/workflows/generate-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,39 @@ jobs:
ref: ${{ steps.ctx.outputs.head_sha }}
fetch-depth: 0

# ── 3. Python ─────────────────────────────────────────────────────────────
# ── 3. Verifica se há arquivos .kt modificados ───────────────────────────
- name: Check for Kotlin file changes
id: kt_guard
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
echo "has_kotlin=true" >> "$GITHUB_OUTPUT"
echo "workflow_dispatch — pulando verificação de .kt"
else
KT=$(git diff --name-only origin/main...HEAD | grep '\.kt$' || true)
if [ -n "$KT" ]; then
echo "has_kotlin=true" >> "$GITHUB_OUTPUT"
echo "Arquivos .kt detectados:"
echo "$KT"
else
echo "has_kotlin=false" >> "$GITHUB_OUTPUT"
echo "Nenhum arquivo .kt modificado. Pulando geração de testes."
fi
fi

# ── 4. Python ─────────────────────────────────────────────────────────────
- name: Set up Python
if: steps.kt_guard.outputs.has_kotlin == 'true'
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install Python dependencies
if: steps.kt_guard.outputs.has_kotlin == 'true'
run: pip install anthropic

# ── 4. Escaneia craftd-core buscando arquivos sem cobertura ──────────────
# ── 5. Escaneia craftd-core buscando arquivos sem cobertura ──────────────
- name: Find uncovered Kotlin files in craftd-core
if: steps.kt_guard.outputs.has_kotlin == 'true'
id: changed
run: |
OVERRIDE="${{ github.event.inputs.override_files }}"
Expand All @@ -69,11 +91,33 @@ jobs:
| grep -v "Test\.kt" | wc -l | tr -d ' ')

if [ -n "$OVERRIDE" ]; then
# Modo manual com arquivos específicos
UNCOVERED=$(echo "$OVERRIDE" | tr ' ' '\n' | grep -v "^$" || true)
UNCOVERED_COUNT=$(echo "$UNCOVERED" | grep -v "^$" | wc -l | tr -d ' ')
echo "Modo override — arquivos informados manualmente:"

elif [ "${{ github.event_name }}" = "workflow_run" ]; then
# Modo automático (PR) — apenas .kt modificados no PR sem teste ainda
echo "Modo PR — apenas arquivos .kt modificados neste PR sem cobertura..."
UNCOVERED=""
while IFS= read -r SRC; do
# Só considera arquivos dentro do craftd-core
if echo "$SRC" | grep -q "android_kmp/craftd-core/src/"; then
TEST=$(echo "$SRC" \
| sed 's|src/commonMain/kotlin/|src/test/java/|' \
| sed 's|src/androidMain/kotlin/|src/test/java/|' \
| sed 's|\.kt$|Test.kt|')
if [ ! -f "$TEST" ]; then
UNCOVERED="$UNCOVERED"$'\n'"$SRC"
fi
fi
done < <(git diff --name-only origin/main...HEAD | grep '\.kt$' | grep -v "Test\.kt" | sort)
UNCOVERED=$(echo "$UNCOVERED" | grep -v "^$" || true)
UNCOVERED_COUNT=$(echo "$UNCOVERED" | grep -v "^$" | wc -l | tr -d ' ')

else
echo "Scan completo — buscando arquivos sem cobertura em craftd-core..."
# Modo workflow_dispatch sem override — scan completo
echo "Scan completo — buscando todos os arquivos sem cobertura em craftd-core..."
UNCOVERED=""
while IFS= read -r SRC; do
TEST=$(echo "$SRC" \
Expand All @@ -93,11 +137,11 @@ jobs:
echo "$UNCOVERED"

COVERED_BEFORE=$((TOTAL - UNCOVERED_COUNT))
COVERED_AFTER=$TOTAL
COVERED_AFTER=$((COVERED_BEFORE + UNCOVERED_COUNT))

if [ "$TOTAL" -gt "0" ]; then
PCT_BEFORE=$(( (COVERED_BEFORE * 100) / TOTAL ))
PCT_AFTER=100
PCT_AFTER=$(( (COVERED_AFTER * 100) / TOTAL ))
else
PCT_BEFORE=0
PCT_AFTER=0
Expand All @@ -117,17 +161,17 @@ jobs:
printf "files<<EOF\n%s\nEOF\n" "$UNCOVERED" >> "$GITHUB_OUTPUT"
fi

# ── 5. Chama Claude API para gerar os testes ─────────────────────────────
# ── 6. Chama Claude API para gerar os testes ─────────────────────────────
- name: Generate unit tests with Claude API
if: steps.changed.outputs.has_changes == 'true'
if: steps.kt_guard.outputs.has_kotlin == 'true' && steps.changed.outputs.has_changes == 'true'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
CHANGED_FILES: ${{ steps.changed.outputs.files }}
run: python .github/scripts/generate_tests.py

# ── 6. Verifica se arquivos foram gerados ─────────────────────────────────
# ── 7. Verifica se arquivos foram gerados ─────────────────────────────────
- name: Check generated files
if: steps.changed.outputs.has_changes == 'true'
if: steps.kt_guard.outputs.has_kotlin == 'true' && steps.changed.outputs.has_changes == 'true'
id: check
run: |
COUNT=$(find android_kmp/craftd-core/src/test -name "*Test.kt" 2>/dev/null | wc -l | tr -d ' ')
Expand All @@ -145,7 +189,7 @@ jobs:
echo "has_tests=false" >> "$GITHUB_OUTPUT"
fi

# ── 7. Cria branch com nome incremental e commita os testes ──────────────
# ── 8. Cria branch com nome incremental e commita os testes ──────────────
- name: Commit generated tests
if: steps.check.outputs.has_tests == 'true'
id: commit
Expand Down Expand Up @@ -173,7 +217,7 @@ jobs:

echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"

# ── 8. Abre PR com os testes gerados e evolução de cobertura ─────────────
# ── 9. Abre PR com os testes gerados e evolução de cobertura ─────────────
- name: Open Pull Request with generated tests
if: steps.check.outputs.has_tests == 'true'
env:
Expand Down
171 changes: 152 additions & 19 deletions claude-doc/step_command_ci_test.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,37 @@ PR aberto
→ Escaneia arquivos sem cobertura
→ Chama Claude API para cada arquivo
→ Gera arquivos *Test.kt
→ Cria branch + abre PR automático com os testes
→ Cria branch cover/test (auto-incremento) + abre PR automático
→ PR body mostra evolução de cobertura (antes vs depois)
```

---

## Como usar este guia em um novo repositório

Se você está replicando este fluxo em um repo diferente, passe as seguintes
instruções para o Claude Code no início da sessão:

```
Quero configurar geração automática de testes com Claude API + GitHub Actions.
Leia o arquivo claude-doc/step_command_ci_test.md do repositório CodandoTV/CraftD
e replique o mesmo fluxo aqui para o módulo <nome-do-modulo>.

Contexto:
- Módulo alvo: <caminho/do/modulo>
- Source sets: <commonMain e/ou androidMain>
- CI existente: <nome exato do workflow de CI>
- Secrets já configurados: ANTHROPIC_API_KEY=sim/não, GH_PAT=sim/não
```

O Claude vai:
1. Analisar os arquivos sem cobertura no módulo
2. Criar `.github/scripts/generate_tests.py`
3. Criar `.github/workflows/generate-tests.yml`
4. Abrir o PR — você só precisa mergear e cadastrar os secrets

---

## Fase 1 — Análise do módulo

Antes de configurar qualquer coisa, entenda o que existe no módulo alvo.
Expand Down Expand Up @@ -132,14 +158,46 @@ if: |
| Step | O que faz |
|------|-----------|
| Resolve trigger context | Normaliza `head_sha` e `pr_number` para os dois gatilhos |
| Checkout | Usa `GH_PAT` (não `GITHUB_TOKEN`) para permitir push |
| Set up Python | Versão 3.11 |
| Checkout | **Sem token** — repo público não precisa de auth para fetch |
| Check for Kotlin changes | Verifica `git diff origin/main...HEAD` — pula tudo se não houver `.kt` modificado. `workflow_dispatch` sempre passa. |
| Set up Python | Versão 3.11 (só roda se houver `.kt` modificado) |
| Install dependencies | `pip install anthropic` |
| Find uncovered files | `find` nos diretórios `commonMain` e `androidMain` |
| Find uncovered files | **`workflow_run`**: só processa `.kt` modificados no PR sem teste. **`workflow_dispatch`** sem override: scan completo. Calcula cobertura antes/depois. |
| Generate tests | Chama `generate_tests.py` com `CHANGED_FILES` |
| Check generated files | Usa `find` (não `git status`) para contar `*Test.kt` |
| Commit tests | `git add --force` + push para nova branch |
| Open PR | Usa `GH_TOKEN: ${{ secrets.GH_PAT }}` |
| Commit tests | Branch com auto-incremento `cover/test` → `cover/test-1` → ..., `git add --force` + push autenticado via `git remote set-url` |
| Open PR | Usa `GH_TOKEN: ${{ secrets.GH_PAT }}` com tabela de evolução de cobertura |

> ⚠️ `workflow_run` não suporta filtros de `paths` nativamente (ao contrário de `push`/`pull_request`).
> O filtro de `.kt` é feito manualmente via `git diff` dentro do job.

**Autenticação no push (crítico):**
```yaml
# Checkout SEM token — repo público não precisa de auth para fetch
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ steps.ctx.outputs.head_sha }}
fetch-depth: 0
# NÃO colocar token aqui — causa "fatal: could not read Username" mesmo em repo público

# Push autentica via remote URL (não via checkout token)
- name: Commit generated tests
run: |
git remote set-url origin https://x-access-token:${{ secrets.GH_PAT }}@github.com/org/repo.git
git push origin "$BRANCH"
```

**Branch com auto-incremento:**
```bash
BASE="cover/test"
BRANCH="$BASE"
N=1
while git ls-remote --exit-code --heads origin "$BRANCH" > /dev/null 2>&1; do
BRANCH="${BASE}-${N}"
N=$((N + 1))
done
```

---

Expand All @@ -154,8 +212,7 @@ Configure em: **Settings → Secrets and variables → Actions → New repositor

### 🔑 `ANTHROPIC_API_KEY` — Chave da API do Claude

**O que é:** A chave que autentica as chamadas à Claude API. Sem ela o script
não consegue chamar o modelo e nenhum teste é gerado.
**O que é:** A chave que autentica as chamadas à Claude API.

**Como obter:**
1. Acesse [console.anthropic.com](https://console.anthropic.com)
Expand All @@ -175,8 +232,7 @@ não consegue chamar o modelo e nenhum teste é gerado.
| `claude-haiku-4-5-20251001` | ~$0.09 | ~$0.25 |
| `claude-opus-4-6` | ~$1.50 | ~$4.50 |

> Use **Haiku** para geração de testes — é ~10x mais barato e suficiente para
> data classes, extension functions, DiffUtil e enums.
> Use **Haiku** para geração de testes — é ~10x mais barato e suficiente.

**Erro comum:** key correta mas conta sem saldo →
```
Expand All @@ -188,8 +244,7 @@ Solução: console.anthropic.com → Plans & Billing → Add credits.

### 🔑 `GH_PAT` — Personal Access Token do GitHub

**O que é:** Um token pessoal do GitHub que permite ao workflow abrir PRs
em nome de um usuário real. Substitui o `GITHUB_TOKEN` padrão do Actions.
**O que é:** Um token pessoal do GitHub que permite ao workflow fazer push e abrir PRs.

**Por que não usar `GITHUB_TOKEN`:**
O `GITHUB_TOKEN` gerado automaticamente pelo Actions tem uma restrição de segurança
Expand All @@ -200,13 +255,14 @@ intencional do GitHub — ele **não pode abrir PRs** que disparariam outros wor
1. Acesse seu perfil no GitHub → **Settings**
2. **Developer settings → Personal access tokens → Tokens (classic)**
3. **Generate new token (classic)**
4. Marque o escopo: `repo` (acesso completo)
4. Marque **apenas** os escopos: `repo` e `workflow`
5. Copie o token (começa com `ghp_...`)

**Boas práticas:**
- Dê um nome descritivo ao token (ex: `craftd-actions-bot`)
- Defina uma data de expiração (90 dias é um bom equilíbrio)
- Guarde o token em local seguro — o GitHub não mostra novamente
- Se o token expirar, crie um novo e atualize o secret `GH_PAT` no repositório

**Erro comum:** usar `GITHUB_TOKEN` no lugar do PAT →
```
Expand Down Expand Up @@ -251,7 +307,7 @@ Para testar antes do merge, use `workflow_dispatch` manualmente.
### ❌ Actions não consegue criar PR (permission error)
**Causa:** `GITHUB_TOKEN` não tem permissão para abrir PRs que disparam workflows.

**Solução:** Criar um PAT pessoal com escopo `repo`, salvar como secret `GH_PAT`
**Solução:** Criar um PAT pessoal com escopo `repo` + `workflow`, salvar como secret `GH_PAT`
e usar `GH_TOKEN: ${{ secrets.GH_PAT }}` no step de criação de PR.

---
Expand All @@ -264,6 +320,81 @@ Com Haiku, $5 cobrem ~50 execuções completas do módulo.

---

### ❌ `token:` no checkout causa "fatal: could not read Username"
**Causa:** Passar o `GH_PAT` como `token:` no step de `actions/checkout` configura
um credential helper que falha mesmo em repos públicos quando o token está
vazio, inválido ou expirado — bloqueando até o simples `git fetch`.

**Solução:** Remover o `token:` do checkout completamente. Para repos públicos,
o fetch não precisa de autenticação. A autenticação só é necessária no `git push`,
e deve ser feita via `git remote set-url`:
```yaml
# ✅ Checkout sem token
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ steps.ctx.outputs.head_sha }}
fetch-depth: 0

# ✅ Push autenticado via remote URL
- name: Commit generated tests
run: |
git remote set-url origin https://x-access-token:${{ secrets.GH_PAT }}@github.com/org/repo.git
git push origin "$BRANCH"
```

---

### ❌ Push rejeitado com "non-fast-forward" em re-runs
**Causa:** O branch `cover/test` já existia no remote de uma execução anterior.
O `git push` padrão não sobrescreve branches divergentes.

**Solução:** Usar auto-incremento no nome do branch — verifica se existe no remote
antes de criar:
```bash
BASE="cover/test"
BRANCH="$BASE"
N=1
while git ls-remote --exit-code --heads origin "$BRANCH" > /dev/null 2>&1; do
BRANCH="${BASE}-${N}"
N=$((N + 1))
done
# Resulta em: cover/test, cover/test-1, cover/test-2, ...
```

---

### ❌ `printf` com variável errada + sem redirecionamento
**Causa:** Ao editar o workflow manualmente, o step de scan ficou assim (errado):
```bash
printf "files<<EOF\n%s\nEOF\n" "$UNCOVER_OUTPUT" # ❌
```
`$UNCOVER_OUTPUT` apontava para o path do arquivo `$GITHUB_OUTPUT`, não para a lista
de arquivos. Além disso, faltava o redirecionamento.

**Solução:**
```bash
printf "files<<EOF\n%s\nEOF\n" "$UNCOVERED" >> "$GITHUB_OUTPUT" # ✅
```

---

### ❌ Repo renomeado quebra o checkout
**Causa:** O repositório `CodandoTV/CraftD-android` foi renomeado para `CodandoTV/CraftD`.
Sem o campo `repository:` explícito no checkout, o Actions usa o nome antigo e falha.

**Solução:** Sempre especificar `repository:` no checkout de workflows que usam `workflow_run`:
```yaml
- name: Checkout
uses: actions/checkout@v4
with:
repository: org/repo # nome atual do repositório
ref: ${{ steps.ctx.outputs.head_sha }}
fetch-depth: 0
```

---

## Fase 6 — Como testar antes do merge

**Opção 1 — Rodar o script localmente:**
Expand Down Expand Up @@ -295,11 +426,12 @@ Deixe o campo vazio para escanear tudo, ou informe arquivos específicos.

Após o workflow rodar, verifique:

1. **Step "Find uncovered Kotlin files"** — lista os arquivos detectados
1. **Step "Find uncovered Kotlin files"** — lista os arquivos detectados e mostra cobertura antes/depois
2. **Step "Generate unit tests with Claude API"** — cada arquivo deve mostrar `[OK] Written: ...`
3. **Step "Check generated files"** — deve mostrar `Found N test file(s)`
4. **Step "Open Pull Request"** — URL do PR gerado aparece no log
5. **PR aberto automaticamente** com os testes em `src/test/java/...`
4. **Step "Commit tests"** — deve mostrar o nome do branch escolhido (`cover/test`, `cover/test-1` etc.)
5. **Step "Open Pull Request"** — URL do PR gerado aparece no log
6. **PR aberto automaticamente** com tabela de evolução de cobertura no body

---

Expand Down Expand Up @@ -328,7 +460,8 @@ Após o workflow rodar, verifique:
- [ ] Criar `.github/scripts/generate_tests.py`
- [ ] Criar `.github/workflows/generate-tests.yml` apontando para o nome correto do CI
- [ ] Adicionar secret `ANTHROPIC_API_KEY` (console.anthropic.com)
- [ ] Adicionar secret `GH_PAT` (PAT com escopo `repo`)
- [ ] Adicionar secret `GH_PAT` (PAT classic com escopos `repo` + `workflow`)
- [ ] **Não colocar `token:` no step de checkout** — usar `git remote set-url` para push
- [ ] Abrir PR com os arquivos do workflow e mergear para `main`
- [ ] Abrir qualquer PR tocando o módulo alvo e acompanhar o Actions
- [ ] Verificar que o PR gerado mostra a tabela de evolução de cobertura
Loading