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
7 changes: 5 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ jobs:
test-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# zizmor[artipacked]: the checkout's credentials are not needed past this step.
persist-credentials: false
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
with:
enable-cache: true
- name: Install workspace
Expand Down
25 changes: 23 additions & 2 deletions .github/workflows/claude-code-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,34 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1
# This job only reads; findings are posted through the GitHub API rather
# than git, so the checkout's credential has no consumer. NOT true of
# claude.yml, where the @claude responder can commit.
persist-credentials: false

# A PR THAT EDITS THIS FILE GETS NO REVIEW, AND THE CHECK STILL SHOWS GREEN.
#
# claude-code-action validates that this workflow file is byte-identical to the
# copy on the default branch, and self-skips when it is not:
#
# Skipping action due to workflow validation: Workflow validation failed. The
# workflow file must exist and have identical content to the version on the
# repository's default branch.
#
# The guard is correct — otherwise a PR could rewrite the reviewer's own
# instructions and have it approve anything. But the skip is a *success*: the job
# completes green in ~20s having reviewed nothing, which is indistinguishable from
# a clean review at a glance.
#
# So: when a PR touches this workflow, check the job duration (a real review takes
# 2-3 minutes) before trusting the green tick, and re-verify the change once it has
# landed on the default branch.
- name: Run Claude Code Review
id: claude-review
uses: anthropics/claude-code-action@v1
uses: anthropics/claude-code-action@6b082c41935b4c8a3b8b0ef85ba4ba4d9eeb8975 # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Post + maintain a managed PR comment with the review (the switch that makes
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/claude.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,17 @@ jobs:
actions: read # Required for Claude to read CI results on PRs
steps:
- name: Checkout repository
uses: actions/checkout@v7
# zizmor: ignore[artipacked]
# The @claude responder can commit and push in response to a mention, so unlike
# claude-code-review.yml this checkout's credential is load-bearing. Removing it
# would be a behaviour change dressed up as hardening.
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 1

- name: Run Claude Code
id: claude
uses: anthropics/claude-code-action@v1
uses: anthropics/claude-code-action@6b082c41935b4c8a3b8b0ef85ba4ba4d9eeb8975 # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}

Expand Down
9 changes: 6 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@ jobs:
if: github.event.repository.private == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: github/codeql-action/init@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# zizmor[artipacked]: the checkout's credentials are not needed past this step.
persist-credentials: false
- uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4
with:
languages: python
- uses: github/codeql-action/analyze@v4
- uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4
34 changes: 31 additions & 3 deletions .github/workflows/deps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,38 @@ jobs:
pip-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: astral-sh/setup-uv@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# zizmor[artipacked]: the checkout's credentials are not needed past this step.
persist-credentials: false
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
- name: Export resolved deps
run: uv export --frozen --format requirements-txt --no-hashes --all-packages > requirements-audit.txt
# Pinning the tool does not freeze what it knows: pip-audit fetches the advisory
# database at run time, so this pins the scanner's behaviour while advisories stay
# current.
- name: pip-audit (informational — Dependabot files the fix PRs)
run: uvx pip-audit -r requirements-audit.txt
run: uvx pip-audit==2.10.1 -r requirements-audit.txt
continue-on-error: true

# pip-audit above is informational and scheduled; this is the blocking PR gate.
# It diffs the dependency graph at base vs head, which GitHub builds from uv.lock
# directly — no committed manifest and no write-scoped token needed, so it covers
# fork PRs too.
dependency-review:
runs-on: ubuntu-latest
# deps.yml also runs on a Monday cron, and dependency-review-action errors out
# when there is no pull request to diff.
if: github.event_name == 'pull_request'
permissions:
contents: read
pull-requests: write # comment-summary-in-pr only; scoped to this job
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# zizmor[artipacked]: the checkout's credentials are not needed past this step.
persist-credentials: false
- uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
with:
fail-on-severity: high
comment-summary-in-pr: on-failure
7 changes: 5 additions & 2 deletions .github/workflows/links.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ jobs:
lychee:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: lycheeverse/lychee-action@v2
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# zizmor[artipacked]: the checkout's credentials are not needed past this step.
persist-credentials: false
- uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
with:
args: "--no-progress './**/*.md'"
fail: false # external links rot; report, don't block
Expand Down
33 changes: 30 additions & 3 deletions .github/workflows/secret-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,44 @@ on:

permissions:
contents: read
pull-requests: write # gitleaks posts a PR comment on a finding

jobs:
gitleaks:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # gitleaks posts a PR comment on a finding; scoped to this job
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# zizmor[artipacked]: the checkout's credentials are not needed past this step.
persist-credentials: false
fetch-depth: 0 # full history so a secret in any commit is caught
- uses: gitleaks/gitleaks-action@v3
- uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Personal account (ubercylon8) needs no GITLEAKS_LICENSE.
# If the repo ever moves to a GitHub org, add a free GITLEAKS_LICENSE secret.

# A second scanner, not a duplicate. gitleaks is regex plus entropy: high recall,
# no verification. TruffleHog with --only-verified calls the provider's API to test
# whether a candidate credential is live, so it reports real leaks rather than
# shapes. The two miss different things, which is why both run.
trufflehog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0 # full history, same reasoning as gitleaks above
# zizmor[artipacked]: the checkout's credentials are not needed past this step.
persist-credentials: false
- uses: trufflesecurity/trufflehog@6f3c981e7b77f235fd2702dd74af25fc4b72bf11 # v3.96.0
with:
# The Lob detector pattern is `test_` plus exactly 35 more characters, so it
# collides with ordinary pytest function names of that length. This repo has
# 27 of them -- test_503_maps_to_api_unavailable_finding,
# test_base_render_finding_redacts_secrets, and so on -- and the collisions
# are reported as confirmed matches, which would break CI on a naming
# coincidence. There is no Lob integration here, so the detector adds no
# coverage. Every other detector stays enabled.
extra_args: --only-verified --exclude-detectors=Lob
9 changes: 7 additions & 2 deletions .github/workflows/semgrep.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,14 @@ jobs:
semgrep:
runs-on: ubuntu-latest
container:
image: semgrep/semgrep
# Pinned by digest: `semgrep/semgrep:latest` is a mutable tag, and this job is a
# blocking gate — a silent image swap changes what the gate enforces.
image: semgrep/semgrep@sha256:bdf7013b2c3634a487671158da77c554f531742326b543a9464d2adf6c433ac8
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# zizmor[artipacked]: the checkout's credentials are not needed past this step.
persist-credentials: false
- name: Semgrep gate (p/python — curated, blocking)
run: semgrep scan --config p/python --error --skip-unknown-extensions
- name: Semgrep advisory (p/security-audit — high-noise, non-blocking)
Expand Down
39 changes: 39 additions & 0 deletions .github/workflows/zizmor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Workflow audit

# zizmor statically audits the workflows themselves — template injection, excessive
# permissions, unpinned actions, credential persistence. The reasoning it checks was
# previously written down only in prose comments across these files; this makes it fail
# a build instead of relying on the next reader noticing.
#
# It found 25 issues on its first run against this repo (17 unpinned action refs, 1
# unpinned container image, 8 credential-persisting checkouts) and reports 0 after the
# change that introduced it, so it lands blocking with no suppression file. The single
# `# zizmor: ignore[artipacked]` in claude.yml carries its reason at the site.

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
zizmor:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# zizmor[artipacked]: the checkout's credentials are not needed past this step.
persist-credentials: false
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7
# Version-pinned for the same reason the actions and the semgrep image are: this
# is a blocking gate, and an unpinned `uvx zizmor` would let a new release change
# what the gate enforces with no commit on this side. Dependabot cannot see inside
# a `run:` block, so bumping this is a deliberate manual step — which is the point.
#
# --format=github surfaces findings as inline annotations. Deliberately NOT
# --format=sarif: zizmor suppresses its non-zero exit in SARIF mode, which would
# turn this gate into a report.
- name: Audit workflows
run: uvx zizmor@1.29.0 --format=github .github/workflows/
Loading
Loading