From 46bb67450029778cbab8e227b7064f39f07e1b79 Mon Sep 17 00:00:00 2001
From: jdx <216188+jdx@users.noreply.github.com>
Date: Tue, 12 May 2026 16:00:12 -0400
Subject: [PATCH] chore(ci): add zizmor workflow for github actions security
analysis (#471)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adds [zizmor](https://github.com/zizmorcore/zizmor) to audit GitHub
Actions workflows for security issues. Runs on push to main and on PRs
that change `.github/workflows/**`. Fails CI on any finding.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---
> [!NOTE]
> **Medium Risk**
> Mostly CI/workflow hardening, but it also changes release automation
(`postversion.sh`) and workflow permissions/credentials behavior, which
could break tagging/publishing if misconfigured.
>
> **Overview**
> Adds a new `zizmor` workflow that runs on PRs/pushes touching
`.github/workflows/**` to security-audit workflows.
>
> Hardens existing workflows by defaulting to least-privilege
`permissions`, setting `actions/checkout` to `persist-credentials:
false`, and adjusting related behavior (e.g., `scripts/postversion.sh`
now runs `gh auth setup-git` so `git push` still works; `ci.yml`
disables `mise-action` caching; `test.yml` avoids interpolating
`steps.bad.outcome` inside a shell string by passing it via env).
>
> Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
d878aee510daaa5a948092861d1408f6120073eb. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).
---------
Co-authored-by: Claude Opus 4.7 (1M context)
---
.github/workflows/check-dist.yml | 2 ++
.github/workflows/ci.yml | 7 +++++++
.github/workflows/codeql-analysis.yml | 2 ++
.github/workflows/release-plz.yml | 1 +
.github/workflows/release.yml | 11 ++++++++---
.github/workflows/test-redacted-env.yml | 5 +++++
.github/workflows/test.yml | 19 ++++++++++++++++++-
.github/workflows/zizmor.yml | 22 ++++++++++++++++++++++
scripts/postversion.sh | 5 +++++
9 files changed, 70 insertions(+), 4 deletions(-)
create mode 100644 .github/workflows/zizmor.yml
diff --git a/.github/workflows/check-dist.yml b/.github/workflows/check-dist.yml
index 4c49be4..14ede83 100644
--- a/.github/workflows/check-dist.yml
+++ b/.github/workflows/check-dist.yml
@@ -32,6 +32,8 @@ jobs:
- name: Checkout
id: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b94ceb2..e7f78fd 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,6 +7,9 @@ on:
- main
- 'releases/*'
+permissions:
+ contents: read
+
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
@@ -20,6 +23,8 @@ jobs:
- name: Checkout
id: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
# `mise.toml` pins both Node and aube; mise-action installs
# whatever's listed there. Reads `package-lock.json`
@@ -28,6 +33,8 @@ jobs:
# npm-flat (rollup's `--configPlugin` resolution
# requires this).
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4
+ with:
+ cache: false
- name: Install Dependencies
id: aube-ci
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index cf9e725..a8f13c6 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -35,6 +35,8 @@ jobs:
- name: Checkout
id: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
- name: Initialize CodeQL
id: initialize
diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml
index 6fbb4d3..f9fff0c 100644
--- a/.github/workflows/release-plz.yml
+++ b/.github/workflows/release-plz.yml
@@ -26,6 +26,7 @@ jobs:
fetch-depth: 0
submodules: recursive
token: ${{ secrets.RELEASE_PLZ_GITHUB_TOKEN }}
+ persist-credentials: false
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4
- run: mise run release-plz
env:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 6dab23e..e6ba3d2 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -5,19 +5,21 @@ on:
types: [closed]
branches: [main]
-permissions:
- contents: write
+permissions: {}
jobs:
release:
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')
runs-on: ubuntu-latest
+ permissions:
+ contents: write
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
+ persist-credentials: false
- name: Setup mise
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4
@@ -30,10 +32,13 @@ jobs:
enhance-release:
needs: [release]
runs-on: ubuntu-latest
+ permissions:
+ contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
fetch-depth: 0
+ persist-credentials: false
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4
- name: Enhance release notes with communique
run: |
@@ -41,4 +46,4 @@ jobs:
communique generate "$TAG_NAME" --github-release
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
- GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_GITHUB_TOKEN }}
\ No newline at end of file
+ GITHUB_TOKEN: ${{ secrets.RELEASE_PLZ_GITHUB_TOKEN }}
diff --git a/.github/workflows/test-redacted-env.yml b/.github/workflows/test-redacted-env.yml
index a7cbd84..03599cb 100644
--- a/.github/workflows/test-redacted-env.yml
+++ b/.github/workflows/test-redacted-env.yml
@@ -7,12 +7,17 @@ on:
branches: [main]
workflow_dispatch:
+permissions:
+ contents: read
+
jobs:
test-redacted-env:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
- name: Create test mise config with sensitive values
run: |
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 21c2cf3..ca6ccc6 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -8,6 +8,9 @@ on: # rebuild any PRs and main branch changes
- main
workflow_dispatch:
+permissions:
+ contents: read
+
concurrency:
group: ${{ github.workflow }}-${{ github.ref_name }}
cancel-in-progress: true
@@ -17,6 +20,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
- uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4
- run: aube install
- run: aubr all
@@ -43,6 +48,8 @@ jobs:
if: ${{ matrix.requirements }}
run: ${{ matrix.requirements }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
- name: Setup mise
uses: ./
with:
@@ -65,6 +72,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
- name: Setup mise
uses: ./
with:
@@ -90,6 +99,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
- name: Setup mise
id: bad
uses: ./
@@ -107,14 +118,18 @@ jobs:
if: ${{ steps.bad.outcome == 'failure' }}
- name: not failed as expected
run: |
- echo "Expected failure but the job was ${{ steps.bad.outcome }}"
+ echo "Expected failure but the job was ${STEPS_BAD_OUTCOME}"
exit 1
if: ${{ steps.bad.outcome != 'failure' }}
+ env:
+ STEPS_BAD_OUTCOME: ${{ steps.bad.outcome }}
custom_cache_key:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
- name: Setup mise with custom cache key
uses: ./
with:
@@ -132,6 +147,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
- name: Setup mise from mise.jdx.dev
uses: ./
with:
diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml
new file mode 100644
index 0000000..90ad35f
--- /dev/null
+++ b/.github/workflows/zizmor.yml
@@ -0,0 +1,22 @@
+name: zizmor
+on:
+ push:
+ branches: [main]
+ paths: ['.github/workflows/**']
+ pull_request:
+ paths: ['.github/workflows/**']
+
+permissions: {}
+
+jobs:
+ zizmor:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
+ with:
+ persist-credentials: false
+ - uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
+ with:
+ advanced-security: false
diff --git a/scripts/postversion.sh b/scripts/postversion.sh
index 7a57ee4..253e80f 100755
--- a/scripts/postversion.sh
+++ b/scripts/postversion.sh
@@ -4,6 +4,11 @@ set -euxo pipefail
VERSION=$(jq -r .version package.json)
MAJOR_VERSION=$(echo "$VERSION" | cut -d. -f1)
+# Configure git to use gh's credential helper. The checkout step uses
+# persist-credentials: false (per zizmor's artipacked audit), so the
+# token isn't written to .git/config and raw `git push` would 403.
+gh auth setup-git
+
# create the version tag (allow it to fail if it already exists)
git tag "v$VERSION" || echo "Tag v$VERSION already exists locally"