はじめに
(Ba)sh スクリプトを静的解析してくれる便利なツール shellcheck が、GitHub Actions 標準の実行環境 から使えるので Pull-Request に含まれた (Ba)sh を自動的にチェックをする ワークフローを書きました。
実行結果
ワークフローのサンプル
2022-10-25追記: checkout
時に fetch-depth: 0
オプションを使わない方法を思いついたので以下サンプルを更新しました。2 回 checkout
している 理由は GitHub Actions を用いて Pull-Request で変更されたファイル一覧を取り出す方法 に詳細があります。
name: Static analysis
on:
pull_request:
types: [opened, synchronize]
paths: '**.sh'
jobs:
shellcheck:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.base.sha }}
- uses: actions/checkout@v3
- name: Check scripts with shellcheck
env:
EXCLUDE_OPTS: SC2086,SC2001
run: |
git diff ${{ github.event.pull_request.base.sha }}.. \
--diff-filter=AM --name-only -- '*.sh' | while read script ; do \
shellcheck ${EXCLUDE_OPTS:+-e ${EXCLUDE_OPTS}} $script || touch error ; \
done
test ! -e error
ポイントの紹介
on.<push|pull_request>.paths
でマッチした時のみトリガーする
on:
pull_request:
types: [opened, synchronize]
paths: '**.sh'
今回は (Ba)sh スクリプトのチェックが目的なので、関係のないファイルが更新されたときはワークフローが走らないようにします。push または pull_request では paths
に 定義されているパターンにマッチするファイルが一つでも含まれている時だけワークフローを実行する といった制限を設けることができます。
注意点としては '*'
は /
を含めないため paths:'*.sh'
としてしまうと、サブディレクトリのスクリプトを更新してもトリガーされません。このあたりの具体例については、最近公開された以下のチートシートがとても参考になります。
また、今回のサンプルでは reopend
の時は実行しないように、types: [opened, synchronize]
のようにアクティビティタイプに変更しています。こちらは前回の記事の types でワークフローのトリガーを細かく制御する が参考になるかもしれません。
jobs.<job_id>.steps.uses
で外部のアクションを参照する
uses: actions/checkout@v3
uses: {owner}/{repo}@{ref}
という構文で、パブリックリポジトリとして公開されているアクションを参照することができます。
今回は、以下で紹介されている actions/checkout という便利なアクションを用いました。
このアクションは名前のとおり、ワークスペースにリポジトリを取得した後にチェックアウトしてくれます。ここでポイントなのが、デフォルトでは $GITHUB_SHA
にチェックアウトされる点です。Pull-Request における $GITHUB_SHA
は他のイベントとは異なり、Pull-Request のブランチからベースブランチに Merge した先頭コミットを指します。
GITHUB_SHA
Last merge commit on the GITHUB_REF branch
つまり、ワークスペースで お試し Merge をして、その先頭コミットにチェックアウトしてくれます。こうすることで「Pull-Request ではうまくいっていたが、いざ Merge してみたら失敗してしまった」ということが避けられるようになっています。 Merge については Actions の実行時ログからも確認することができます。
git checkout --progress --force refs/remotes/pull/19/merge
(...)
HEAD is now at 5aaad4d Merge eeb2415444ae2c220766235724e679e624ce12d2
into 31bdf0f35c15a591b0b3cf5afa6a754e18bb02d7
更新があった *.sh
ファイルだけを取り出して shellcheck
をかける
run: |
git diff ${{ github.event.pull_request.base.sha }}.. \
--diff-filter=AM --name-only -- '*.sh' | while read script ; do ...
前述の paths: '**.sh'
は Pull-Request のコミットに 1 つでも .sh
が含まれていたらトリガーする という制御なので、.sh
以外のファイルが含まれている場合も考えられます。したがって、更新のあった拡張子が .sh
のファイルだけを改めて取り出そうと思います。まず git diff ${{ github.event.pull_request.base.sha }}..
では、ベースブランチの SHA
と $GITHUB_SHA
の差分を得ます。 SHA
については以下を参考にしました。
次に、削除やリネームの時はチェックの対象外としたいので --diff-filter=AM
で A
新規追加と M
変更のみをフィルタしています。最後に --name-only -- '*.sh'
で拡張子が .sh
にマッチするファイル名を取得し、これらを shellcheck
の対象としました。
shellcheck の除外オプション
env:
EXCLUDE_OPTS: SC2086,SC2001
shellcheck を使おう でも紹介されていますが、実行時に -e CODE1[,CODE2...]
とすることで、指定したルールをチェックの対象外とすることができます。今回のサンプルでは、SC2086 と SC2001 のルールをチェックの対象外としています。
失敗した場合は
Pull-Request 上ではこのように表示されます。 失敗してしまった場合は Details から Actions の実行時ログを参照して、何故エラーしたかを確認します。
上記の例では SC2070 というルールに沿っていない点を指摘されているため、コードを修正するか、または 対象外のルール登録をする必要があります。他のルールについては shellcheck/wiki から確認することができます。
おわり
今回 GitHub Actions 標準の実行環境 に shellcheck が含まれていることに気付いて、このようなチェックの仕組みを作ってみました。 uses:
から気軽に使えるように、そのうち Marketplace かパブリックなアクションとして公開してみたいと思います。