From 0ef52b17af1d16c8ec01bfce2afd3df6598ccc58 Mon Sep 17 00:00:00 2001 From: Rods Date: Sun, 29 Mar 2026 11:21:48 -0300 Subject: [PATCH 1/3] docs: update CI test guide with all lessons learned and new features --- claude-doc/step_command_ci_test.md | 165 +++++++++++++++++++++++++---- 1 file changed, 147 insertions(+), 18 deletions(-) diff --git a/claude-doc/step_command_ci_test.md b/claude-doc/step_command_ci_test.md index 9e9d397..2ed0930 100644 --- a/claude-doc/step_command_ci_test.md +++ b/claude-doc/step_command_ci_test.md @@ -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 . + +Contexto: +- Módulo alvo: +- Source sets: +- CI existente: +- 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. @@ -132,14 +158,42 @@ 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 | +| Checkout | **Sem token** — repo público não precisa de auth para fetch | | Set up Python | Versão 3.11 | | Install dependencies | `pip install anthropic` | -| Find uncovered files | `find` nos diretórios `commonMain` e `androidMain` | +| Find uncovered files | `find` nos diretórios `commonMain` e `androidMain` + 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 | + +**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 +``` --- @@ -154,8 +208,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) @@ -175,8 +228,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 → ``` @@ -188,8 +240,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 @@ -200,13 +251,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 → ``` @@ -251,7 +303,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. --- @@ -264,6 +316,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<> "$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:** @@ -295,11 +422,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 --- @@ -328,7 +456,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 From bacc4069b4b214e48b9cc057a71b83de2509e36d Mon Sep 17 00:00:00 2001 From: Rods Date: Sun, 29 Mar 2026 11:27:17 -0300 Subject: [PATCH 2/3] feat: skip workflow when no .kt files changed + update docs --- .github/workflows/generate-tests.yml | 38 ++++++++++++++++++++++------ claude-doc/step_command_ci_test.md | 6 ++++- 2 files changed, 35 insertions(+), 9 deletions(-) diff --git a/.github/workflows/generate-tests.yml b/.github/workflows/generate-tests.yml index feccd21..b92a8bf 100644 --- a/.github/workflows/generate-tests.yml +++ b/.github/workflows/generate-tests.yml @@ -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 }}" @@ -117,17 +139,17 @@ jobs: printf "files<> "$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 ' ') @@ -145,7 +167,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 @@ -173,7 +195,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: diff --git a/claude-doc/step_command_ci_test.md b/claude-doc/step_command_ci_test.md index 2ed0930..16473ea 100644 --- a/claude-doc/step_command_ci_test.md +++ b/claude-doc/step_command_ci_test.md @@ -159,7 +159,8 @@ if: | |------|-----------| | Resolve trigger context | Normaliza `head_sha` e `pr_number` para os dois gatilhos | | Checkout | **Sem token** — repo público não precisa de auth para fetch | -| Set up Python | Versão 3.11 | +| 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` + calcula cobertura antes/depois | | Generate tests | Chama `generate_tests.py` com `CHANGED_FILES` | @@ -167,6 +168,9 @@ if: | | 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 From 1a1a44f142ad06d90dd71cdb97725aa6925bc0ea Mon Sep 17 00:00:00 2001 From: Rods Date: Sun, 29 Mar 2026 11:33:05 -0300 Subject: [PATCH 3/3] feat: limit Claude API calls to modified .kt files in PR mode --- .github/workflows/generate-tests.yml | 28 +++++++++++++++++++++++++--- claude-doc/step_command_ci_test.md | 2 +- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/generate-tests.yml b/.github/workflows/generate-tests.yml index b92a8bf..15f23bd 100644 --- a/.github/workflows/generate-tests.yml +++ b/.github/workflows/generate-tests.yml @@ -91,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" \ @@ -115,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 diff --git a/claude-doc/step_command_ci_test.md b/claude-doc/step_command_ci_test.md index 16473ea..341ac1a 100644 --- a/claude-doc/step_command_ci_test.md +++ b/claude-doc/step_command_ci_test.md @@ -162,7 +162,7 @@ if: | | 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` + calcula cobertura antes/depois | +| 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 | Branch com auto-incremento `cover/test` → `cover/test-1` → ..., `git add --force` + push autenticado via `git remote set-url` |