Skip to content

Commit a6b9fec

Browse files
wochingeclaude
andcommitted
ci: harden GitHub Actions workflows with zizmor
- Add `permissions: {}` defaults to ci, dependabot-rebase-stale, package-availability-check workflows - Add `persist-credentials: false` to all checkout steps - Move `${{ }}` interpolations in run blocks to env vars (release.yml) - Replace `softprops/action-gh-release` with `gh release create` - Switch claude-review from `pull_request_target` to `pull_request` with fork check - Replace spoofable `github.actor` check with `user.id` for dependabot - Add zizmor CI workflow for ongoing monitoring - Add `lookup-only: true` to mypy cache (type-checking job) - Disable uv cache in release workflow (publishes to PyPI) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent cfbe7a3 commit a6b9fec

File tree

8 files changed

+107
-36
lines changed

8 files changed

+107
-36
lines changed

.github/workflows/ci.yml

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ on:
1010
branches:
1111
- "*"
1212

13+
permissions: {}
14+
1315
concurrency:
1416
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
1517
cancel-in-progress: true
@@ -19,12 +21,14 @@ jobs:
1921
runs-on: blacksmith-2vcpu-ubuntu-2404
2022
steps:
2123
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
24+
with:
25+
persist-credentials: false
2226
- name: Install uv and set Python version
2327
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
2428
with:
2529
version: "0.11.2"
2630
python-version: "3.13"
27-
enable-cache: true
31+
enable-cache: true # zizmor: ignore[cache-poisoning] CI-only, no artifacts published
2832
- name: Install dependencies
2933
run: uv sync --locked
3034
- name: Run Ruff
@@ -34,13 +38,15 @@ jobs:
3438
runs-on: blacksmith-2vcpu-ubuntu-2404
3539
steps:
3640
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
41+
with:
42+
persist-credentials: false
3743
- name: Install uv and set Python version
3844
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
3945
with:
4046
version: "0.11.2"
4147
python-version: "3.13"
42-
enable-cache: true
43-
- uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
48+
enable-cache: true # zizmor: ignore[cache-poisoning] CI-only, no artifacts published
49+
- uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5 # zizmor: ignore[cache-poisoning]
4450
name: Cache mypy cache
4551
with:
4652
path: ./.mypy_cache
@@ -73,12 +79,14 @@ jobs:
7379
name: Unit tests on Python ${{ matrix.python-version }}
7480
steps:
7581
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
82+
with:
83+
persist-credentials: false
7684
- name: Install uv and set Python version
7785
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
7886
with:
7987
version: "0.11.2"
8088
python-version: ${{ matrix.python-version }}
81-
enable-cache: true
89+
enable-cache: true # zizmor: ignore[cache-poisoning] CI-only, no artifacts published
8290

8391
- name: Check Python version
8492
run: python --version
@@ -134,12 +142,14 @@ jobs:
134142
name: ${{ matrix.job_name }}
135143
steps:
136144
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
145+
with:
146+
persist-credentials: false
137147
- name: Install uv and set Python version
138148
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
139149
with:
140150
version: "0.11.2"
141151
python-version: "3.13"
142-
enable-cache: true
152+
enable-cache: true # zizmor: ignore[cache-poisoning] CI-only, no artifacts published
143153
- name: Install the project dependencies
144154
run: uv sync --locked
145155
- name: Check uv Python version

.github/workflows/claude-review-maintainer-prs.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
name: Claude Review on Maintainer PRs
22

33
on:
4-
pull_request_target:
4+
pull_request:
55
types:
66
- opened
77
- ready_for_review
88

99
jobs:
1010
comment:
11-
if: github.event.pull_request.draft == false
11+
# Only run on PRs that are not drafts and are from the same repository (i.e., not from forks)
12+
if: github.event.pull_request.draft == false && github.event.pull_request.head.repo.full_name == github.repository
1213
runs-on: ubuntu-latest
1314
permissions:
1415
issues: write

.github/workflows/codeql.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ jobs:
5656
steps:
5757
- name: Checkout repository
5858
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
59+
with:
60+
persist-credentials: false
5961

6062
# Initializes the CodeQL tools for scanning.
6163
- name: Initialize CodeQL

.github/workflows/dependabot-merge.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ permissions:
1111
jobs:
1212
dependabot:
1313
runs-on: ubuntu-latest
14-
if: ${{ github.actor == 'dependabot[bot]' }}
14+
if: github.event.pull_request.user.id == 49699333 # dependabot[bot]
1515
steps:
1616
- name: Dependabot metadata
1717
id: metadata

.github/workflows/dependabot-rebase-stale.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ on:
66
- main
77
workflow_dispatch:
88

9+
permissions: {}
10+
911
jobs:
1012
rebase-dependabot:
1113
runs-on: ubuntu-latest

.github/workflows/package-availability-check.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ on:
55
- cron: "*/30 * * * *"
66
workflow_dispatch:
77

8+
permissions: {}
9+
810
jobs:
911
build:
1012
runs-on: ubuntu-latest

.github/workflows/release.yml

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -47,32 +47,35 @@ jobs:
4747
steps:
4848
- name: Verify branch
4949
run: |
50-
if [ "${{ github.ref }}" != "refs/heads/main" ]; then
50+
if [ "${GITHUB_REF}" != "refs/heads/main" ]; then
5151
echo "❌ Error: Releases can only be triggered from main branch"
52-
echo "Current ref: ${{ github.ref }}"
52+
echo "Current ref: ${GITHUB_REF}"
5353
exit 1
5454
fi
5555
5656
- name: Confirm major release
5757
if: ${{ inputs.version == 'major' || inputs.version == 'premajor' }}
5858
run: |
59-
if [ "${{ inputs.confirm_major }}" != "RELEASE MAJOR" ]; then
59+
if [ "${INPUTS_CONFIRM_MAJOR}" != "RELEASE MAJOR" ]; then
6060
echo "❌ For major/premajor releases, set confirm_major to RELEASE MAJOR"
6161
exit 1
6262
fi
63+
env:
64+
INPUTS_CONFIRM_MAJOR: ${{ inputs.confirm_major }}
6365

6466
- name: Checkout repository
6567
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
6668
with:
6769
fetch-depth: 0
6870
token: ${{ secrets.GH_ACCESS_TOKEN }}
71+
persist-credentials: false
6972

7073
- name: Install uv and set Python version
7174
uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8
7275
with:
7376
version: "0.11.2"
7477
python-version: "3.12"
75-
enable-cache: true
78+
enable-cache: false
7679

7780
- name: Configure Git
7881
env:
@@ -94,10 +97,10 @@ jobs:
9497
- name: Calculate new version
9598
id: new-version
9699
run: |
97-
current_version="${{ steps.current-version.outputs.version }}"
98-
version_type="${{ inputs.version }}"
99-
prerelease_type="${{ inputs.prerelease_type }}"
100-
prerelease_increment="${{ inputs.prerelease_increment }}"
100+
current_version="${STEPS_CURRENT_VERSION_OUTPUTS_VERSION}"
101+
version_type="${INPUTS_VERSION}"
102+
prerelease_type="${INPUTS_PRERELEASE_TYPE}"
103+
prerelease_increment="${INPUTS_PRERELEASE_INCREMENT}"
101104
102105
# Extract base version (strip any pre-release suffix like a1, b2, rc1)
103106
base_version=$(echo "$current_version" | sed -E 's/(a|b|rc)[0-9]+$//')
@@ -195,31 +198,42 @@ jobs:
195198
echo "version=$new_version" >> $GITHUB_OUTPUT
196199
echo "is_prerelease=$is_prerelease" >> $GITHUB_OUTPUT
197200
echo "New version: $new_version (prerelease: $is_prerelease)"
201+
env:
202+
STEPS_CURRENT_VERSION_OUTPUTS_VERSION: ${{ steps.current-version.outputs.version }}
203+
INPUTS_VERSION: ${{ inputs.version }}
204+
INPUTS_PRERELEASE_TYPE: ${{ inputs.prerelease_type }}
205+
INPUTS_PRERELEASE_INCREMENT: ${{ inputs.prerelease_increment }}
198206

199207
- name: Check if tag already exists
200208
run: |
201-
if git rev-parse "v${{ steps.new-version.outputs.version }}" >/dev/null 2>&1; then
202-
echo "❌ Error: Tag v${{ steps.new-version.outputs.version }} already exists"
209+
if git rev-parse "v${STEPS_NEW_VERSION_OUTPUTS_VERSION}" >/dev/null 2>&1; then
210+
echo "❌ Error: Tag v${STEPS_NEW_VERSION_OUTPUTS_VERSION} already exists"
203211
exit 1
204212
fi
205-
echo "✅ Tag v${{ steps.new-version.outputs.version }} does not exist"
213+
echo "✅ Tag v${STEPS_NEW_VERSION_OUTPUTS_VERSION} does not exist"
214+
env:
215+
STEPS_NEW_VERSION_OUTPUTS_VERSION: ${{ steps.new-version.outputs.version }}
206216

207217
- name: Update version in pyproject.toml
208218
run: |
209-
uv version ${{ steps.new-version.outputs.version }}
219+
uv version ${STEPS_NEW_VERSION_OUTPUTS_VERSION}
220+
env:
221+
STEPS_NEW_VERSION_OUTPUTS_VERSION: ${{ steps.new-version.outputs.version }}
210222

211223
- name: Verify version consistency
212224
run: |
213225
pyproject_version=$(uv version --short)
214226
215227
echo "pyproject.toml version: $pyproject_version"
216228
217-
if [ "$pyproject_version" != "${{ steps.new-version.outputs.version }}" ]; then
229+
if [ "$pyproject_version" != "${STEPS_NEW_VERSION_OUTPUTS_VERSION}" ]; then
218230
echo "❌ Error: Version in files doesn't match expected version"
219231
exit 1
220232
fi
221233
222234
echo "✅ Versions are consistent: $pyproject_version"
235+
env:
236+
STEPS_NEW_VERSION_OUTPUTS_VERSION: ${{ steps.new-version.outputs.version }}
223237

224238
- name: Build package
225239
run: uv build --no-sources
@@ -250,14 +264,16 @@ jobs:
250264
ls -lh dist/
251265
252266
# Verify the version in the built artifacts matches
253-
expected_version="${{ steps.new-version.outputs.version }}"
267+
expected_version="${STEPS_NEW_VERSION_OUTPUTS_VERSION}"
254268
wheel_file=$(ls dist/*.whl | head -1)
255269
if ! echo "$wheel_file" | grep -q "$expected_version"; then
256270
echo "❌ Error: Wheel filename doesn't contain expected version $expected_version"
257271
echo "Wheel file: $wheel_file"
258272
exit 1
259273
fi
260274
echo "✅ Artifact version verified"
275+
env:
276+
STEPS_NEW_VERSION_OUTPUTS_VERSION: ${{ steps.new-version.outputs.version }}
261277

262278
- name: Smoke test wheel
263279
run: |
@@ -270,32 +286,38 @@ jobs:
270286
- name: Commit version changes
271287
run: |
272288
git add pyproject.toml uv.lock
273-
git commit -m "chore: release v${{ steps.new-version.outputs.version }}"
289+
git commit -m "chore: release v${STEPS_NEW_VERSION_OUTPUTS_VERSION}"
290+
env:
291+
STEPS_NEW_VERSION_OUTPUTS_VERSION: ${{ steps.new-version.outputs.version }}
274292

275293
- name: Create and push tag
276294
id: push-tag
277295
run: |
278-
git tag "v${{ steps.new-version.outputs.version }}"
296+
git tag "v${STEPS_NEW_VERSION_OUTPUTS_VERSION}"
279297
git push origin main
280-
git push origin "v${{ steps.new-version.outputs.version }}"
298+
git push origin "v${STEPS_NEW_VERSION_OUTPUTS_VERSION}"
299+
env:
300+
STEPS_NEW_VERSION_OUTPUTS_VERSION: ${{ steps.new-version.outputs.version }}
281301

282302
- name: Publish to PyPI
283303
id: publish-pypi
284304
run: uv publish --trusted-publishing always
285305

286306
- name: Create GitHub Release
287-
id: create-release
288-
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
289-
with:
290-
tag_name: v${{ steps.new-version.outputs.version }}
291-
name: v${{ steps.new-version.outputs.version }}
292-
generate_release_notes: true
293-
prerelease: ${{ steps.new-version.outputs.is_prerelease == 'true' }}
294-
files: |
295-
dist/*.whl
296-
dist/*.tar.gz
307+
run: |
308+
prerelease_flag=""
309+
if [ "${IS_PRERELEASE}" = "true" ]; then
310+
prerelease_flag="--prerelease"
311+
fi
312+
gh release create "v${VERSION}" \
313+
--title "v${VERSION}" \
314+
--generate-notes \
315+
$prerelease_flag \
316+
dist/*.whl dist/*.tar.gz
297317
env:
298-
GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
318+
GH_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
319+
VERSION: ${{ steps.new-version.outputs.version }}
320+
IS_PRERELEASE: ${{ steps.new-version.outputs.is_prerelease }}
299321

300322
- name: Notify Slack on success
301323
if: success()

.github/workflows/zizmor.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
name: Check GitHub Actions
3+
4+
on:
5+
workflow_dispatch:
6+
push:
7+
branches:
8+
- "main"
9+
merge_group:
10+
pull_request:
11+
branches:
12+
- "main"
13+
14+
permissions: {}
15+
16+
jobs:
17+
zizmor:
18+
name: Check GitHub Actions security
19+
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
20+
runs-on: ubuntu-latest
21+
permissions:
22+
security-events: write
23+
contents: read
24+
steps:
25+
- name: Checkout
26+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
27+
with:
28+
persist-credentials: false
29+
- name: Run zizmor
30+
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
31+
with:
32+
advanced-security: true

0 commit comments

Comments
 (0)