Skip to content

[6.x] Harden GitHub Actions workflows with zizmor#14687

Merged
jasonvarga merged 22 commits into
6.xfrom
zizmor
May 18, 2026
Merged

[6.x] Harden GitHub Actions workflows with zizmor#14687
jasonvarga merged 22 commits into
6.xfrom
zizmor

Conversation

@jasonvarga
Copy link
Copy Markdown
Member

Introduces zizmor, a static analyser for GitHub Actions, and applies its recommendations to existing workflows.

Changes include:

  • Add Dependabot cooldown to mitigate supply chain attacks by delaying dependency update PRs, giving the community time to identify compromised releases
  • Suppress a zizmor false positive for the pull_request_target trigger in the pull requests workflow (no fork code is checked out, so the pattern is safe)
  • Scope all workflow job permissions to the minimum required rather than relying on overly broad defaults
  • Disable auto-caching in the release workflow to prevent a poisoned cache from contaminating published release artifacts
  • Set persist-credentials: false on all checkout steps to prevent the GitHub token from being exposed to subsequent steps
  • Add a zizmor workflow that runs on every PR and push, uploading findings as SARIF to the Security tab

jasonvarga and others added 13 commits May 18, 2026 11:32
Adds a 7-day cooldown before Dependabot proposes updates to GitHub
Actions dependencies. This gives the community time to discover and
report any supply chain compromise in a newly released version before
it lands in a PR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
zizmor flags pull_request_target as a dangerous trigger because it runs
with write permissions and access to secrets, making it risky when
combined with code checked out from a fork. This workflow never checks
out any code — it only makes GitHub API calls using the built-in token
— so the trigger is safe here.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
GitHub Actions defaults to overly broad token permissions when no
permissions block is defined. Each job now declares only what it needs:

- code-style-lint: contents:read — only checks out and reads code
- pr-title: none — only uses event context variables, no token ops
- release: contents:write — creates releases and uploads assets
- stale: issues:write + pull-requests:write — comments and closes stale items
- tests (php/js): contents:read — checks out and runs tests, no writes
- tests (slack): actions:read — reads workflow conclusion to decide whether to notify

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
actions/setup-node enables dependency caching automatically when
package.json includes a packageManager or devEngines.packageManager
field. A poisoned cache could silently contaminate published release
artifacts. Explicitly disabling it ensures release builds always use
a clean install.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
By default, actions/checkout writes the GitHub token into the local
Git config so subsequent steps can push or pull. Any step after
checkout — including third-party actions — can read that token.
Setting persist-credentials: false removes it immediately after
checkout, limiting the token's exposure to only the checkout step
itself.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Runs zizmor on every push to master/*.x and on all PRs. Results are
uploaded as SARIF and will appear in the Security tab. The workflow is
intentionally added before all findings are resolved so we can see them
surface in the UI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the SARIF upload approach with inline PR annotations, which
surface findings directly in the diff where they're most actionable.
The job will now fail on findings, enabling it to be used as a required
status check to block merges.

Also adds path filtering so the workflow only runs when .github/**.yml
files are changed — zizmor only understands Actions syntax so there's
no value in running it on unrelated changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the archived actions/create-release and actions/upload-release-asset
(×4) with a single softprops/action-gh-release step that handles both release
creation and asset uploads.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Uses the pre-installed gh CLI instead of a third-party action, avoiding
a supply chain dependency for functionality the runner already provides.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tion

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@statamic statamic deleted a comment from github-advanced-security AI May 18, 2026
jasonvarga and others added 9 commits May 18, 2026 14:51
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents redundant runs queuing up when multiple commits are pushed to
the same branch in quick succession. PR-triggered workflows use
cancel-in-progress: true so a new push immediately cancels the prior
run. The tests workflow uses a conditional so only PR runs are cancelled
— scheduled nightly runs are left to complete. Release and stale
workflows are suppressed instead since they trigger on tags/schedule
where concurrent runs are impossible in practice.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a workflow-level `permissions: {}` block to all workflows that
were missing one, ensuring no job can accidentally inherit broad default
permissions. Moves the pull-requests.yml permission down to the job
level where it belongs. Adds explanatory comments to every non-obvious
permission so it's clear why each one is needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Avoids direct template expansion of the secret into the shell script,
which could allow shell metacharacters in the secret value to be
interpreted as commands. Passing it as an env variable means bash
treats it as data, not syntax.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Gives each job an explicit human-readable display name in the GitHub
Actions UI rather than falling back to the job key.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Job keys are already human-readable and match the required status check
names in branch protection rules — adding name: fields would break
those rules without adding value.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The other workflows that added a concurrency block use the pull_request
trigger, where github.ref is the synthetic refs/pull/<n>/merge ref --
already unique per PR, so each PR lands in its own group.

pull-requests.yml uses pull_request_target, which runs in the context of
the base branch, so github.ref resolves to refs/heads/<base> instead.
Every PR opened against the same base shares one group, and
cancel-in-progress would cancel earlier PRs' runs.

Keying the group on github.event.pull_request.number restores per-PR
isolation for this workflow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jasonvarga jasonvarga marked this pull request as ready for review May 18, 2026 21:02
@jasonvarga jasonvarga merged commit 66762e1 into 6.x May 18, 2026
18 checks passed
@jasonvarga jasonvarga deleted the zizmor branch May 18, 2026 21:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant