Skip to content

ci: audit workflows with zizmor, harden checkouts and action pins - #65

Merged
konard merged 2 commits into
mainfrom
issue-64-cd0968b5c246
Sep 3, 2026
Merged

ci: audit workflows with zizmor, harden checkouts and action pins#65
konard merged 2 commits into
mainfrom
issue-64-cd0968b5c246

Conversation

@konard

@konard konard commented Sep 3, 2026

Copy link
Copy Markdown
Member

Closes #64.

.github/workflows/workflows.yml ran actionlint but not zizmor, and there was no .github/zizmor.yml. actionlint validates 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

pipx run zizmor==1.30.0 --min-confidence medium --persona regular .github/workflows

Before: 51 high-severity findings — 49 unpinned-uses and 2 excessive-permissions.
After: No findings to report. Good job! (23 ignored, 16 suppressed).

actionlint (via rhysd/actionlint:1.7.7) still exits 0.

Changes

  1. zizmor job in workflows.yml — runs zizmorcore/zizmor-action@v0.6.2 with --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 as actionlint.

  2. .github/zizmor.ymlunpinned-uses policies: actions/*, github/*, docker/*, astral-sh/*, lycheeverse/*, zizmorcore/* are trusted at tag granularity; '*': hash-pin for everything else. Matches the JS template's config minus changesets/*, which this template does not use.

  3. 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-07
  4. persist-credentials: false on every checkout. All 18 checkouts now state the choice explicitly. The single exception is manual-release, which keeps persist-credentials: true with a comment: scripts/version_and_commit.py runs git push origin main (scripts/version_and_commit.py:236) and needs the token wired into the remote. auto-release does not push — it creates the release through gh with GH_TOKEN — so it no longer persists credentials either.

  5. docs.yml permissions moved per job. pages: write and id-token: write were granted workflow-wide, which handed them to the build job as well; they now sit on the jobs that publish. This was excessive-permissions, the remaining pair of findings.

Deviation from the issue

The issue suggested adding zizmor to the needs: list of pipeline-status. pipeline-status lives in release.yml, a different workflow, and GitHub Actions cannot express a needs: edge across workflows. zizmor fails its own run in the Workflows workflow, the same way actionlint already does, and should be added to branch protection alongside it.

Tests

tests/test_workflows.py gains four regression tests, plus a shared assert_action_hash_pin helper that requires the # <tag> @ <date> annotation:

  • test_workflow_audit_job_runs_zizmor
  • test_zizmor_config_requires_hash_pins_by_default
  • test_every_checkout_declares_credential_persistence — every checkout sets persist-credentials, and at most one keeps it true
  • test_write_permissions_are_granted_per_job — no workflow-level write scope in any workflow

All four fail against main (verified by stashing the .github/ changes) and pass with the fix. Full suite: 92 passed. ruff check, ruff format --check and mypy src are clean.

Adding .gitkeep for PR creation (default mode).
This file will be removed when the task is complete.

Issue: #64
@konard konard self-assigned this Sep 3, 2026
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.
@konard konard changed the title [WIP] workflows.yml runs actionlint but not zizmor: 17 checkouts persist credentials and pypa/gh-action-pypi-publish@release/v1 is unpinned ci: audit workflows with zizmor, harden checkouts and action pins Sep 3, 2026
@konard
konard marked this pull request as ready for review September 3, 2026 19:51
@konard

konard commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Working session summary

All five workflow runs are green on aad2ea6 (the only non-pass checks are release/Docker/Pages jobs that skip on PRs).

Summary

PR: #65 (ready for review, CI green)

The template's Workflows workflow ran actionlint but not zizmor, and actionlint doesn't detect credential persistence, template injection, or unpinned actions — so those defects shipped into every generated repository. zizmor 1.30.0 --min-confidence medium --persona regular reported 51 high-severity findings on main; it now reports zero.

Changes:

  1. zizmor job in workflows.yml (zizmorcore/zizmor-action@v0.6.2, annotations rather than SARIF so it fails loudly in forks without code scanning) plus .github/zizmor.yml — trusted publishers tag-pinned, '*': hash-pin for the rest.
  2. Hash pins, each annotated with tag and date: pypa/gh-action-pypi-publish (was @release/v1, a mutable branch running in the PyPI trusted-publishing job) and codecov/codecov-action.
  3. persist-credentials: false on every checkout. The one exception is manual-release, which keeps credentials because scripts/version_and_commit.py:236 runs git push origin main; it carries a comment saying so. auto-release does not push — it uses gh with GH_TOKEN — so it no longer persists credentials either.
  4. docs.yml Pages write scopes moved per job, clearing the two excessive-permissions findings.

Tests: four new regression tests in tests/test_workflows.py, all verified to fail against main and pass with the fix. Full suite 92 passed; ruff, mypy, and actionlint clean.

One deviation from the issue: it suggested adding zizmor to the needs: of pipeline-status. That job lives in release.yml, a different workflow, and GitHub Actions can't express a needs: edge across workflows. zizmor fails its own run the same way actionlint already does — it should be added to branch protection alongside it, which is a repository setting I can't change from here.


This summary was automatically extracted from the AI working session output.

@konard

konard commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

🤖 Solution Draft Log

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost: $2.421484

📊 Context and tokens usage:

  • 73.8K / 1M (7%) input tokens, 18.2K / 128K (14%) output tokens

Total: (104 new + 65.5K cache writes + 2.6M cache reads) input tokens, 18.2K output tokens, $2.421484 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: opus (claude-opus-5)
  • Thinking level: off (disabled)
  • Model: Claude Opus 5 (claude-opus-5)

📎 Log file uploaded as Gist (1346KB)


Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard
konard merged commit 81c9786 into main Sep 3, 2026
22 checks passed
@konard

konard commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

🎉 Auto-merged

This pull request has been automatically merged by hive-mind.

  • All CI checks have passed

Auto-merged by hive-mind with --auto-merge flag

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.

workflows.yml runs actionlint but not zizmor: 17 checkouts persist credentials and pypa/gh-action-pypi-publish@release/v1 is unpinned

1 participant