actionlintでGitHub Actionsワークフローの構文・式・シェルを静的解析してみる
はじめに
データ事業本部のkobayashiです。
GitHub Actionsのワークフローを書いていると、YAMLの構文ミスや式の参照誤り、runs-onに指定したラベルのタイポなど、pushしてActionsが動き出してから気づくということはよくあるかと思います。CIを通すためだけのコミットが積み重なってしまうのも避けたいところです。
そこで今回はGitHub Actionsのワークフローファイル専用の静的解析ツールactionlintの基本的な使い方とサンプルをまとめます。
actionlintとは
actionlintはGitHub Actionsのワークフローファイル(.github/workflows/*.yml)を対象にした、Go製の静的解析ツールです。pushする前に手元でワークフローを検証することができ、CIの試行錯誤を大きく減らすことができます。
主な特徴としては以下になります。
- ワークフロー構文・YAML構文のチェック
${{ ... }}式の型チェック(コンテキストやプロパティのタイポ検出)runs-onに指定したランナーラベルの検証(self-hostedランナーの登録もサポート)actions/checkoutなどのアクション入力の検証- 埋め込みシェルスクリプトを
shellcheck(シェルスクリプト用の静的解析ツール)で、shell: python指定のrunをpyflakes(Python用の静的解析ツール)で連携チェック - スクリプトインジェクション等のセキュリティリスクの検出
.github/actionlint.yamlで設定を集約できる
では早速試してみます。
actionlintを使ってみる
環境
今回使用した環境は以下の通りです。本記事のコマンド出力はすべてこの環境で実際に実行した結果です。
macOS Sequoia 15.7.3 (arm64)
actionlint 1.7.12
shellcheck 0.11.0
actionlintのインストール
macOSの場合はHomebrewで簡単にインストールできます。
$ brew install actionlint
$ actionlint -version
1.7.12
installed by building from source
built with go1.26.1 compiler for darwin/arm64
shellcheckを連携させたい場合は併せてインストールしておきます。
$ brew install shellcheck
基本的な使い方
リポジトリのルートで引数なしで実行すると、.github/workflows/配下のワークフローファイルが自動で検査対象になります。
$ actionlint
特定のファイルだけを検査したい場合はパスを渡します。
$ actionlint .github/workflows/ci.yml
問題が無ければ何も出力されず、終了コードは0になります。問題が見つかった場合は終了コードが1になるためCIでもそのまま使用できます。
サンプル: 典型的なミスを含むワークフロー
それでは実際にミスが含まれるワークフローを用意して、actionlintで検出してみます。
name: Bad Workflow
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest-xl
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.13
- name: Print message
run: |
echo "Hello, ${{ github.event.head_commit.message }}"
- name: Use undefined matrix value
run: echo "${{ matrix.os }}"
ポイントは以下の点です。
runs-onにGitHub-hosted標準ラベルにないubuntu-latest-xlを指定しているgithub.event.head_commit.messageをrunに直接埋め込んでいる(スクリプトインジェクションリスク)matrixを定義していないのにmatrix.osを参照している
actionlintを実行してみます。
$ actionlint -shellcheck= -pyflakes= .github/workflows/bad.yml
.github/workflows/bad.yml:9:14: label "ubuntu-latest-xl" is unknown. available labels are "windows-latest", "windows-latest-8-cores", "windows-2025", "windows-2025-vs2026", "windows-2022", "windows-11-arm", "ubuntu-slim", "ubuntu-latest", "ubuntu-latest-4-cores", "ubuntu-latest-8-cores", "ubuntu-latest-16-cores", "ubuntu-24.04", "ubuntu-24.04-arm", "ubuntu-22.04", "ubuntu-22.04-arm", "macos-latest", "macos-latest-xlarge", "macos-latest-large", "macos-26-intel", "macos-26-xlarge", "macos-26-large", "macos-26", "macos-15-intel", "macos-15-xlarge", "macos-15-large", "macos-15", "macos-14-xlarge", "macos-14-large", "macos-14", "self-hosted", "x64", "arm", "arm64", "linux", "macos", "windows". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file [runner-label]
|
9 | runs-on: ubuntu-latest-xl
| ^~~~~~~~~~~~~~~~
.github/workflows/bad.yml:17:31: "github.event.head_commit.message" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/reference/security/secure-use#good-practices-for-mitigating-script-injection-attacks for more details [expression]
.github/workflows/bad.yml:20:24: property "os" is not defined in object type {} [expression]
|
20 | run: echo "${{ matrix.os }}"
| ^~~~~~~~~
$ echo $?
1
3件のエラーがそれぞれ行・列付きで指摘されました。エラーメッセージの末尾に表示されている[runner-label]や[expression]がチェックの種別(kind)になります。
なお、上の例では-shellcheck=と-pyflakes=に空文字を渡すことで外部ツール連携を無効化しています。シェルやPythonのチェックが不要な場合は無効化することで実行時間を大きく短縮できます。
修正後のワークフロー
検出された3点を修正したものが以下になります。
name: Good Workflow
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: 3.13
- name: Print message safely
env:
MESSAGE: ${{ github.event.head_commit.message }}
run: |
echo "Commit: $MESSAGE"
修正のポイントは以下になります。
runs-onを有効なubuntu-latestに変更- 信頼できないコミットメッセージは
envを経由して環境変数として受け取る matrix.osの参照を削除
このファイルに対して実行するとエラーは出力されず、終了コードも0になります。
$ actionlint .github/workflows/good.yml
$ echo $?
0
サンプル: shellcheck連携
runステップに書かれているシェルスクリプトはactionlintからshellcheckに渡されて検査されます。次のように壊れたスクリプトを書くと、
name: Shell Issue
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run script
env:
INPUT: "hello world"
run: |
echo $INPUT
for f in $(ls *.txt); do
cat $f
done
shellcheckの警告がそのまま伝播してきます。
$ actionlint .github/workflows/shell-issue.yml
.github/workflows/shell-issue.yml:15:9: shellcheck reported issue in this script: SC2035:info:2:15: Use ./*glob* or -- *glob* so names with dashes won't become options [shellcheck]
|
15 | run: |
| ^~~~
.github/workflows/shell-issue.yml:15:9: shellcheck reported issue in this script: SC2045:error:2:10: Iterating over ls output is fragile. Use globs [shellcheck]
|
15 | run: |
| ^~~~
.github/workflows/shell-issue.yml:15:9: shellcheck reported issue in this script: SC2086:info:1:6: Double quote to prevent globbing and word splitting [shellcheck]
|
15 | run: |
| ^~~~
.github/workflows/shell-issue.yml:15:9: shellcheck reported issue in this script: SC2086:info:3:7: Double quote to prevent globbing and word splitting [shellcheck]
|
15 | run: |
| ^~~~
SC2086(クォート漏れ)やSC2045(lsのループ)といった、シェルスクリプト単体ではよく見落とされる問題もワークフロー単位で拾えるのが便利な点です。
設定ファイル: .github/actionlint.yaml
self-hostedランナーの独自ラベルや、リポジトリで使っているvars.*の変数名、特定パスでのエラー無視ルールなどは.github/actionlint.yamlに集約できます。雛形は-init-configで生成できます。
なお、actionlintはgitプロジェクトのルートを基準に設定ファイルを探索します。そのため-init-configや設定ファイルの読み込みを使う場合は、対象ディレクトリがgitリポジトリである必要があります。gitリポジトリでないディレクトリで実行するとproject is not found. check current project is initialized as Git repositoryというエラーになるため、先にgit initしておきます。
$ git init
$ actionlint -init-config
Config file was generated at "/tmp/actionlint-demo/.github/actionlint.yaml"
生成された雛形は以下のようにコメントアウトされたサンプル付きで、必要な項目のみ有効化していく形になります。
self-hosted-runner:
# Labels of self-hosted runner in array of strings.
labels: []
# Configuration variables in array of strings defined in your repository or
# organization. `null` means disabling configuration variables check.
# Empty array means no configuration variable is allowed.
config-variables: null
# Configuration for file paths. The keys are glob patterns to match to file
# paths relative to the repository root. The values are the configurations for
# the file paths. Note that the path separator is always '/'.
# The following configurations are available.
#
# "ignore" is an array of regular expression patterns. Matched error messages
# are ignored. This is similar to the "-ignore" command line option.
paths:
# .github/workflows/**/*.yml:
# ignore: []
例えば、self-hostedランナーのラベルubuntu-latest-xlをactionlintに認識させてrunner-label警告を抑制しつつ、特定のワークフローでのみSC2086を無視する場合は以下のように記述します。
self-hosted-runner:
labels:
- ubuntu-latest-xl
- linux-multi-gpu
config-variables:
- DEFAULT_RUNNER
- ENVIRONMENT_STAGE
paths:
.github/workflows/release.yaml:
ignore:
- 'shellcheck reported issue in this script: SC2086:.+'
主な設定項目は以下になります。
| 設定項目 | 説明 |
|---|---|
self-hosted-runner.labels |
self-hostedランナーの独自ラベルを宣言する。runner-labelの警告を抑制。各要素はpath.Match系のglobパターンとしても書ける |
config-variables |
リポジトリ/組織で定義済みのvars.*変数名のリスト。nullでチェック自体を無効化、空配列は「変数を一切許可しない」を意味する |
paths.<glob>.ignore |
キーはリポジトリルートからのglobパターン(doublestar)。値はエラーメッセージにマッチさせる正規表現(Go RE2)のリスト |
設定ファイルはリポジトリルート(gitプロジェクトルート)直下の.github/actionlint.yamlを自動で読み込みます。読み込み状況を明示するため-verboseをつけて先ほどのbad.ymlを再度実行すると、Using project at /tmp/actionlint-demoの行でプロジェクトルートが認識され、ubuntu-latest-xlはself-hostedランナーのラベルとして扱われてrunner-labelの警告のみ消えていることが確認できます。
$ actionlint -verbose -shellcheck= -pyflakes= .github/workflows/bad.yml
verbose: Linting .github/workflows/bad.yml
verbose: Using project at /tmp/actionlint-demo
verbose: Found 0 parse errors in 0 ms for .github/workflows/bad.yml
verbose: Rule "shellcheck" was disabled since shellcheck command name was empty
verbose: Rule "pyflakes" was disabled since pyflakes command name was empty
verbose: Found total 2 errors in 0 ms for .github/workflows/bad.yml
.github/workflows/bad.yml:17:31: "github.event.head_commit.message" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/reference/security/secure-use#good-practices-for-mitigating-script-injection-attacks for more details [expression]
.github/workflows/bad.yml:20:24: property "os" is not defined in object type {} [expression]
|
20 | run: echo "${{ matrix.os }}"
| ^~~~~~~~~
設定ファイル適用前は3件のエラーが出ていましたが、適用後はrunner-labelのみが抑制されて2件に減っています。
よく使うコマンドラインオプション
普段使いで覚えておくと便利なオプションをまとめておきます。
| オプション | 説明 |
|---|---|
-shellcheck= / -pyflakes= |
空文字を渡すと連携を無効化。実行時間を大きく短縮できる |
-ignore <regex> |
エラーメッセージを正規表現でフィルタ。複数指定可 |
-format <go-template> |
Goテンプレートで出力フォーマットを変更(JSON/JSONLなど) |
-color / -no-color |
カラー出力の制御 |
-init-config |
.github/actionlint.yamlの雛形を生成 |
-verbose |
検査対象・プロジェクトルート・有効/無効ルールなどの内部処理を表示 |
-version |
バージョン表示 |
例えばCIや他ツールから扱いたい場合はJSONで出力すると後段で扱いやすくなります。下記は-formatの結果を見やすくするためpython -m json.toolで整形し、可読性のためmessage内の利用可能ラベル列挙のみ...で省略しています。
$ actionlint -shellcheck= -pyflakes= -format '{{json .}}' .github/workflows/bad.yml | python -m json.tool
[
{
"message": "label \"ubuntu-latest-xl\" is unknown. available labels are \"windows-latest\", ... \"self-hosted\", \"x64\", \"arm\", \"arm64\", \"linux\", \"macos\", \"windows\". if it is a custom label for self-hosted runner, set list of labels in actionlint.yaml config file",
"filepath": ".github/workflows/bad.yml",
"line": 9,
"column": 14,
"kind": "runner-label",
"snippet": " runs-on: ubuntu-latest-xl\n ^~~~~~~~~~~~~~~~",
"end_column": 29
},
{
"message": "\"github.event.head_commit.message\" is potentially untrusted. avoid using it directly in inline scripts. instead, pass it through an environment variable. see https://docs.github.com/en/actions/reference/security/secure-use#good-practices-for-mitigating-script-injection-attacks for more details",
"filepath": ".github/workflows/bad.yml",
"line": 17,
"column": 31,
"kind": "expression",
"snippet": " run: |",
"end_column": 31
},
{
"message": "property \"os\" is not defined in object type {}",
"filepath": ".github/workflows/bad.yml",
"line": 20,
"column": 24,
"kind": "expression",
"snippet": " run: echo \"${{ matrix.os }}\"\n ^~~~~~~~~",
"end_column": 32
}
]
kindがチェック種別、line/columnが問題箇所、snippetが該当行のスニペットになっており、他ツールから絞り込みや集計をする際に扱いやすい形式です。
GitHub Actionsでの実行例
ローカルだけでなく、GitHub Actions上でも継続的に検査するのが理想です。actionlint本家リポジトリには専用のGitHub Actionは同梱されていませんが、配布スクリプト・Dockerイメージ・reviewdog/action-actionlint・super-linter経由など複数の方法が公式ドキュメントで紹介されています。シンプルな例として配布スクリプトを使う方法を紹介します。
name: Lint GitHub Actions
on:
push:
paths:
- '.github/workflows/**'
pull_request:
paths:
- '.github/workflows/**'
jobs:
actionlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Download actionlint
id: get_actionlint
run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash) 1.7.12
- name: Run actionlint
run: ${{ steps.get_actionlint.outputs.executable }} -color
これでワークフローを変更するPRが上がるたびにactionlintが走り、問題があればCIで落ちるようになります。download-actionlint.bashの引数にバージョンを渡しているのは再現性のためで、引数なしにすると常に最新版が取得されます。
まとめ
GitHub Actions向け静的解析ツールactionlintの基本的な使い方とサンプルを試してみました。導入が手軽でワークフローの修正も不要なため、CIに組み込んで活用していきたいと思います。
最後まで読んで頂いてありがとうございました。




