Skip to content

Commit a593e93

Browse files
committed
adding post on enforcing immutable actions
1 parent 37e0651 commit a593e93

11 files changed

Lines changed: 147 additions & 0 deletions
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
---
2+
title: 'Enforcing Immutable Actions with Required Workflows'
3+
author: Josh Johanning
4+
date: 2026-04-03 20:00:00 -0500
5+
description: Using a required workflow to enforce that all GitHub Actions in your pull requests reference immutable releases, adding a supply chain security gate at the organization or enterprise level
6+
categories: [GitHub, Actions]
7+
tags: [GitHub, GitHub Actions, Supply Chain Security]
8+
media_subpath: /assets/screenshots/2026-04-03-enforcing-immutable-actions-required-workflows
9+
image:
10+
path: enforcing-immutable-actions-light.png
11+
width: 100%
12+
height: 100%
13+
alt: An immutable actions check failing on a pull request because a workflow references a mutable action release
14+
---
15+
16+
## Overview
17+
18+
[Immutable releases](https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases) are one of the best supply chain security features GitHub offers for actions. When a repository enables immutable releases, published releases cannot be modified or deleted. This prevents an attacker (or compromised account) from force-pushing a tag after users have already pinned to it.
19+
20+
The problem is that there is no built-in policy today to **require** that actions referenced in your workflows use immutable releases. GitHub [recently added](https://github.blog/changelog/2025-08-15-github-actions-policy-now-supports-blocking-and-sha-pinning-actions/) a policy that supports blocking actions and requiring full 40-character SHA pinning, but that only covers commit SHAs. There is nothing for immutable releases specifically.
21+
22+
And there is another subtlety: just because a repository's *current* release is immutable doesn't guarantee that *future* releases will be. A repo owner can disable the immutable releases setting at any time, meaning past releases stay immutable but new ones won't be. So you need a way to continuously verify that the actions you depend on are referencing immutable releases or commit SHAs.
23+
24+
In my [previous post on maintaining open source GitHub Actions](/posts/maintaining-oss-github-actions/#immutable-releases), I mentioned the [`ensure-immutable-actions`](https://github.com/joshjohanning/ensure-immutable-actions) action I built for exactly this purpose. In this post, I will walk through how to set it up as a **required workflow** so that every pull request across your organization (or enterprise) is automatically checked.
25+
26+
## The ensure-immutable-actions Action
27+
28+
The [`joshjohanning/ensure-immutable-actions`](https://github.com/joshjohanning/ensure-immutable-actions) action scans the workflow files in a repository and reports on every action reference it finds. It categorizes each action as:
29+
30+
- ✅ **First-party**: Actions from `actions/*`, `github/*`, and `octokit/*` organizations are automatically allowed by default. These organizations are [excluded in GitHub's own CodeQL immutable actions query pack](https://github.com/github/codeql/blob/main/actions/ql/extensions/immutable-actions-list/ext/immutable_actions.yml). If you want to require that even first-party actions use immutable releases or full SHA references, you can opt in with the `include-first-party` input
31+
- ✅ **Immutable**: Third-party actions referencing an immutable release tag or a full 40-character commit SHA
32+
- ❌ **Mutable**: Third-party actions referencing a mutable release, a major version tag without a release (e.g., `v3`), or a branch name
33+
34+
When `fail-on-mutable` is `true` (the default), the action fails the workflow if any mutable actions are found, preventing the PR from being merged.
35+
36+
### Inputs
37+
38+
| Input | Description | Default |
39+
| ----- | ----------- | ------- |
40+
| `github-token` | GitHub token for API calls | {% raw %}`${{ github.token }}`{% endraw %} |
41+
| `fail-on-mutable` | Fail the workflow if mutable actions are found | `true` |
42+
| `workflows` | Specific workflow files to check (comma-separated). If not specified, checks **all** workflows | All workflows |
43+
| `exclude-workflows` | Workflow files to exclude from checks. Only applies when `workflows` is not specified | - |
44+
| `include-first-party` | Include first-party actions (`actions/*`, `github/*`, `octokit/*`) in immutability checks instead of automatically allowing them | `false` |
45+
46+
The `workflows` and `exclude-workflows` inputs are useful if you want to check only specific workflows or skip certain ones.
47+
48+
## Setting Up the Required Workflow
49+
50+
To enforce this check across all repositories in your organization (or enterprise), you can configure it as a [required workflow](https://docs.github.com/en/enterprise-cloud@latest/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets#require-workflows-to-pass-before-merging) using repository rulesets.
51+
52+
### Step 1: Create the Workflow
53+
54+
First, create the workflow in a shared repository. I use a dedicated [`required-workflows-public`](https://github.com/joshjohanning-org/required-workflows-public) repo for this.
55+
56+
> The [workflow file needs to be in a repository whose visibility](https://docs.github.com/en/enterprise-cloud@latest/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets#using-a-workflow-file) matches the repositories you want to run it in: a public workflow can run on any repository, an internal workflow on internal and private repositories, and a private workflow only on private repositories. If the workflow is in an internal or private repository, you will need to [allow access](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/managing-repository-settings#allowing-access-to-components-in-a-private-repository) to it from other repositories in the organization.
57+
{: .prompt-info }
58+
59+
{% raw %}
60+
```yml
61+
name: Check Action Immutability
62+
63+
on:
64+
pull_request:
65+
66+
permissions:
67+
contents: read
68+
69+
jobs:
70+
check-immutable:
71+
runs-on: ubuntu-latest
72+
steps:
73+
- uses: actions/checkout@v6
74+
75+
- name: Ensure immutable actions
76+
uses: joshjohanning/ensure-immutable-actions@v2
77+
```
78+
{: file='.github/workflows/ensure-immutable-actions.yml'}
79+
{% endraw %}
80+
81+
The workflow triggers on `pull_request`, which covers PR opens, reopens, and synchronization events (new pushes to the PR branch). The action checks out the repository and then scans the workflow files for mutable action references.
82+
83+
### Step 2: Create the Repository Ruleset
84+
85+
Next, create a repository ruleset at the organization level (or enterprise level) that requires this workflow to pass before merging:
86+
87+
1. Navigate to your organization's (or enterprise's) **Settings** → **Rules** → **Rulesets**
88+
2. Create a new ruleset targeting the repositories you want to enforce
89+
3. Set the target branches - I target the **default branch** so the check runs on any PR merging into `main` / `master` / etc.
90+
4. Under **Require workflows to pass before merging**, add the workflow from your shared repository
91+
5. Set the enforcement status to active (or evaluate if you want to see the workflow run but not be blocking)
92+
93+
![Repository ruleset configured to require the immutable actions check workflow](ruleset-config-light.png){: .shadow }{: .light }
94+
![Repository ruleset configured to require the immutable actions check workflow](ruleset-config-dark.png){: .shadow }{: .dark }
95+
_Configuring the required workflow in a repository ruleset_
96+
97+
The ruleset points to the `ensure-immutable-actions.yml`{: .filepath} workflow in the `required-workflows-public` repository on the `main` branch. Every PR in the targeted repositories will now run this check automatically.
98+
99+
### Step 3: Create a PR and Test
100+
101+
Open a pull request in one of the targeted repositories to verify the check runs. The required workflow will appear as a status check on the PR, and the job summary will show the results of the immutability scan.
102+
103+
## What It Looks Like
104+
105+
### All Actions Immutable (SHA Pinned)
106+
107+
When all third-party actions in the repository's workflows are using immutable releases or full SHA references, the check passes:
108+
109+
![Immutable actions check passing with a SHA-pinned third-party action](check-passed-sha-light.png){: .shadow }{: .light }{: width="550" }
110+
![Immutable actions check passing with a SHA-pinned third-party action](check-passed-sha-dark.png){: .shadow }{: .dark }{: width="550" }
111+
_All actions pass: first-party actions are automatically allowed, and the third-party action uses a full SHA reference_
112+
113+
In this example, the `hashicorp/setup-terraform` action is pinned to a full 40-character commit SHA, which is inherently immutable. The `actions/checkout` and `github/codeql-action` actions are first-party and automatically allowed.
114+
115+
### All Actions First-Party
116+
117+
If your workflows only use first-party actions, the check passes with no third-party actions to validate:
118+
119+
![Immutable actions check passing with only first-party actions](check-passed-first-party-light.png){: .shadow }{: .light }{: width="550" }
120+
![Immutable actions check passing with only first-party actions](check-passed-first-party-dark.png){: .shadow }{: .dark }{: width="550" }
121+
_Only first-party actions in the workflow, nothing else to check_
122+
123+
### Mutable Action Detected
124+
125+
When a workflow references an action with a mutable release, the check fails and blocks the PR from merging:
126+
127+
![Immutable actions check failing because a workflow uses a mutable action release](check-failed-mutable-light.png){: .shadow }{: .light }{: width="550" }
128+
![Immutable actions check failing because a workflow uses a mutable action release](check-failed-mutable-dark.png){: .shadow }{: .dark }{: width="550" }
129+
_The check fails: `azure/webapps-deploy@v2` is using a mutable release_
130+
131+
The job summary clearly shows which action is the problem and why. In this case, `azure/webapps-deploy@v2` is flagged as mutable because the release is not immutable. The developer can fix this by switching to a version that has an immutable release, pinning to a commit SHA, or asking the action maintainer to enable immutable releases.
132+
133+
> If you want help pinning actions to full commit SHAs, check out the [`gh-pin-actions`](https://github.com/amenocal/gh-pin-actions) `gh` CLI extension. It can automatically pin all actions in your workflow files to their corresponding SHAs.
134+
{: .prompt-tip }
135+
136+
## Summary
137+
138+
While GitHub's built-in policies cover SHA pinning, there is currently no native policy to require immutable releases for actions. By combining the [`ensure-immutable-actions`](https://github.com/joshjohanning/ensure-immutable-actions) action with a required workflow in a repository ruleset, you can enforce immutable action references across your entire organization or enterprise.
139+
140+
This gives you a continuous enforcement mechanism, not just a point-in-time check. Even if an action maintainer disables immutable releases in the future, the next PR that references a new mutable version of that action will be caught.
141+
142+
> The action itself uses immutable releases! You can verify this on the [releases page](https://github.com/joshjohanning/ensure-immutable-actions/releases). Look for the immutable 🔒 badge on each release.
143+
{: .prompt-info }
144+
145+
For more context on immutable releases and how I use this in my own action maintenance workflow, check out my post on [how I maintain my open source GitHub Actions](/posts/maintaining-oss-github-actions/#immutable-releases).
146+
147+
Constant vigilance! 👀
249 KB
Loading
246 KB
Loading
170 KB
Loading
169 KB
Loading
196 KB
Loading
195 KB
Loading
321 KB
Loading
320 KB
Loading
42 KB
Loading

0 commit comments

Comments
 (0)