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
15 changes: 13 additions & 2 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ on:
- '.github/workflows/docs.yml'
workflow_dispatch:

# Least-privilege default; the write scopes GitHub Pages needs are granted
# per job, to the two jobs that actually publish.
permissions:
contents: read
pages: write
id-token: write

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand All @@ -42,8 +42,14 @@ jobs:
name: Build docs
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
# actions/configure-pages enables the Pages site on push to main.
pages: write
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Setup Python
uses: actions/setup-python@v6
Expand Down Expand Up @@ -86,6 +92,11 @@ jobs:
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && vars.DEPLOY_GITHUB_PAGES == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
# actions/deploy-pages publishes the artifact using an OIDC token.
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/links.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Check links with lychee
id: lychee
Expand Down
27 changes: 24 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0

- name: Setup Python
Expand Down Expand Up @@ -115,6 +116,7 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0

- name: Simulate fresh merge with base branch (PR only)
Expand Down Expand Up @@ -214,6 +216,8 @@ jobs:
)
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Setup Python
uses: actions/setup-python@v6
Expand Down Expand Up @@ -272,7 +276,7 @@ jobs:

- name: Upload coverage to Codecov
if: env.CODECOV_TOKEN != ''
uses: codecov/codecov-action@v7
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 @ 2026-06-07
with:
files: ${{ steps.python_layout.outputs.root }}/coverage.xml
token: ${{ env.CODECOV_TOKEN }}
Expand Down Expand Up @@ -300,6 +304,8 @@ jobs:
)
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Setup Python
uses: actions/setup-python@v6
Expand Down Expand Up @@ -366,6 +372,7 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0

- name: Set up Python
Expand Down Expand Up @@ -453,6 +460,8 @@ jobs:
if: github.event_name == 'pull_request' && needs.detect-changes.outputs.any-code-changed == 'true'
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Detect Dockerfile
id: dockerfile
Expand Down Expand Up @@ -501,6 +510,7 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
fetch-depth: 0

- name: Setup Python
Expand Down Expand Up @@ -573,7 +583,7 @@ jobs:

- name: Publish to PyPI
if: steps.version_check.outputs.should_release == 'true'
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 @ 2026-07-29

- name: Smoke test published package
if: steps.version_check.outputs.should_release == 'true'
Expand Down Expand Up @@ -620,8 +630,13 @@ jobs:
contents: write
id-token: write
steps:
# The only checkout in this repository that keeps credentials in
# .git/config: scripts/version_and_commit.py runs `git push origin main`
# to publish the version bump, which needs the token wired into the
# remote. Every other checkout sets persist-credentials: false.
- uses: actions/checkout@v6
with:
persist-credentials: true
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}

Expand Down Expand Up @@ -705,7 +720,7 @@ jobs:

- name: Publish to PyPI
if: steps.version.outputs.version_committed == 'true' || steps.version.outputs.already_released == 'true'
uses: pypa/gh-action-pypi-publish@release/v1
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2 @ 2026-07-29
with:
packages-dir: ${{ steps.python_layout.outputs.dist_dir }}

Expand Down Expand Up @@ -755,6 +770,8 @@ jobs:
RELEASE_VERSION: ${{ needs.auto-release.outputs.version || needs.manual-release.outputs.version }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Check Docker publish configuration
id: config
Expand Down Expand Up @@ -793,6 +810,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
Expand Down Expand Up @@ -889,6 +908,8 @@ jobs:
docker-publish-build, docker-publish]
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Fail the run when a required job was cancelled or failed
env:
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ jobs:
cancel-in-progress: true
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Setup Python
uses: actions/setup-python@v6
Expand All @@ -45,6 +47,8 @@ jobs:
language: [python, actions]
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Initialize CodeQL
uses: github/codeql-action/init@v4
Expand All @@ -67,6 +71,8 @@ jobs:
pull-requests: write
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

- name: Review dependency changes
uses: actions/dependency-review-action@v5
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/workflows.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ jobs:
contents: read
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

# The Docker image bundles shellcheck and pyflakes, so this lints every
# `run:` block too. A native actionlint binary without shellcheck on
Expand All @@ -40,3 +42,33 @@ jobs:
uses: docker://rhysd/actionlint:1.7.7
with:
args: -color

# actionlint validates workflow schema and shell; it does not detect
# credential persistence, template injection or unpinned actions. zizmor
# covers exactly those, so both linters run on every change under .github/.
# Reproduce locally with:
# pipx run zizmor==1.30.0 --min-confidence medium --persona regular \
# --config .github/zizmor.yml .github/workflows
zizmor:
name: Audit Workflows
runs-on: ubuntu-latest
timeout-minutes: 10
concurrency:
group: check-${{ github.workflow }}-${{ github.ref }}-zizmor
cancel-in-progress: true
permissions:
contents: read
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false

# Annotations instead of SARIF: forks of this template do not necessarily
# have code scanning enabled, and the job should fail loudly either way.
- name: Audit workflow files
uses: zizmorcore/zizmor-action@v0.6.2
with:
advanced-security: false
annotations: true
config: .github/zizmor.yml
min-confidence: medium
15 changes: 15 additions & 0 deletions .github/zizmor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# zizmor configuration - https://docs.zizmor.sh/configuration/
rules:
unpinned-uses:
config:
policies:
# These publishers are trusted at tag granularity: their release tags
# are the reference this template is meant to read at a glance.
# Everything else must be pinned to a full commit hash.
actions/*: ref-pin
github/*: ref-pin
docker/*: ref-pin
astral-sh/*: ref-pin
lycheeverse/*: ref-pin
zizmorcore/*: ref-pin
'*': hash-pin
3 changes: 2 additions & 1 deletion .gitkeep
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# .gitkeep file auto-generated at 2026-08-20T04:56:54.559Z for PR creation at branch issue-60-1431435e7081 for issue https://github.com/link-foundation/python-ai-driven-development-pipeline-template/issues/60
# Updated: 2026-08-28T10:49:28.004Z
# Updated: 2026-08-28T10:49:28.004Z
# Updated: 2026-09-03T19:44:32.240Z
24 changes: 24 additions & 0 deletions changelog.d/20260903_issue_64_workflow_audit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
### Added

- Added a `zizmor` job to the `Workflows` workflow and a `.github/zizmor.yml`
policy file. `actionlint` validates workflow schema and shell; it does not
detect credential persistence, template injection or unpinned actions, so
those defects previously shipped into every repository generated from this
template (closes #64). The policy tag-pins the publishers whose release tags
the template is meant to read at a glance (`actions/*`, `github/*`,
`docker/*`, `astral-sh/*`, `lycheeverse/*`, `zizmorcore/*`) and requires a
full commit hash for everything else.

### Fixed

- Set `persist-credentials: false` on every `actions/checkout` step except the
one in `manual-release`, which pushes the version bump commit and therefore
needs the token in `.git/config`. Previously all 17 checkouts wrote the
`GITHUB_TOKEN` into the working tree, where any later step in the same job
could read it.
- Hash-pinned `pypa/gh-action-pypi-publish` (was `@release/v1`, a mutable
branch executing in the PyPI trusted-publishing job) and
`codecov/codecov-action`, each annotated with the tag and date pinned.
- Moved `pages: write` and `id-token: write` in `docs.yml` from the workflow
level to the two jobs that publish, so the build job no longer carries write
scopes it does not use.
Loading
Loading