Skip to content

CI fixa as actions pelo commit, não grava a credencial do checkout e audita os workflows com o zizmor - #257

Open
bdcdo wants to merge 4 commits into
mainfrom
chore/ci-actions-fixadas
Open

bdcdo wants to merge 4 commits into
mainfrom
chore/ci-actions-fixadas

Conversation

@bdcdo

@bdcdo bdcdo commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Problema

Na main de agora (b48bd03), uvx zizmor@1.30.1 --offline .github/workflows acusa 39 achados e sai com código 14:

  • 29 unpinned-uses: as 29 linhas uses: dos dois workflows citam a action por tag (actions/checkout@v7, astral-sh/setup-uv@v10.2.0 e as outras cinco). O dono da action pode mover a tag para outro commit, e o workflow passa a rodar código que ninguém revisou.
  • 8 artipacked: nenhum dos 8 actions/checkout tem persist-credentials: false, e o token do GitHub fica no .git/config do runner durante o job. Os jobs unit e codex-provider sobem artefato.
  • 2 excessive-permissions: pages: write e id-token: write, no nível do docs.yml, valem também para o job build, que roda o código do repositório e não publica nada.

O que muda

Os quatro itens da proposta da issue estão corrigidos, e o zizmor passa a sair com zero achados e código 0.

  • Actions fixadas pelo commit. Cada uses: aponta para o SHA, com a versão em comentário:

    Action Tag usada Commit Comentário
    actions/checkout v7 3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
    actions/setup-python v7 5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
    astral-sh/setup-uv v10.2.0 c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
    actions/upload-artifact v7 cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
    actions/download-artifact v8 9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
    actions/upload-pages-artifact v5 fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
    actions/deploy-pages v5 368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1

    Cada SHA veio de gh api repos/<dono>/<action>/git/ref/tags/<tag>. As sete tags são leves, então o objeto do ref já é o commit, e a tag de versão do comentário aponta para o mesmo commit da tag usada.

  • Checkout sem credencial gravada. persist-credentials: false nos 8 checkouts. Nenhum passo usa essa credencial: os passos rodam uv, ruff, ty, vulture, deptry, pytest, coverage, mkdocs e cp, e o deploy-pages se autentica com o token OIDC no job deploy, que não faz checkout.

  • Permissões do Pages só no deploy. No docs.yml, contents: read fica no nível do workflow, e pages: write e id-token: write passam para o job deploy. Permissão declarada no job substitui a do workflow, e o deploy deixa de receber contents: read, que ele não usa.

  • zizmor no job quality. Passo novo, antes do ruff: uvx zizmor@1.30.1 --offline .github/workflows. Nenhum achado precisou ser ignorado na configuração. Os 15 achados que só as personas pedantic e auditor mostram, como template-injection de matrix.python-version e anonymous-definition, ficam fora da persona padrão e têm a mesma contagem por tipo na base e no ramo.

O CHANGELOG.md ganha a entrada em Segurança, e o CONTRIBUTING.md diz que o CI roda o zizmor e que action nova entra fixada pelo SHA, com persist-credentials: false no checkout.

Decisões

  • Comentário com a versão exata (# v7.0.1), e não com a tag móvel (# v7). É a forma que o próprio Dependabot escreve quando sobe um SHA fixado: o VersionCommenter monta o comentário com most_specific_version_tag_for_sha. Com isso, o primeiro PR dele não troca o formato do comentário, e quem lê o workflow vê qual release o SHA é.
  • zizmor 1.30.1, e não 1.31.1. A 1.31.0 e a 1.31.1 saíram há 6 dias, e o projeto espera 7 dias antes de adotar versão nova, como o cooldown do dependabot.yml. A 1.31.1 também dá zero achados no ramo. O Dependabot não atualiza versão escrita dentro de run:, e por isso a subida do zizmor é manual; o comentário do passo diz isso.
  • --offline. As auditorias online do zizmor consultam a API do GitHub e precisariam de token no job. A correspondência entre SHA e tag, que uma delas confere, foi verificada pela API na preparação deste PR.

O que o CI deste PR não exercita

O docs.yml roda só em push na main e em workflow_dispatch. As permissões novas do job deploy e o checkout sem credencial do job build não rodam neste PR. Como o PR toca docs.yml e CHANGELOG.md, que estão no paths do gatilho, o push do merge dispara o deploy, e é nele que essa parte roda pela primeira vez.

Closes #209

@bdcdo

bdcdo commented Oct 22, 2026

Copy link
Copy Markdown
Owner Author

Evidência

Rodado no head do PR (b5b4e02f) e na main de agora (b48bd034), esta numa worktree --detach à parte. Comandos a partir da raiz do repositório.

Caso na base (b48bd034)

$ uvx zizmor@1.30.1 --offline .github/workflows
(achados agrupados por regra com grep -E '^(error|warning|help|info)\[' | sort | uniq -c)
      2 error[excessive-permissions]: overly broad permissions
     29 error[unpinned-uses]: unpinned action reference
      8 help[artipacked]: credential persistence through GitHub Actions artifacts
54 findings (15 suppressed, 8 unsafe fixes): 0 informational, 8 low, 0 medium, 31 high
exit=14

Por arquivo: tests.yml com 24 unpinned-uses e 7 artipacked; docs.yml com 5 unpinned-uses, 1 artipacked e 2 excessive-permissions (pages: write e id-token: write no nível do workflow). A zizmor@1.31.1 dá o mesmo resumo (54 achados, 15 suprimidos) e o mesmo código 14.

O passo novo reprova esses workflows. No commit que só o acrescenta (b1707f26, base mais o passo zizmor no job quality), o mesmo comando sai com os mesmos 39 achados e código 14.

Mesmo caso no ramo (b5b4e02f)

$ uvx zizmor@1.30.1 --offline .github/workflows
 INFO zizmor: 🌈 zizmor v1.30.1
 INFO audit: zizmor: 🌈 completed .github/workflows/docs.yml
 INFO audit: zizmor: 🌈 completed .github/workflows/tests.yml
No findings to report. Good job! (15 suppressed)
exit=0

Com zizmor@1.31.1, também zero achados e código 0. Os 15 suprimidos são o que só a persona auditor mostra (--persona auditor): 9 anonymous-definition, 4 template-injection de matrix.python-version, 1 concurrency-limits e 1 undocumented-permissions, com a mesma contagem por tipo na base e no ramo.

No CI do PR (run 39703038888, head b5b4e02f), o passo zizmor do job quality imprimiu No findings to report. Good job! (15 suppressed).

Casos cobertos

Caso Esperado Obtido
uses: por tag 29 na base, 0 no ramo 29 → 0. No ramo, as 29 linhas uses: casam com @[0-9a-f]{40} # vX.Y.Z
SHA de cada action o commit da tag usada na base e o da tag do comentário são o SHA fixado 7 de 7, por gh api repos/<dono>/<action>/commits/<tag> (bloco abaixo)
actions/checkout sem persist-credentials: false 8 na base, 0 no ramo 8 → 0 (7 em tests.yml, 1 em docs.yml)
Passo que dependa da credencial do checkout nenhum grep -rnE 'git (push|fetch|pull|clone)|GITHUB_TOKEN|github\.token|(^|[^a-z])gh ' .github src tests mkdocs.yml pyproject.toml sem ocorrência na base e no ramo
Permissões do docs.yml workflow só com contents: read; deploy com pages: write e id-token: write; build herda contents: read igual no ramo. Na base, o workflow tinha as três e os dois jobs herdavam
Permissões do tests.yml contents: read no workflow, nenhum job com bloco próprio igual na base e no ramo
Passo zizmor no job quality reprova a base, passa no ramo código 14 em b1707f26 (local), 0 em b5b4e02f (local e no CI)
Workflows válidos actionlint sem erro uvx --from actionlint-py@1.7.12.25 actionlint -shellcheck= .github/workflows/*.yml com código 0 na base e no ramo
Nada fora dos workflows e da documentação mudou src, tests, pyproject.toml e uv.lock idênticos à base git diff --stat origin/main -- src tests pyproject.toml uv.lock vazio; o diff toca só docs.yml, tests.yml, CHANGELOG.md e CONTRIBUTING.md

Os casos de checkout e de permissões foram lidos com yaml.safe_load dos dois workflows: para cada passo com uses: actions/checkout@..., se with.persist-credentials é False; e o bloco permissions do workflow e de cada job.

SHA fixado contra as tags, pela API do GitHub
$ paste -d' ' \
    <(git show origin/main:.github/workflows/tests.yml origin/main:.github/workflows/docs.yml | grep -oE 'uses: [^ ]+') \
    <(cat .github/workflows/tests.yml .github/workflows/docs.yml | grep -oE 'uses: [^ ]+ # v[0-9.]+') \
  | sort -u | while read -r _ antigo _ novo _ versao; do
      repo=${novo%@*}; sha=${novo#*@}; tag=${antigo#*@}
      echo "$repo $sha $versao:$(gh api repos/$repo/commits/$versao --jq .sha) $tag:$(gh api repos/$repo/commits/$tag --jq .sha)"
    done
actions/checkout 3d3c42e5aac5ba805825da76410c181273ba90b1 v7.0.1:3d3c42e5aac5ba805825da76410c181273ba90b1 v7:3d3c42e5aac5ba805825da76410c181273ba90b1
actions/deploy-pages 368f82528645a54fb793d4d04e342629a3f51346 v5.0.1:368f82528645a54fb793d4d04e342629a3f51346 v5:368f82528645a54fb793d4d04e342629a3f51346
actions/download-artifact 9000827ccba6bdab643e8b6fd33ac0654aef8333 v8.0.2:9000827ccba6bdab643e8b6fd33ac0654aef8333 v8:9000827ccba6bdab643e8b6fd33ac0654aef8333
actions/setup-python 5fda3b95a4ea91299a34e894583c3862153e4b97 v7.0.0:5fda3b95a4ea91299a34e894583c3862153e4b97 v7:5fda3b95a4ea91299a34e894583c3862153e4b97
actions/upload-artifact cf430e030ddbb5b0abf93d22962f4752f3646cd9 v7.0.2:cf430e030ddbb5b0abf93d22962f4752f3646cd9 v7:cf430e030ddbb5b0abf93d22962f4752f3646cd9
actions/upload-pages-artifact fc324d3547104276b827a68afc52ff2a11cc49c9 v5.0.0:fc324d3547104276b827a68afc52ff2a11cc49c9 v5:fc324d3547104276b827a68afc52ff2a11cc49c9
astral-sh/setup-uv c18668ad3cf93ea998bef934396af7bb5c839dc7 v10.2.0:c18668ad3cf93ea998bef934396af7bb5c839dc7 v10.2.0:c18668ad3cf93ea998bef934396af7bb5c839dc7

Testes

Base (b48bd034) Ramo (b5b4e02f)
Suíte inteira, Python 3.11, todos os extras: uv run --python 3.11 --extra dev --extra all --extra search-all --extra polars --extra excel --extra codex pytest --cov=dataframeit --cov-branch 1760 passados, 0 pulados; cobertura 99,96% (código 1 pelo fail_under = 100) 1760 passados, 0 pulados; cobertura 99,96% (código 1 pelo fail_under = 100)
Job unit do CI: uv run --python 3.11 --extra dev --extra all --extra search-all pytest --cov=dataframeit --cov-branch --cov-report= --cov-fail-under=0, venv sem o extra codex 1599 passados, 129 pulados 1599 passados, 129 pulados
Job codex-provider do CI: uv run --python 3.11 --extra dev --extra codex pytest --cov=dataframeit --cov-branch --cov-report= --cov-fail-under=0 1393 passados, 367 pulados 1393 passados, 367 pulados
Job coverage do CI: coverage combine dos dois anteriores e coverage report TOTAL 3298 0 1312 0 100%, código 0 TOTAL 3298 0 1312 0 100%, código 0
Gates do job quality, Python 3.13 com os extras do job: ruff format --check src tests, ruff check src tests, ty check, vulture, deptry src código 0 nos cinco código 0 nos cinco
uv run --python 3.13 --extra docs mkdocs build (o CHANGELOG.md entra no site) código 0, 2 avisos do mkdocs_static_i18n sobre navigation.instant código 0, os mesmos 2 avisos

Na suíte num ambiente só, a cobertura fica em 99,96% na base e no ramo porque as linhas 700 e 701 de src/dataframeit/codex.py, o except ImportError do import de openai_codex.errors, só rodam sem o extra codex instalado. A combinação que o CI faz cobre as duas e chega a 100%. Cada configuração rodou num venv próprio, com o dataframeit importado do src da árvore em teste.

No CI do PR (run 39703038888, head b5b4e02f), os 10 jobs passaram. O job coverage baixou os 4 artefatos pelo download-artifact já fixado, combinou os 4 arquivos e imprimiu TOTAL 3298 0 1312 0 100%.

Risco: a parte que o CI do PR não roda é o docs.yml, que dispara só em push na main e em workflow_dispatch. O PR toca docs.yml e CHANGELOG.md, que estão no paths do gatilho, e por isso o push do merge roda o build sem credencial gravada e o deploy com as permissões só dele pela primeira vez. Se o deploy falhar por permissão, o site fica na versão anterior, sem efeito para quem usa a biblioteca. Desfazer: git revert do merge, ou devolver pages: write e id-token: write ao nível do workflow. Uma action futura que dependa da credencial gravada pelo checkout falha no passo que a usa, e o conserto é ligar persist-credentials só naquele checkout. As actions fixadas continuam com atualização pelo Dependabot, que tem a entrada github-actions no dependabot.yml; a versão do zizmor, escrita dentro de run:, sobe à mão.

SHA revisado: b5b4e02

O zizmor --offline reprova a action referida por tag ou branch no lugar do
SHA, o actions/checkout sem a chave persist-credentials e a permissão de
escrita no nível do workflow. Ele não confere o comentário de versão e aceita
persist-credentials: true, e o CONTRIBUTING passa a dizer que essas duas partes
da convenção ficam com a revisão do PR.

O comentário do passo zizmor troca a razão do --offline pela verdadeira, as
consultas à API do GitHub a cada execução, e nomeia as cinco auditorias que
ficam de fora. O .pre-commit-config.yaml deixa de dizer que src/ e tests/ são
as únicas pastas que o CI confere.
@bdcdo

bdcdo commented Oct 22, 2026

Copy link
Copy Markdown
Owner Author

Revisão independente

A revisão foi feita por três subagentes do Claude sobre o SHA b5b4e02f, um com cada lente: correção, prova e contrato. Cada achado que não era nit passou por três verificadores independentes, que tentaram refutá-lo.

Corrigido em 85b8b79f, que descende do SHA revisado:

  • CONTRIBUTING.md:36 dizia que o passo zizmor reprova a action que entrar sem SHA, sem a versão em comentário ou sem persist-credentials: false. Com o comando do CI, três casos passam com saída 0: a action sem comentário de versão, a action com o comentário errado e o checkout com persist-credentials: true. O texto agora diz o que o gate reprova e deixa o comentário de versão e o false explícito para a revisão do PR:
    • a action referida por tag ou branch no lugar do SHA (saída 14);
    • o checkout sem a chave persist-credentials (saída 13);
    • a permissão de escrita no nível do workflow (saída 14).
  • No mesmo commit, três frases que o diff tinha deixado imprecisas:
    • a entrada do CHANGELOG.md;
    • o comentário do .pre-commit-config.yaml sobre as pastas que o CI confere;
    • o comentário do tests.yml sobre o --offline, que agora nomeia as cinco auditorias online que ficam de fora.

Descartado: o comando audita só .github/workflows, e uma action composta em .github/actions/ passaria sem auditoria. Não existe action composta no repositório, e o comando segue a proposta da issue.

Preexistente, aberto como issue: #268. Nenhum check de PR confere as permissões do job que publica a documentação.

Não executado:

  • O deploy do Pages com as permissões no nível do job, porque o docs.yml só roda em push na main. A disposição nova foi conferida contra o README do actions/deploy-pages no SHA fixado.
  • As auditorias online do zizmor. No lugar delas, os 7 SHAs foram conferidos contra as tags pela API do GitHub.

O merge deste PR dispara o deploy da documentação, porque toca .github/workflows/docs.yml.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CI: fixar as actions por commit e restringir credenciais e permissões dos workflows

1 participant