Skip to content
Français
CI/CD & Automatisation medium

actionlint: validating your GitHub Actions workflows

35 min de lecture

Read this page in French

You spent an hour writing a GitHub Actions workflow. You push it, and it fails immediately. A syntax error. A misspelled action input. A reference to a secret that does not exist. Those errors could have been caught before the push.

actionlint is a static linter that analyses your workflow files and catches errors before they happen on GitHub.

What you will learn

  • Install actionlint on Linux, macOS and Windows
  • Validate your workflows and read the error messages
  • Wire actionlint into VS Code and a pre-commit hook
  • Add a lint workflow that validates pull requests
  • Configure the rules and the variables specific to your organisation

What is actionlint?

actionlint is a command-line tool that statically analyses your GitHub Actions workflows. It checks the YAML syntax, the action references, the expressions, the permissions and much more, all without running the workflow.

Concretely, actionlint catches:

  • YAML syntax errors (indentation, structure)
  • Unknown or misspelled properties, including invalid with: inputs of popular actions
  • Invalid context references (${{ github.sha }})
  • Actions that do not exist or are badly referenced
  • Permissions declared with an unknown scope or value
  • Type problems in expressions
  • Invalid glob patterns
  • And many other categories, including script injection and hardcoded credentials

Why use actionlint?

The table below contrasts the usual fix loop with the one actionlint brings. The heaviest line is the second: without a linter, every typo in a workflow costs a full round trip with the GitHub runner, several tens of seconds at best, and that cycle repeats until the error is found. actionlint brings that loop down to a local validation in under a second, before the commit even happens.

Without actionlintWith actionlint
The error is found after the pushThe error is caught before the commit
Waiting for the runner (30s to several minutes)Instant validation (< 1s)
History polluted with failed runsCleaner runs
Debugging by trial and errorClear, precise error messages

Installation

With Homebrew:

Fenêtre de terminal
brew install actionlint

Checking the installation:

Fenêtre de terminal
actionlint --version

The first line prints the installed version (for example 1.7.12), followed by two lines about the installation method and the Go compiler used.

Basic usage

Validating every workflow

At the root of your project:

Fenêtre de terminal
actionlint

actionlint finds the files in .github/workflows/ automatically and analyses all of them.

A sample output: every error shows the file, the line, the column, then the category in square brackets ([action], [expression], and so on). The category is what tells you which rule you are facing:

.github/workflows/ci.yml:11:11: input "node-verion" is not defined in action "actions/setup-node@v4". available inputs are "always-auth", "architecture", "cache", ..., "node-version", ... [action]
|
11 | node-verion: 20
| ^~~~~~~~~~~~

Here, actionlint recognises actions/setup-node, knows its valid inputs and spots the typo node-verion instead of node-version. Without that linter, the error would only show up when running on GitHub.

Validating a specific workflow

Passing a path as an argument narrows the analysis to that single file, which helps when you work on one precise workflow in a repository holding several. actionlint always expects to find the root of the Git repository above the file, because it detects the project through the .github/workflows/ directory: run outside a repository, it stops with no project was found.

Fenêtre de terminal
actionlint .github/workflows/ci.yml

Validating from stdin

Useful for scripts and pipelines:

Fenêtre de terminal
cat .github/workflows/ci.yml | actionlint -

Reading the error messages

actionlint gives precise messages with the line number, the column, and an explanation. Here are the main categories:

Syntax errors

.github/workflows/ci.yml:1:1: "on" section is missing in workflow [syntax-check]

The workflow has no on: section defining when it triggers.

Property errors

.github/workflows/ci.yml:11:11: input "node-verion" is not defined in action "actions/setup-node@v4" [action]

A typo: node-verion instead of node-version. The [action] category says actionlint validated the with: key against the real list of the action's inputs.

Runner errors

.github/workflows/ci.yml:5:14: label "123" is unknown. available labels are "windows-latest", ..., "ubuntu-24.04", ... [runner-label]

The runs-on value matches no known runner label. A custom self-hosted runner label is declared in the actionlint.yaml file to avoid that false positive.

Security errors

.github/workflows/ci.yml:7:24: "github.event.issue.title" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable [expression]

actionlint detects script injection: a piece of data a third party controls (github.event.issue.title) interpolated straight into a run: block. The counter is to pass it through an env: block. The [credentials] category covers container passwords written in clear text. actionlint does not, however, report a missing SHA pin: see SHA pinning and the dedicated scanners.

Expression errors

.github/workflows/ci.yml:9:18: property "secret" is not defined in object type {action: string; ...} [expression]

The github.secret context does not exist (secrets.X is what you want).

Useful options

Output format

The default format is meant to be read by a human in the terminal. The other three exist to plug actionlint into another tool: json for a script that parses the results, sarif to push the alerts into the GitHub Security tab through Code Scanning, and the ::error template so GitHub Actions highlights the error directly in the run interface. Choose the format according to the consumer, not to your taste.

Fenêtre de terminal
# Default format (readable)
actionlint
# JSON format (for CI integration)
actionlint -format json
# SARIF format (for GitHub Code Scanning)
actionlint -format sarif > results.sarif
# Format compatible with GitHub Actions problem matchers
actionlint -format '{{range $err := .}}::error file={{$err.Filepath}},line={{$err.Line}},col={{$err.Column}}::{{$err.Message}}{{end}}'

Ignoring certain errors

The -ignore flag takes a regular expression matched against the error message, not a rule identifier. It helps to silence a known, accepted shellcheck warning: the SC2086 and SC2129 codes come from the shellcheck integration that analyses your run: blocks.

Fenêtre de terminal
# Silence one precise shellcheck warning (unquoted words)
actionlint -ignore 'SC2086'
# Silence several
actionlint -ignore 'SC2086' -ignore 'SC2129'

You can also neutralise a category on one precise block, with a comment inside the workflow file:

# actionlint: ignore=expression
- name: Step with ignored warning
run: echo "${{ github.secret }}"

Verbose mode

Fenêtre de terminal
actionlint -verbose

Prints the files analysed and the run time.

actionlint validates the with: inputs of about a hundred popular actions (actions/checkout, actions/setup-node, actions/cache, and so on) thanks to a database embedded in the binary at compile time. It therefore never needs network access for those checks, which makes it usable offline and in an isolated runner. That database only covers the major-version form (actions/checkout@v4); a version frozen at the patch level (@v4.0.1) or a @main reference are not validated.

VS Code integration

actionlint integrates with editors to show errors in real time.

The VS Code extension

  1. Install GitHub's GitHub Actions extension
  2. It uses actionlint automatically when it is installed

Or use the dedicated actionlint extension:

  1. Open VS Code
  2. Extensions, then search for "actionlint"
  3. Install the extension

Errors then appear directly in the editor, underlined.

VS Code configuration

In .vscode/settings.json:

{
"actionlint.executable": "/usr/local/bin/actionlint",
"yaml.schemas": {
"https://json.schemastore.org/github-workflow.json": ".github/workflows/*.yml"
}
}

CI integration

A validation workflow

Create a workflow that validates your other workflows:

name: Lint Workflows
on:
push:
paths:
- '.github/workflows/**'
pull_request:
paths:
- '.github/workflows/**'
# No rights by default: the job asks for the minimum
permissions: {}
jobs:
actionlint:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install actionlint
env:
ACTIONLINT_VERSION: 1.7.12
run: |
BASE="https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}"
curl -sSLO "${BASE}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
curl -sSLO "${BASE}/actionlint_${ACTIONLINT_VERSION}_checksums.txt"
sha256sum --ignore-missing --check "actionlint_${ACTIONLINT_VERSION}_checksums.txt"
tar -xzf "actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" actionlint
sudo install -m 0755 actionlint /usr/local/bin/actionlint
- name: Validate the workflows
run: actionlint -color

With the official action

name: Lint Workflows
on:
pull_request:
paths:
- '.github/workflows/**'
# No rights by default: the job asks for the minimum
permissions: {}
jobs:
actionlint:
runs-on: ubuntu-24.04
permissions:
contents: read
pull-requests: write # So reviewdog can comment on the pull request
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: reviewdog/action-actionlint@50842263c20a7c46bd0065b9e624d3c569db061e # v1.73.0
with:
reporter: github-pr-review

That action uses reviewdog to comment the detected errors directly on the pull requests.

A pre-commit hook

Validate before every commit with pre-commit:

.pre-commit-config.yaml
repos:
- repo: https://github.com/rhysd/actionlint
rev: v1.7.12
hooks:
- id: actionlint

Then:

Fenêtre de terminal
pip install pre-commit
pre-commit install

The validation rules

actionlint checks many rules. The word in square brackets at the end of each error message is the category of the rule triggered, and that category is what the -ignore flag or the # actionlint: ignore= comment targets. The tables below group the most frequent categories by theme.

Syntax and structure

These are the errors actionlint catches first, before it even understands the logic of the workflow: badly indented YAML, a malformed ${{ }} expression, or a workflow command GitHub has removed. The deprecated-commands category is useful during migrations, as it spots the old ::set-output:: calls GitHub no longer supports.

CategoryDescription
syntax-checkBasic YAML errors and missing keys
expressionInvalid ${{ }} expressions or incompatible types
deprecated-commandsObsolete workflow commands (set-output, save-state)

Security

These three categories cover what actionlint can detect on the security side without overlapping the supply chain scanners. Script injection has no category of its own: it is reported under expression, with an explicit message inviting you to pass the data through env:. As a reminder, SHA pinning is not part of it.

CategoryDescription
permissionsUnknown permissions: scope or value
credentialsContainer password written in clear text
expressionUntrusted data interpolated into run: (script injection)

Actions and references

actionlint knows the expected structure of events and of inter-job dependencies, which lets it spot a needs: pointing at a job that does not exist or a misspelled event name. The action category covers the invalid with: inputs of the popular actions seen above.

CategoryDescription
actionInvalid with: inputs, obsolete action (runner too old)
eventsInvalid trigger events
job-needsIncorrect needs: dependencies

Types and values

These categories check that values match what GitHub really expects: a runner label that exists, a valid glob pattern in a paths: filter, a well-formed matrix. The runner-label check is the one that produces the most false positives on self-hosted runners, hence the configuration file described below.

CategoryDescription
runner-labelUnknown runner labels
globInvalid glob patterns in paths:
matrixErrors in the matrix definition

Combining with act

For a complete validation of your workflows:

  1. actionlint: static validation (syntax, types, references)
  2. act: local execution (logic, scripts, behaviour)
#!/usr/bin/env bash
# Complete validation script
set -euo pipefail
echo "Static validation with actionlint..."
actionlint
echo "Dry run with act..."
act -n
echo "Workflows valid."

With set -euo pipefail, the script stops at the first command that fails: no need to test $? after each step, a failing actionlint or act -n interrupts everything and returns a non-zero code to the CI.

Advanced configuration

The configuration file

Create a .github/actionlint.yaml file:

# actionlint configuration
self-hosted-runner:
labels:
- my-runner
- gpu-runner
config-variables:
- MY_ORG_VAR
- DEPLOYMENT_ENV
paths:
ignore:
- '.github/workflows/deprecated-*.yml'

Organisation variables

If you use organisation-level variables (${{ vars.ORG_VAR }}), declare them to avoid false positives:

.github/actionlint.yaml
config-variables:
- ORG_CONFIG
- COMPANY_NAME

Troubleshooting

"command not found: actionlint"

Symptom: the terminal cannot find the command.

Fixes:

Fenêtre de terminal
# Check the installation
which actionlint
# If installed through Go, add it to the PATH
export PATH="$PATH:$(go env GOPATH)/bin"

False positives on custom actions

Symptom: actionlint reports local actions as non-existent.

Fix: local actions (./actions/my-action) are validated if they exist in the repository. Check the path.

"no project was found"

Symptom: actionlint stops with no project was found in any parent directories.

Cause: actionlint looks for the root of a Git repository above the files it analyses. Run outside a repository (or in a folder extracted without .git), it finds no project.

Fix: run it from the repository root, or pass the workflow through stdin:

Fenêtre de terminal
cat ci.yml | actionlint -

Key points

Instant validation

Catch errors in under a second, before the push.

50+ checks

Syntax, security, types, references: everything is verified.

IDE integration

Errors shown directly in VS Code while you write.

CI/CD ready

JSON and SARIF formats, plus reviewdog integration for pull requests.

The main points:

  1. Install actionlint and use it before every push
  2. Wire it into VS Code for real-time validation
  3. Add a lint workflow to validate pull requests automatically
  4. Combine it with act for a complete validation
  5. Configure the rules and variables specific to your organisation

The actionlint repository documents every rule, and the online playground lets you test a workflow in the browser.

Next steps

  • zizmor: the security audit that takes over where the linter stops, on the workflow itself.
  • poutine: the same check extended to a whole organisation and to several CI platforms.
  • OpenSSF Scorecard: the posture score that measures what a linter cannot see, from branch protection to release signing.

Is this site useful to you?

Fewer than 1% of readers support this site.

I maintain more than 700 free guides, with no ads and no tracking. Any support, even a symbolic one, helps cover hosting and keeps these resources free. Thank you for the help.

The form does not show? Open Ko-fi in a new tab.

Subscribe and follow my DevSecOps work on LinkedIn