4 ポイント 投稿者 GN⁺ 2025-04-09 | 2件のコメント | WhatsAppで共有
  • GitHub Actionsのshellrun:ブロックの実行方法を決める設定だが、実際には決め打ちのシェル一覧ではなく、$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件のコメント

 
tujuc 2025-04-09

github/runner-image レポジトリを見るだけでも、そのまま使えるパッケージがかなり多くインストールされていますよね…。

イメージを作ると、1GBくらいはあっという間に入ってしまう…。

 
GN⁺ 2025-04-09
Hacker News のコメント
  • Actions ワークフローで bash が実行するすべてのコマンドを出力させるために、以前 -x フラグを使ったことがあり、デバッグにかなり役立った
    https://github.com/jstrieb/just.sh/blob/2da1e2a3bfb51d583be0...
    • ちなみに pipefail を有効にすると、パイプのいずれかが失敗したときにステップは失敗するが、エラー出力がないため、なぜ失敗したのか分からないことがある
      pipefail がより複雑なエラー状態を防いでくれるわけでもない。たとえば設定内の curl ... | sudo tar ... ステップでは、tar の展開失敗、sudo の失敗、シェルエラーのような場合は表面化するが、curl がネットワークエラーで途中で失敗すると、tarjust バイナリを半分だけ展開したままシェルが即座に終了する可能性がある。このときエラーメッセージはなく、壊れた実行ファイルがディスクに残り、失敗のスキップが有効になっていれば実行まで試みられる可能性がある
  • 職場で見たすごい非公開 GitHub Actions のトリックは、repository_dispatch イベント名にワイルドカードを使ってマッチできること
    on: repository_dispatch: - security_scan - security_scan::* のように書ける。リリースパイプラインを中央集約すると、各リポジトリが定義済みの再利用ワークフローを通るよう強制でき、security_scan::$product_name::$version のようなイベントを送れば、中央リリースリポジトリの Actions タブで、どの製品とバージョンのワークフローが走っているのかをずっと把握しやすくなる
    • こうした中央集約アプローチが実際にうまく機能したのか気になる。組織ではすべてをまったく同じ方法でビルドするよう求めているのか?
      似たことをやろうとしている組織に入ったばかりだが、実際にはほとんど役に立たなさそうに見える。テンプレートはよく壊れるし、コードをどうビルドすべきかの文書もないまま、特定のビルド方法を前提に書かれていることが多い
  • GitHub Actions の中でやることは少ないほどよいと思う
    たいていは Make のようなビルドシステムにロジックを入れて GitHub Actions からは呼び出すだけにするか、小さなコマンドラインプログラムを書いて呼び出すほうを好む。こういうものは CI よりローカルでデバッグするほうがずっと簡単だ。面白いトリックではあるが、どこで役に立つのかはよく分からない
    • 私たちのワークフローは実質的に make buildmake test 程度
      買収後、買収した会社のワークフローファイルを見たら数百行もあり、繰り返し部分も多かった。古臭いと言われてもいいが、YAML 村からはできるだけ早く抜け出したい
    • この記事は著者がこれをやれと勧めているのではなく、可能であるという事実を知らせているように見える
      実際に使う予定のない機能でも、システムで何が可能かを知っておくことはセキュリティやデバッグに役立つ
    • 私の解釈では、これは役に立たないし、間違いなく有用ではない。ただ潜在的なセキュリティリスクはある
      特にセルフホストランナーを「探索」するときはなおさらだ
  • 私たちの世代は、絶えず変わるスプレッドシートをコードに移せと頼まれると震え上がった
    次の世代は、GitHub Actions で作られたデプロイに規律を持たせろと頼まれると震えることになる
    • 今、大規模エンタープライズを GH Actions に移行する仕事をしているが、正確には「git に結びついた YAML ベースのパイプライン」に近い。ここでいう規律とはどんな姿であるべきなのか?
      説明してもらえれば、うちの組織でたぶん実現できると思う
    • これがなぜ規律がないことになるのか気になる
  • デフォルトシェルである bash をだまして任意のプログラムを実行させることもできる
  • Action を読む他の人たちが何が起きているのか分かっているなら、かなり有用に見える
    手で入力した数行のコマンドをほぼそのまま移して始めたシェルスクリプトが、100 行を超える怪物になってしまったことが何度もあり、そのたびに本物の配列や型、Python 標準ライブラリのバッテリー同梱機能があればと思った。とはいえ、会社のビルド Action を elisp で実装するつもりはない
  • Github Actions Runner のコードはかなり読みやすい。人気のあるシェル/バイナリのデフォルト引数を定義している場所はここ: https://github.com/actions/runner/blob/main/src/Runner.Worke...
    ScriptHandlerHelpers.GetScriptArgumentsFormat メソッドとして公開されている。ScriptHandler.cs にはプロセス環境と引数の準備コードがあり、実際にプロセスを起動するコードはここ: https://github.com/actions/runner/blob/main/src/Runner.Worke...
    全体として、このコードの単純さには良い意味で驚いた。非常に手続き的で、多数の例外的状況を扱っているが、理解してデバッグしやすそうに見える
  • これでついに本番 CI/CD に C を使って、「低レベルシステム作業」と呼べそうだ
    アセンブリもいけそう
  • これを見て、goeval [1] を使い、GitHub ワークフロー YAML ファイル内で Go コードを CI ジョブとして直接実行しやすくなるかもしれないという期待が出てきた
    ただし 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 を出力」より関連のある例を見せてくれていれば、クリックせずに済んだかもしれない
  • GitHub CI YAML は run: pipeline.sh のような bash スクリプトより何が優れているのだろう?
    • 自動管理される GitHub API トークンがあり、リリース自動化、アーティファクトやコンテナの公開に便利
      複数の OS や CPU アーキテクチャでジョブを同時に走らせることができ、ジョブを引き起こしたイベントやリポジトリの状態に関するコンテキスト情報も得られる。PR ごとのジョブやリリース自動化に便利で、リンターが PR の各行に問題を表示したり、テスト失敗を Web ページにレンダリングしたりする形で GitHub Web UI とも統合できる。変更されていないファイルを再ダウンロードしたり再ビルドしたりしないように、小さなキャッシュも使える。理想的には、ローカルで動く通常のツールにできるだけ多くを入れ、GitHub CI 設定はトリガー、キャッシュ、GitHub 統合に必要な接着コードだけを担当させるのがよい
    • GitHub UI で各ステップの概要を見て、ステップごとの出力を展開したり折りたたんだりして確認できる
      他の人が作った GitHub Actions をパイプラインで簡単に活用でき、ワークフローをモジュール化し、依存関係と並列実行を制御できる。ほかにもあるだろうが、主な利点はこうしたものを自分で実装しなくてよい点にある
    • GitHub 側にとっては、ユーザーが他の Git フォージへ移行する可能性が低くなるという利点がある