- GitHub Actionsの
shellはrun:ブロックの実行方法を決める設定だが、実際には決め打ちのシェル一覧ではなく、$PATH上の実行ファイルまで対象にできる
- ワークフローでは任意、アクション定義では必須で、デフォルト値はランナーに応じて Linux/macOS の
bash、Windows のpwshのように異なる
shell: bashのように明示すると、GitHub はbash --noprofile --norc -eo pipefailのような追加フラグを付けるが、実行対象そのものは任意のプログラムにできる
- 実行ファイルが単一ファイル入力を受け取らない場合は、
shellの値に**{0}引数**を入れる必要があり、GitHub はこれをrunブロックを含む一時ファイルのパスに置き換える
bashのような見慣れた名前も$PATHで解決されるため、$GITHUB_PATHの変更や偽の実行ファイルによって、その後のステップの実行方法に影響が出る可能性がある
shellキーワードの基本動作
- GitHub Actionsの
shellキーワードは、特定のrun:ブロックをどのシェルで実行するかを指定する
- ワークフローでは任意だが、アクション定義では必須で使われる
- デフォルトシェルはランナー環境によって決まる
- Linux と macOS では通常
bash
- Windows では通常
pwsh
明示的なシェル指定と追加フラグ
- GitHub は
defaults.run.shellのドキュメントで、シェルを明示すると GitHub が選んだフラグも一緒に適用されると案内している
- たとえば
shell: bashを明示すると、実行形式は次のようになる
bash --noprofile --norc -eo pipefail
- この動作だけを見ると、GitHub が制限された有効なシェル値の一覧を管理し、その値にだけ特別なフラグを付けると考えやすい
$PATH上の任意の実行ファイルもシェルにできる
- 実際には、
shellには$PATH上にある任意の実行ファイルを指定できる
- GitHub は指定された実行ファイルで
runブロックを実行する
- そのコマンドが単一ファイル入力を直接受け取らない場合は、
shellの値に{0}を渡す必要がある
- GitHub は
{0}を一時ファイルのパスに置き換える
- この一時ファイルには、テンプレート展開された
runブロックの内容が入る
C をステップランナーとして使う例
- C コンパイラ/インタプリタのように動作するツールも、GitHub Actions のステップ実行に使える
tcc -run {0}をshellに指定する例は正常に動作する
- run: sudo apt install -y tcc
- shell: tcc -run {0}
run: |
#include <stdio.h>
int main() {
printf("Hello, world!\n");
return 0;
}
$GITHUB_PATHでシェル解決を変える例
$GITHUB_PATHで$PATHを動的に変更すると、その後のshell: bashが期待と異なる実行ファイルを指すことがある
- 例では、現在のディレクトリに
bashという実行ファイルを作成し、現在のディレクトリを$GITHUB_PATHに追加している
- その後
shell: bashを使うと、GitHub が$PATH上で見つけた偽の bashを実行できてしまう
- run: |
touch ./bash
chmod +x ./bash
echo '#!/bin/sh' > ./bash
echo 'echo hello from fake bash' >> ./bash
echo "${PWD}" >> "${GITHUB_PATH}"
- run: |
echo "this doesn't do what you expect"
shell: bash
セキュリティ上、予想と異なる点
- この挙動がセキュリティ上どれほど重要かは断定しにくい
- GitHub Actions には、ファイル書き込みが実行につながる経路がすでに多くあり、
GITHUB_ENVもその一例に含まれる
- ただし、GitHub が
bashのようなよく知られたシェル値に対しても$PATH検索を行う点は、予想外かもしれない
- とくによく知られたシェル値に応じてコマンドラインフラグを注入するのであれば、
bashを/bin/bashのような特定パスに固定すると期待することもできる
- より一般的には、GitHub がツールキャッシュに事前登録されたツールだけを許可していると考えるかもしれないが、観測された挙動は
$PATH上の実行ファイルを使う方式だった
2件のコメント
github/runner-image レポジトリを見るだけでも、そのまま使えるパッケージがかなり多くインストールされていますよね…。
イメージを作ると、1GBくらいはあっという間に入ってしまう…。
Hacker News のコメント
-xフラグを使ったことがあり、デバッグにかなり役立ったhttps://github.com/jstrieb/just.sh/blob/2da1e2a3bfb51d583be0...
pipefail がより複雑なエラー状態を防いでくれるわけでもない。たとえば設定内の
curl ... | sudo tar ...ステップでは、tarの展開失敗、sudoの失敗、シェルエラーのような場合は表面化するが、curlがネットワークエラーで途中で失敗すると、tarがjustバイナリを半分だけ展開したままシェルが即座に終了する可能性がある。このときエラーメッセージはなく、壊れた実行ファイルがディスクに残り、失敗のスキップが有効になっていれば実行まで試みられる可能性があるrepository_dispatchイベント名にワイルドカードを使ってマッチできることon: repository_dispatch: - security_scan - security_scan::*のように書ける。リリースパイプラインを中央集約すると、各リポジトリが定義済みの再利用ワークフローを通るよう強制でき、security_scan::$product_name::$versionのようなイベントを送れば、中央リリースリポジトリの Actions タブで、どの製品とバージョンのワークフローが走っているのかをずっと把握しやすくなる似たことをやろうとしている組織に入ったばかりだが、実際にはほとんど役に立たなさそうに見える。テンプレートはよく壊れるし、コードをどうビルドすべきかの文書もないまま、特定のビルド方法を前提に書かれていることが多い
たいていは Make のようなビルドシステムにロジックを入れて GitHub Actions からは呼び出すだけにするか、小さなコマンドラインプログラムを書いて呼び出すほうを好む。こういうものは CI よりローカルでデバッグするほうがずっと簡単だ。面白いトリックではあるが、どこで役に立つのかはよく分からない
make buildとmake test程度買収後、買収した会社のワークフローファイルを見たら数百行もあり、繰り返し部分も多かった。古臭いと言われてもいいが、YAML 村からはできるだけ早く抜け出したい
実際に使う予定のない機能でも、システムで何が可能かを知っておくことはセキュリティやデバッグに役立つ
特にセルフホストランナーを「探索」するときはなおさらだ
次の世代は、GitHub Actions で作られたデプロイに規律を持たせろと頼まれると震えることになる
説明してもらえれば、うちの組織でたぶん実現できると思う
bashをだまして任意のプログラムを実行させることもできる手で入力した数行のコマンドをほぼそのまま移して始めたシェルスクリプトが、100 行を超える怪物になってしまったことが何度もあり、そのたびに本物の配列や型、Python 標準ライブラリのバッテリー同梱機能があればと思った。とはいえ、会社のビルド Action を elisp で実装するつもりはない
ScriptHandlerHelpers.GetScriptArgumentsFormatメソッドとして公開されている。ScriptHandler.csにはプロセス環境と引数の準備コードがあり、実際にプロセスを起動するコードはここ: https://github.com/actions/runner/blob/main/src/Runner.Worke...全体として、このコードの単純さには良い意味で驚いた。非常に手続き的で、多数の例外的状況を扱っているが、理解してデバッグしやすそうに見える
アセンブリもいけそう
ただし goeval はまだファイル入力を直接サポートしておらず、標準入力だけなのでシェルトリックが必要になる。今は
go run github.com/dolmen-go/goeval@v1 - <<'EOF' ... EOFのような方式だが、多少のボイラープレートが必要だ。ちなみに goeval の作者です[1] https://github.com/dolmen-go/goeval
go run github.com/dolmen-go/goeval@v1 - < file.goでできないのか?この文脈でおもちゃプロジェクトを宣伝するなら、「hello を出力」より関連のある例を見せてくれていれば、クリックせずに済んだかもしれない
run: pipeline.shのような bash スクリプトより何が優れているのだろう?複数の OS や CPU アーキテクチャでジョブを同時に走らせることができ、ジョブを引き起こしたイベントやリポジトリの状態に関するコンテキスト情報も得られる。PR ごとのジョブやリリース自動化に便利で、リンターが PR の各行に問題を表示したり、テスト失敗を Web ページにレンダリングしたりする形で GitHub Web UI とも統合できる。変更されていないファイルを再ダウンロードしたり再ビルドしたりしないように、小さなキャッシュも使える。理想的には、ローカルで動く通常のツールにできるだけ多くを入れ、GitHub CI 設定はトリガー、キャッシュ、GitHub 統合に必要な接着コードだけを担当させるのがよい
他の人が作った GitHub Actions をパイプラインで簡単に活用でき、ワークフローをモジュール化し、依存関係と並列実行を制御できる。ほかにもあるだろうが、主な利点はこうしたものを自分で実装しなくてよい点にある