From 3a95af4cc39a312eba3693c8d76a92a3f5fc82ae Mon Sep 17 00:00:00 2001 From: "Michael Peters Jr." Date: Mon, 31 Aug 2026 14:48:33 -0700 Subject: [PATCH 1/7] feat: add zizmor static analysis gate for GitHub Actions workflows Runs zizmorcore/zizmor-action on PRs and pushes to main that touch .github/workflows/**, uploading results to code scanning. First step toward the cleanup tracked in #76. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/zizmor.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 .github/workflows/zizmor.yml diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000..0e04a86 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,28 @@ +name: zizmor + +on: + push: + branches: [main] + paths: + - '.github/workflows/**' + pull_request: + paths: + - '.github/workflows/**' + +permissions: + security-events: write + contents: read + actions: read + +jobs: + zizmor: + name: zizmor + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Run zizmor + uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3 From 34e9f5d66c2d0bab8cc8807bca2ceb7751bf8e76 Mon Sep 17 00:00:00 2001 From: "Michael Peters Jr." Date: Tue, 1 Sep 2026 11:28:23 -0700 Subject: [PATCH 2/7] fix: use GitHub annotations instead of SARIF for zizmor findings SARIF/code scanning requires a GitHub Advanced Security license, which most private consumer repos don't have. Standardize on the `github` annotation format so this workflow behaves identically everywhere it's adopted. Drops the now-unused security-events permission. The annotation format propagates zizmor's real exit code (unlike SARIF, which always exits 0), so continue-on-error is added temporarily until the findings tracked in #76 are cleaned up. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/zizmor.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 0e04a86..473c194 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -10,7 +10,6 @@ on: - '.github/workflows/**' permissions: - security-events: write contents: read actions: read @@ -24,5 +23,16 @@ jobs: with: persist-credentials: false + # Standardized on GitHub annotations (not SARIF/code scanning) so this + # workflow behaves the same in every consumer repo, most of which are + # private and don't have a GitHub Advanced Security license. + # + # continue-on-error is temporary: the repo currently has real findings + # (see #76), and unlike SARIF, the `github` annotation format propagates + # zizmor's real exit code. Remove this once the cleanup in #76 lands. - name: Run zizmor uses: zizmorcore/zizmor-action@70fb788f84895a7701f5643d103d587e460b5c99 # v0.6.3 + continue-on-error: true + with: + advanced-security: false + annotations: true From d6fc04d732d9501a1c40042dfd40337e1ae03374 Mon Sep 17 00:00:00 2001 From: "Michael Peters Jr." Date: Tue, 1 Sep 2026 11:34:34 -0700 Subject: [PATCH 3/7] feat: post a PR summary comment with zizmor findings Annotations land on individual lines and are easy to miss, so add a second job that runs zizmor a second time for JSON output, builds a rule/severity breakdown, and posts (or updates, keyed on a marker comment) a single summary comment on the PR. Split into its own job with only the permissions it needs (pull-requests: write) rather than granting that at the workflow level, per zizmor's own excessive-permissions finding on the first draft of this change. Also adds a concurrency group and explanatory comments on the remaining permissions. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/zizmor.yml | 80 ++++++++++++++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 3 deletions(-) diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 473c194..82a55ac 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -9,14 +9,19 @@ on: paths: - '.github/workflows/**' -permissions: - contents: read - actions: read +permissions: {} # each job below grants only what it needs + +concurrency: + group: zizmor-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: zizmor: name: zizmor runs-on: ubuntu-latest + permissions: + contents: read # to check out the repo + actions: read # for zizmor's online audits (e.g. archived-uses, known-vulnerable-actions) steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -36,3 +41,72 @@ jobs: with: advanced-security: false annotations: true + + comment: + name: PR summary comment + needs: zizmor + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + permissions: + contents: read + actions: read # for zizmor's online audits, same as the zizmor job + pull-requests: write # to post/update the findings summary comment + steps: + - name: Checkout repository + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # Annotations from the zizmor job land on individual lines and are easy + # to miss, so also post/update a single summary comment on the PR. + # Zizmor is run a second time here (not via zizmor-action, which has no + # JSON output option) purely to build that summary; it's informational + # only and doesn't affect the zizmor job's pass/fail result. + - name: Generate zizmor summary + env: + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + pipx install zizmor + zizmor --format=json . > /tmp/zizmor.json || true + + { + echo "" + echo "### 🌈 zizmor findings" + echo + total=$(jq 'length' /tmp/zizmor.json) + if [ "$total" -eq 0 ]; then + echo "No findings. Good job!" + else + echo "Found **${total}** finding(s) across the workflows in this PR." + echo + echo "| Rule | Severity | Count |" + echo "|---|---|---|" + jq -r ' + group_by(.ident + "|" + .determinations.severity) + | map({rule: .[0].ident, severity: .[0].determinations.severity, count: length}) + | sort_by(-.count) + | .[] + | "| `\(.rule)` | \(.severity) | \(.count) |" + ' /tmp/zizmor.json + fi + echo + echo "See the [\`zizmor\` job run](${RUN_URL}) for details, or the [audit docs](https://docs.zizmor.sh/audits/)." + } > /tmp/zizmor_comment.md + + - name: Post or update PR summary comment + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + existing_id=$(gh api "repos/${REPO}/issues/${PR_NUMBER}/comments" \ + --jq '[.[] | select(.body | startswith(""))][0].id // empty') + + if [ -n "$existing_id" ]; then + gh api --method PATCH "repos/${REPO}/issues/comments/${existing_id}" \ + -f body=@/tmp/zizmor_comment.md + else + gh pr comment "${PR_NUMBER}" \ + --repo "${REPO}" \ + --body-file /tmp/zizmor_comment.md + fi From cfc77d9890ae4bc5f23c42e94771f5c995c502cf Mon Sep 17 00:00:00 2001 From: "Michael Peters Jr." Date: Tue, 1 Sep 2026 11:35:27 -0700 Subject: [PATCH 4/7] chore: trigger CI to verify sticky comment update Co-Authored-By: Claude Sonnet 5 From 2e7f31bea0b399702ff0b536138894f0cfe6a18c Mon Sep 17 00:00:00 2001 From: "Michael Peters Jr." Date: Tue, 1 Sep 2026 11:36:34 -0700 Subject: [PATCH 5/7] fix: use gh api -F (not -f) to read the comment body from file -f/--raw-field only accepts literal string values; -F/--field is required for the "@" file-read syntax. The lowercase flag was posting the literal string "@/tmp/zizmor_comment.md" as the comment body instead of its contents, caught by testing the update path on PR #78. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/zizmor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index 82a55ac..b3055c6 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -104,7 +104,7 @@ jobs: if [ -n "$existing_id" ]; then gh api --method PATCH "repos/${REPO}/issues/comments/${existing_id}" \ - -f body=@/tmp/zizmor_comment.md + -F body=@/tmp/zizmor_comment.md else gh pr comment "${PR_NUMBER}" \ --repo "${REPO}" \ From 6ca4deef9b0e262c1167032a16e26274e2bbed29 Mon Sep 17 00:00:00 2001 From: "Michael Peters Jr." Date: Tue, 1 Sep 2026 11:37:26 -0700 Subject: [PATCH 6/7] chore: trigger CI to verify sticky comment update works end to end Co-Authored-By: Claude Sonnet 5 From 47eb228fd5873f1aa2683e5df9ce6c1e0a462f85 Mon Sep 17 00:00:00 2001 From: "Michael Peters Jr." Date: Tue, 1 Sep 2026 11:39:45 -0700 Subject: [PATCH 7/7] fix: sort PR summary comment by severity, then count Order rows high -> low severity, breaking ties by finding count descending, instead of count alone. Makes the highest-risk rows easiest to spot at the top of the table. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/zizmor.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml index b3055c6..47fd8b7 100644 --- a/.github/workflows/zizmor.yml +++ b/.github/workflows/zizmor.yml @@ -82,9 +82,12 @@ jobs: echo "| Rule | Severity | Count |" echo "|---|---|---|" jq -r ' + def severity_rank: + {"High": 0, "Medium": 1, "Low": 2, "Informational": 3}[.] // 4; group_by(.ident + "|" + .determinations.severity) | map({rule: .[0].ident, severity: .[0].determinations.severity, count: length}) - | sort_by(-.count) + | map(. + {rank: (.severity | severity_rank)}) + | sort_by(.rank, -.count) | .[] | "| `\(.rule)` | \(.severity) | \(.count) |" ' /tmp/zizmor.json