GitHub Actionsはもっと良くできる
(blog.yossarian.net)- GitHub Actionsは生産性と機能面で有用だが、実際のワークフロー作成では デバッグの遅さ、セキュリティ上のミス、型の欠如、公式アクション不足が繰り返し時間のロスにつながる
- 小さな修正でも
git add; git commit; git pushとブラウザでのログ確認を繰り返す必要があり、単純なリリースワークフローでも 4回のコミットと失敗したリリース が必要だった事例がある ${{... }}展開、pull_request_target、広すぎるGITHUB_TOKENのデフォルト権限、fork の SHA 参照はいずれもミスしやすいセキュリティ上のポイントで、とくに fork SHA は意図したリポジトリのコミットのように見えることがある- アクション入力には
type:検証がなく、workflow_call・workflow_dispatchとサポート範囲も異なるため、配列・オブジェクト入力の代わりに CSV風文字列 やフラット化した入力を自分で処理しなければならない - push 時点の検証、ランタイムのセキュリティチェック、個人リポジトリでのデフォルトトークン権限の縮小、より厳格な型チェック、さらに多くの公式・準公式アクションが、ワークフローの信頼性を高められる
有用だが繰り返し足を引っ張る GitHub Actions
- GitHub Actionsは2019年から、プロのプロジェクトでも趣味のプロジェクトでも毎日使うほど、生産性と安心感に大きく貢献してきたツール
- 機能拡張も継続的に行われてきた
- それでも実際の開発プロセスでは 大きなフラストレーションと時間の損失 の原因にもなる
デバッグは小さなミスにも重すぎる
- リリースワークフローを設定する過程で、小さなミスを直すために 4回のコミット と 4回の失敗したリリース が必要だった事例がある
- 必要な箇所で
${{ ... }}を使っていなかった needs:の関係を入れ忘れた
- 必要な箇所で
- 現在のデバッグのサイクルは、単純なミス1つを確認するだけでも手順が多い
- 開発環境からブラウザに切り替える必要がある
- 正しいタブを探さなければならない
- Actions の概要とステータス画面をクリックして開かなければならない
- バッファされたコンソールログを更新しながら次のエラーを探さなければならない
- 小さな変更でも全体で 30秒以上 かかることがある
-
改善の方向性
- 対話型デバッグシェル を提供するか、少なくとも
git add; git commit; git pushなしで小さな変更を加えてワークフローを再実行できるようにすべき - リポジトリ設定で、明らかに誤ったワークフローを push 時点で拒否できるべき
- 実行不能な構文
- 存在しない job や step への参照
- 不正な YAML のせいでワークフローが静かに実行されない状況
- 対話型デバッグシェル を提供するか、少なくとも
セキュリティ上のミスはあまりに起こりやすい
- GitHub Actionsでは、ユーザーが書いたワークフローが意図せず脆弱になりやすい
${{ ... }}展開をシェルや別の実行コンテキストで使う場合、展開される値がユーザー制御の入力なら、悪意あるコード注入につながり得る- 例では
inputs.frobを通じてコードが注入され、$MY_IMPORTANT_SECRETが漏えいし得る
- 例では
pull_request_targetは、単純ではないワークフローで安全に使うのが非常に難しい- 想定される用途は、fork から来た PR にラベルを付けたりコメントしたりするような限定的なものに見える
- しかし実際には大きな foot-gun のように露出している
- ワークフローおよび job レベルの権限も過剰に設定されやすい
- 通常の
GITHUB_TOKENのデフォルトアクセス権は非常に広い - 個人アカウントのリポジトリでは、デフォルトトークン権限の縮小を忘れがち
- 必要な権限範囲を正確に把握できず、トークン権限を過剰に付与してしまいがち
- 通常の
- GitHub の権限モデルが
read/write/noneという単一軸に押し込められていることも混乱を増やしているid-token: writeでワークフローの OpenID Connect トークンを読めるようになる- 一部の security advisory の
GET操作はwrite権限を要求し、別の操作はreadのみで済む
SHA 固定アクションは fork のコミットと見分けがつきにくい
actions/checkout@c7d749a2d57b4b375d1ebcd17cfbfb60c676f18eのように見える参照が、実際にはactions/checkoutリポジトリのコミットではないことがある- その SHA は
actions/checkoutのネットワーク内にある fork 上のコミットであり、GitHub の alternates の使用によって親リポジトリに属しているように見えることがある - Chainguard の関連記事 では問題を3つに分けている
- fork の SHA 参照と対象リポジトリの SHA 参照が見た目で区別できない
- GitHub REST API の
/repos/{user}/{repo}/commits/{ref}は、{ref}が fork にしかなくても{user}/{repo}のみを参照する JSON 応答を返す - fork と非 fork の SHA を区別できないと、GitHub Actions の信頼済みソース制限を回避できてしまう
- GitHub の対応は今のところ、ドキュメントに 追記 を入れる程度にとどまっている
-
改善の方向性
- 明らかに安全でないワークフローを push 時点で拒否する paranoid workflow security モードが必要
- AddressSanitizer のようなランタイム計測のように、ワークフロー実行中にセキュリティ上危険なパターンを失敗扱いにできるべき
- 例:
pull_request_targetで対象リポジトリではない ref に対してactions/checkoutを使った場合は失敗にする
- 例:
pull_request_targetを非推奨化または削除する案も検討できる- 個人リポジトリでも、組織・エンタープライズと同様にデフォルト
GITHUB_TOKENの範囲をより制限的に設定できるべき - 参照されたリポジトリには存在せず fork にしかない SHA 固定アクションは、デフォルトで拒否すべき
型システムは一貫性と表現力が不足している
- カスタム GitHub Action は
inputs:配下に入力を定義できるが、アクション入力には 型の強制 がない type: numberのような宣言はアクション入力では機能しない- GitHub Actions 内部でも型サポートの場所に一貫性がない
workflow_call:boolean,number,stringをサポートworkflow_dispatch:boolean,choice,number,stringをサポート- action inputs: 型サポートなし
- 型をサポートする入力でも、配列やオブジェクトのような複合データ構造は扱えない
paths: [foo, bar, baz]のような配列入力は不可headers:の下に階層を持つオブジェクト入力は不可
- そのためアクション作者は代わりに間に合わせの形式を要求することになる
paths: foo,bar,bazのように CSV 風のパースを自前で実装するheader-foo,header-bazのように自然な階層構造をフラット化する
- こうしたやり方は保守性にもセキュリティにもよくない
- 単一のフラットな入力名前空間を自分で管理しなければならない
- 複雑な入力のために任意の非仕様言語を作ることになる
-
改善の方向性
- action と workflow の作者が、どこでも
type:を使えるようにすべき choiceもworkflow_dispatchに限定せず、あらゆる場所で使えるべき- 静的に型推論できる場合は、誤った型のワークフロー変更を push 時点で拒否すべき
type: objectとtype: arrayが必要- 内部型が異種混在になる可能性があるため完全ではなくても、現状よりは改善できる
- 必要であれば JSON シリアライズ文字列として渡せる
- GitHub Actions にはすでに
fromJSON(...)関数がある
- action と workflow の作者が、どこでも
公式アクションはプラットフォームへの信頼と直結する
- GitHub Actions のサードパーティエコシステムには高品質なアクションが多い
- その土台には GitHub が保守する公式アクションがある
- 基本的な
git操作:actions/checkout - GitHub 操作とリポジトリ管理:
actions/{upload,download}-artifact,actions/cache,actions/stale - 必須セットアップ:
actions/setup-python,actions/setup-node
- 基本的な
- こうしたアクションは汎用性と重要性が高く、公式に保守された実装である価値が大きい
- しかし公式アクションの数は少なく、GitHub 機能を直接つなぐ処理でさえ公式アクションとして提供されていない場合がある
- 例: pull request を merge queue にプログラムから追加するアクション
- GitHub CLI には
gh pr mergeがあるが、アクションとしては公開されていない
-
メンテナンス終了した公式アクションの例
actions/create-release: 2021年3月からメンテナンス終了- ユーザーにはコミュニティ保守のワークフローへ移行することが推奨されている
- 代表例として
softprops/action-gh-releaseが挙げられている actions/upload-release-asset:actions/create-releaseと同時期にメンテナンス終了扱いactions/setup-ruby: 2021年2月からメンテナンス終了- ユーザーには
ruby/setup-rubyへの移行が推奨されている - 移行自体の痛みは比較的小さかったが、中核コンポーネントが放棄され得るという印象はプラットフォームへの信頼を損なう
- 高品質な公式アクションが不足すると、ユーザーは GitHub API の表面を直接扱うことになり、セキュリティ上のミスを作り続けることになる
-
エコシステム改善の方向性
- GitHub インフラの異なる部分を直接つなぎ、現在 REST API 呼び出しや
gh実行で手動対応している作業は、公式アクションとして提供される価値がある - 大規模なサードパーティアクションと協力して、
community-actionsのような 準公式組織 を作ることも考えられる- GitHub のレビューを受けたアクション
- リポジトリのセキュリティベストプラクティス準拠
- セマンティックバージョン更新
- 主要エコシステムアクションに対する明確な信頼経路
- GitHub インフラの異なる部分を直接つなぎ、現在 REST API 呼び出しや
既存ツールと GitHub のロードマップ
- 多くの人がローカル GitHub Actions エミュレーションツール
nektos/actを勧めている- 単純なワークフローを高速に反復・デバッグするのに有用
- ただしイメージやイベントが GitHub の実環境と完全には一致しない場合があり、完全ではないツールと評価されている
rhysd/actionlintはローカル lint とセキュリティ lint に使われている- workflows だけを lint でき、actions 自体は lint できない点が限界
- GitHub Actions extension for VS Code は、エディタ内 lint とリポジトリワークフロー統合を提供する
- 公開 JSON schema に基づいている
- 実行中の workflow 表示、変数補完などを提供する
- ただし secrets のタイプミスや動的に生成された output 名のタイプミスのような問題は検知できず、add-commit-push デバッグは依然として必要
- GitHub Actions プロダクト管理責任者の Julian Dunn は、いくつかの進行中機能と優先事項を共有している
1件のコメント
Hacker News の意見
GitHub Actions のワークフローには2つのやり方がある。1) GitHub Actions で「プログラミング」し、メール送信のようなことまでマーケットプレイスのツールをつなぎ、YAML が500〜1000行に膨れ上がり、条件文が乱立して理解しにくくなるやり方。
2) GitHub Actions は「設定」だけにして、「この YAML の複雑さをスクリプトへ押し出せないか?」と常に問い続けるやり方。メール送信もスクリプトに入れれば、ワークフローは50〜60行程度で済み、ローカルでスクリプトをデバッグできるので、愚かな push-debug-commit の繰り返しもほぼなくなる。新しいチームに行くたびに、1は狂気の道で、2が合理的だと言っているが、半分くらいは今でも1を選ぶ。デバッグツールの不足、ベンダーロックインも、2を選べばはるかに問題になりにくい
ただし、すべてをきちんとしたプログラミング言語へ押し込むだけでは十分でないことも多い。GHA のベンダー固有機能を使う必要があったり、ジョブ間の依存関係を示したり、すでにアクションとしてうまく抽象化されている REST API を呼び出したりする場合がある。好きな言語で再実装することはできるが、ベンダー依存が消えるわけでもなく、依然として脆い。結局、GHA のベンダーロックイン的な価値提案は非常に強く、人々を2番へ説得するには、より強い利点が必要になる
これは GHA だけでなく、すべてのビルドサーバーに当てはまる。ビルドサーバーは履歴、成果物管理、権限/監査を加えるスクリプト実行器であるべきで、実際のビルド過程はビルド対象のリポジトリに委ねるべきだ
参考までに、GHA の動作をローカルで再現しようとする nektos/act がある: https://github.com/nektos/act
ビルド/テスト/署名/デプロイなどをできるだけ移植性の高いスクリプトとして書き、このスクリプトは常にローカルで動作すべきだ。GitHub は、そのスクリプトを実行する環境を自動で用意し、実際に実行する役割としてだけ見る
git commit、push、wait のループはひどいユーザー体験だ。ユーザーはローカルマシンを含め、どこでも実行できる移植可能なパイプラインを享受する資格がある。Act はこの問題をある程度解決するが、たいてい本物の環境をそのまま代表しているわけではない。
本番のようにローカルで実行できないパイプラインも多いが、重要度の低い開発段階では、こうしたワークフローをキャプチャしてローカルで実行できない理由はない。Garden は移植可能なパイプラインを提供し、依存関係グラフ全体にまたがるキャッシュも追加する。一部の顧客では実行時間が80%以上短縮され、開発者は先に git へ push しなくても、テストの成功/失敗を即座に確認できる。オープンソースだ: https://github.com/nektos/act、https://docs.garden.io
make releaseのような同じターゲット/コマンドを使うべきだGH Actions の実行をデバッグする苦痛には全面的に共感する。使える道具といえば、デバッグをオンにして再実行する機能くらいしかない。パイプラインを直したりデバッグしたりするために作ったゴミコミットが多すぎて、そのほとんどは動くかどうかを見るために手当たり次第に投げているだけのレベルだ。
再利用可能なロジックのようなごく基本的なことですら、不必要に複雑だったり、ドキュメントがひどかったりする。分かってしまえばかなり簡単だったが、GitHub のドキュメントはお粗末だった。再利用性を得るには Action を公開するか、別のリポジトリに置かなければならないように見えたが、実際には再利用ロジックを入れた新しい YAML ファイルを作れる。ただし、動作させるには workflows フォルダのルートに置く必要がある。GH Actions は作業するのが本当に苦痛だが、いったん動けば非常に便利だ。コミットして push する前に Action をテストできるローカルランナーのようなものがあるとよいのに。
GitHub だけの問題でもない。他の大手 CI プラットフォームも、ワークフローや統合の面で特に優れているわけではない。今はできるだけすべてをスクリプト化している。
GitHub Actions はひどい CI/CD システムだ。同じ VM 上でステップを並列実行できず、コンテナベースのジョブは二級市民扱いされている。
1つ目の問題のせいで、ローカル認証情報や環境依存の設定を並列化できない。
google-github-actions/setup-gcloudに1分以上かかるのを見ると腹が立つ。2つ目の問題のせいで、リポジトリ内の Dockerfile で CI 環境の設定を表現し、イメージ内容が変わったら CI がそのイメージを再ビルドして、そのイメージに依存するワークフローを待機させ、内容が変わっていなければ再ビルドせず、すでにインストール済みの依存関係とともに即座に実行する、という構成が非常に難しい。GitHub Actions では毎回キャッシュを再充填しようとすることになる。キャッシュには 5GB の制限があり、金を払っても増やせず、FIFO で押し出されるので、5GB を超えると実質的に無いのと同じだ。Concourse が本当に恋しい。並列ジョブ、ユーザー定義のパイプライントリガー、CI 環境に SSH で入ってデバッグ、パイプラインなしで一回限りのジョブを実行、成果物を作らずにジョブ間でディレクトリ内容を受け渡す、といったことが可能だった。GitHub Actions は.github/workflowsにファイルを1つ置けばすぐ使えるという入りやすさのおかげで人気を得た、おもちゃの CI/CD に近い。参加させるには良いが、初期機能を超えると「最高」と呼ぶには十分に強力ではない。SSH デバッグと一回限りのジョブは本当に夢のようだった。
GitHub が fork と non-fork の SHA 参照を区別できず、fork が GitHub Actions のセキュリティ設定を迂回できるという問題が、どうしてまだ解決されていないのか分からない。
セキュリティ制御を簡単に迂回できるなら、重大なセキュリティ問題だ。誰かに自分の SHA を使うよう説得する必要はあるが、ソーシャルエンジニアリングはたいてい簡単な部類だ。
正直、どれほどひどいか恥ずかしいくらいだ。無関係なビルド失敗の通知が、たまたま最後に merge した最新の maintainer だけに行くのもおかしい。新しく追加された maintainer が Matrix で知らせてくれるまで、自分の更新/ビルドが1週間失敗し続けていることに気づかないこともある。
cache action は目で見ても明らかに壊れているのに、管理者がいないように見える。UI が壊れたりタブがアンロードされたりすると、ステップのビルドログを tail することすらできない。ワークフローをワーカーノード間で移植可能にする抽象化も事実上ない。ベースイメージは惨事に近い。最初はただ肥大化しすぎていて苛立っただけだったが、掘り下げるほど「Linux を知らない Microsoft の人が担当したんだな」と思うようになった。Nix や Docker を使う人向けに、もっと軽量なイメージも提供しようという努力がない。GitHub から離れつつあるが、主な理由は Actions で目が覚めたからだ。YAML でプログラミングしない理由は、YAML が、モダンで実際に有用な技術を知らないエコシステムの上に VC マネーが注ぎ込まれて生まれた不名誉だと思っているからだ。
https://pre-commit.ciを強くおすすめします。関係者ではなく、満足しているユーザーにすぎません。
考え方としては、コミット前にコードを検査し、可能なら修正する hook を書くか既存の hook を使い、push 後に CI が再度検査して、必要なら自動で修正コミットまで行うというものです。自動キャッシュが優れていて信じがたいほど高速で、ローカル開発マシンと CI で同じ設定を使うため、デバッグ上の問題が大きく減ります。オープンソースでは無料です。自動リリースのようなことはせず検査だけですが、その検査は非常にうまくやってくれます。
ソースコード自体を検査するには、
skip_install = Trueで静的検査も走らせられます。グローバルツールとして tox がインストールされ、必要な Python バージョンがすべて入っているコンテナで実行すれば十分です。たとえば https://github.com/georgek/docker-python-multiversion のようなイメージがあります。メンテナンスはされていませんが、更新は簡単です。[tox] envlist = py{310,311}、[testenv]、[testenv:check]にpre-commit run --all-files --show-diff-on-failureを置く、といったボイラープレートで十分です。git commit; git push; repeatループをあまりに何度も経験した末に、https://github.com/mxschmitt/action-tmateを見つけました。ステップ間でシェル接続を開いてくれるので、すべての問題を解決するわけではありませんが、ときには苦痛を確実に減らしてくれます。個人的には、今のように
actions/upload-artifactなどを使わなくても、action run にHTML レポートを添付できるとよいと思います。特にテストビルドでは、出力された HTML レポートをすばやく見たいことがよくあります。現時点で知っている方法は upload-artifact か GH Pages ですが、GH Pages は同じリポジトリに複数のレポート出力がある場合や、最新ではなく過去のレポートをすぐ見たい場合にはいまひとつです。ジョブのサマリーに HTML レポートへのリンクを付け、クリックすると HTML をレンダリングしてくれるだけの単純な
attach-reportアクションがあるとよいです。他の自動化 CI/CD システムは、HTML レポートのキャプチャと閲覧を標準でずっと豊富にサポートしています。$GITHUB_STEP_SUMMARYへMarkdown 出力を直接追加できます。GH Actions は実質的に Microsoft の「Azure Pipelines」のリブランディングに近いものだと見ています。TFS/VSTS/AzDO のビルドおよびリリースパイプラインの以前の形をすべて使ってきた立場からすると、このチームはこれが得意ではありません。
Azure Pipelines がどうにか使えるようになったのも、それ以前に試したあらゆるアプローチが文字どおり失敗したからです。コミットなしで個人環境上の edit-run-debug ループを回せるよう、パイプラインをローカルで実行するプロジェクトもありましたが、当然ながらキャンセルされました: https://github.com/microsoft/azure-pipelines-agent/pull/2687/files#diff-570b52bcb927a5365c22d17a21e8e19e1ba3427c7f3ec16ea64b7b3d14e4ee20。それでも生活の質を上げるツールはあります。たとえば構文を認識する VS Code 拡張機能です: https://marketplace.visualstudio.com/items?itemName=ms-azure-devops.azure-pipelines。少し議論を呼ぶ言い方をすると、YAML の代わりに XML を使っていれば、先頭の xmlns 宣言だけで、まともなコードエディタのほとんどでユーザーが特に介入しなくても検証を受けられたはずです。XML はひどいものですが、私たちが全部捨てたときに一緒に失ってしまった便利な機能もたくさんあります。