Workflow Linting with actionlint
Workflow Linting with actionlint
This document describes the actionlint pre-push hook and CI gate added to prevent workflow configuration errors from reaching CI.
Background
Broken workflow YAML is a high-impact failure mode: when .github/workflows/ci.yml or a related file is misconfigured, all CI stops until the error is found and fixed. A 5-hour CI incident prompted the introduction of two complementary safety nets that catch these errors as early as possible in the development cycle.
Safety Net 1 — Pre-Push Hook (.pre-commit-config.yaml)
The repo root contains a pre-commit configuration that runs actionlint as a pre-push hook:
repos: - repo: https://github.com/rhysd/actionlint rev: v1.7.7 hooks: - id: actionlintSetup
Install the pre-commit framework (once per machine):
pip install pre-commit# orbrew install pre-commitThen install the hooks from the repo root:
pre-commit installThe default_install_hook_types: [pre-push] key in .pre-commit-config.yaml tells pre-commit to install the pre-push hook automatically — no --hook-type pre-push flag required. The stages: [pre-push] entry on the actionlint hook ensures it only fires on git push, not on every commit.
To run it manually against all workflow files at any time:
pre-commit run actionlint --all-filesSafety Net 2 — lint-workflows CI Gate (.github/workflows/lint-workflows.yml)
A standalone workflow that runs actionlint in CI on every push and pull request that touches .github/workflows/** or .github/actions/**:
name: Lint Workflows
on: push: paths: - '.github/workflows/**' - '.github/actions/**' pull_request: paths: - '.github/workflows/**' - '.github/actions/**'
jobs: actionlint: runs-on: ubuntu-latest timeout-minutes: 5 permissions: contents: read steps: - uses: actions/checkout@... # v6.0.2 - uses: rhysd/actionlint@... # v1.7.7 with: args: >- -ignore SC2086 -ignore SC2016 -ignore SC2129 -ignore SC2088 -ignore "string should not be empty" -ignore "unknown permission scope"The -ignore args suppress pre-existing findings that are known false positives or shellcheck info/style codes — the gate only fails on genuine structural errors:
SC2086,SC2016,SC2129,SC2088— shellcheck info/style warnings in pre-existingrun:stepsstring should not be empty— actionlint v1.7.7 incorrectly flags the empty-string entry (- '') used as the default choice inworkflow_dispatchtype: choiceinputs; this is valid GitHub Actions syntaxunknown permission scope— themodels: readGitHub Models permission was introduced after actionlint v1.7.7; this ignore can be removed once actionlint is upgraded to a version that includes it
Custom runner labels (e.g. macos-15-intel) are declared in .github/actionlint.yaml — see Actionlint config.
This workflow is registered as a required status check in branch protection, meaning any PR that introduces broken workflow YAML will be blocked from merging into main.
Why a Separate Workflow?
lint-workflows is intentionally a standalone workflow file rather than a job inside ci.yml. This means:
- It runs even when the rest of
ci.ymlis broken (which is precisely the situation it needs to catch) - It has minimal blast radius — a failure here only blocks workflow-touching PRs, not all PRs
- It completes in under 30 seconds with no external dependencies
What actionlint Catches
actionlint performs static analysis on GitHub Actions workflow YAML and detects:
| Category | Examples |
|---|---|
| Expression syntax errors | Malformed ${{ }} expressions, wrong context variables |
Invalid needs: references | Referencing a job that doesn’t exist |
| Action version mismatches | Using a uses: ref that doesn’t resolve |
| Shell script errors | Via shellcheck integration on run: steps |
| Deprecated runner labels | ubuntu-18.04, windows-2019, etc. |
| Secret/input name typos | secrets.CLOUDFLARE_API_TOKEN vs secrets.CF_API_TOKEN |
Missing required with: inputs | Calling an action without its required inputs |
Actionlint config
.github/actionlint.yaml configures actionlint’s static analysis rules:
self-hosted-runner: labels: - macos-15-intelself-hosted-runner.labels — any runner label that isn’t in GitHub’s published list (e.g. a self-hosted or Intel-slice runner) must be declared here to prevent false-positive [runner-label] errors. When a new custom runner is added to the repo’s workflows, add its label to this file at the same time.
Note: The
-ignoreregex patterns for suppressing known false positives (shellcheck codes andmodelspermission) are passed as command-line args inlint-workflows.yml, not in this config file. Theignore:key is not supported in.github/actionlint.yaml.
Versions
Both the pre-push hook and the CI action use actionlint v1.7.7:
.pre-commit-config.yaml:rev: v1.7.7lint-workflows.yml:rhysd/actionlint@03d0035246f3e81f36aed592ffb4bebf33a03106 # v1.7.7
When upgrading actionlint, update both files together so local and CI behavior stay in sync.
Related
.pre-commit-config.yaml— pre-push hook configuration.github/actionlint.yaml— custom runner labels and rule config.github/workflows/lint-workflows.yml— CI gate workflow.github/workflows/README.md— workflow inventory table- Workflow Improvements — broader CI parallelization and hardening history
- actionlint on GitHub — upstream tool documentation
Bloqr AI™ — The privacy you didn't know you needed.
© 2026 Bloqr AI™, a trademark of Bloqr Systems™. Created by Bloqr Systems™, founded by Jayson Knight.
Internet Hygiene (n.) — the ongoing practices that keep your digital life clean, private, and safe.
Our product repos live in the BloqrAI org, part of the Bloqr Systems GitHub Enterprise. Product repos are internal-visibility — enterprise membership is required to view them.