ci: audit workflows with zizmor, harden checkouts and action pins - #65
Conversation
Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: #64
actionlint validates workflow schema and shell, but does not detect credential persistence, template injection or unpinned actions, so those defects shipped silently into every repository generated from this template (#64). - Add a zizmor job to workflows.yml and .github/zizmor.yml, which tag-pins trusted publishers and hash-pins everything else. - Set persist-credentials: false on every checkout except manual-release, which pushes the version bump and needs the token in .git/config. - Hash-pin pypa/gh-action-pypi-publish (was the mutable release/v1 branch) and codecov/codecov-action, annotated with tag and date. - Grant docs.yml Pages write scopes per job instead of workflow-wide. - Cover all four with regression tests in tests/test_workflows.py.
Working session summaryAll five workflow runs are green on SummaryPR: #65 (ready for review, CI green) The template's Changes:
Tests: four new regression tests in One deviation from the issue: it suggested adding This summary was automatically extracted from the AI working session output. |
🤖 Solution Draft LogThis log file contains the complete execution trace of the AI solution draft process. 💰 Cost: $2.421484📊 Context and tokens usage:
Total: (104 new + 65.5K cache writes + 2.6M cache reads) input tokens, 18.2K output tokens, $2.421484 cost 🤖 Models used:
📎 Log file uploaded as Gist (1346KB)Now working session is ended, feel free to review and add any feedback on the solution draft. |
🎉 Auto-mergedThis pull request has been automatically merged by hive-mind.
Auto-merged by hive-mind with --auto-merge flag |
Closes #64.
.github/workflows/workflows.ymlranactionlintbut notzizmor, and there was no.github/zizmor.yml.actionlintvalidates workflow schema and shell; it does not detect credential persistence, template injection or unpinned actions — so those defects shipped silently into every repository generated from this template.Reproduction
Before: 51 high-severity findings — 49
unpinned-usesand 2excessive-permissions.After:
No findings to report. Good job! (23 ignored, 16 suppressed).actionlint(viarhysd/actionlint:1.7.7) still exits 0.Changes
zizmorjob inworkflows.yml— runszizmorcore/zizmor-action@v0.6.2with--min-confidence medium, using annotations rather than SARIF so it fails loudly in forks that do not have code scanning enabled. It triggers on the same.github/**paths asactionlint..github/zizmor.yml—unpinned-usespolicies:actions/*,github/*,docker/*,astral-sh/*,lycheeverse/*,zizmorcore/*are trusted at tag granularity;'*': hash-pinfor everything else. Matches the JS template's config minuschangesets/*, which this template does not use.Hash pins for the two publishers that fall under the catch-all rule, each annotated with what was pinned and when:
pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 @ 2026-07-29(was@release/v1, a mutable branch running in the PyPI trusted-publishing job)codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 @ 2026-06-07persist-credentials: falseon every checkout. All 18 checkouts now state the choice explicitly. The single exception ismanual-release, which keepspersist-credentials: truewith a comment:scripts/version_and_commit.pyrunsgit push origin main(scripts/version_and_commit.py:236) and needs the token wired into the remote.auto-releasedoes not push — it creates the release throughghwithGH_TOKEN— so it no longer persists credentials either.docs.ymlpermissions moved per job.pages: writeandid-token: writewere granted workflow-wide, which handed them to the build job as well; they now sit on the jobs that publish. This wasexcessive-permissions, the remaining pair of findings.Deviation from the issue
The issue suggested adding
zizmorto theneeds:list ofpipeline-status.pipeline-statuslives inrelease.yml, a different workflow, and GitHub Actions cannot express aneeds:edge across workflows.zizmorfails its own run in theWorkflowsworkflow, the same wayactionlintalready does, and should be added to branch protection alongside it.Tests
tests/test_workflows.pygains four regression tests, plus a sharedassert_action_hash_pinhelper that requires the# <tag> @ <date>annotation:test_workflow_audit_job_runs_zizmortest_zizmor_config_requires_hash_pins_by_defaulttest_every_checkout_declares_credential_persistence— every checkout setspersist-credentials, and at most one keeps ittruetest_write_permissions_are_granted_per_job— no workflow-levelwritescope in any workflowAll four fail against
main(verified by stashing the.github/changes) and pass with the fix. Full suite: 92 passed.ruff check,ruff format --checkandmypy srcare clean.