差分があるサービスだけbuildする — 汎用パターンで理解する
この課題の価値は、難しいシェルを書くことではありません。変更ファイルを影響サービスへ変換し、その結果だけを後続ジョブへ渡す設計を、どのモノレポでも再利用できる形で理解することです。
TL;DR
detectは変更影響だけを判定し、サービス名のJSON配列を返す。buildはMakefileやクラウド固有includeを見て、自分が実行可能か最終確認する。- AIには「依存マップ」と「5つの確認ケース」を渡して実装させる。
01課題の正体 — diffを実行対象へ変換する
Dockerfile探索もpaths-filterも、入口と出口は同じです。
教材は、Dockerfileのあるディレクトリを候補として集め、git diff の変更ファイルと照合し、build-targets を後続matrixへ渡します。覚えるべきなのはシェルの細部より、このデータ変換です。
AIに任せてよい: glob、JSON化、YAML実装。
人が決める: どの変更がどのサービスへ影響するか。
022つの実装法 — 手製shellとpaths-filter
学習には手製方式、現場の汎用テンプレートには宣言的フィルタが扱いやすい。
| 観点 | find + git diff | dorny/paths-filter |
|---|---|---|
| 対象追加 | Dockerfile追加で候補を自動発見 | フィルタ名とパスを追加 |
| 比較処理 | fetch範囲・merge-baseを自前管理 | eventに応じた変更検出をActionへ委譲 |
| JSON出力 | jq 等で組み立てる | changes 出力をmatrixへ渡せる |
| 共有依存 | 別ロジックが必要 | 各フィルタへ明示しやすい |
| 向く場面 | 教材・自動発見重視 | 現場テンプレート・モノレポ |
教材例の境界: HEAD^ HEAD は1コミット比較です。汎用化ではpush内の複数コミットやmerge-baseをどう扱うかを、トリガーとActionの仕様に合わせて決めます。
03なぜMakefile確認をbuild側に残すのか
detectは影響判定、buildは実行能力の確認。段階的に絞ると責務が崩れません。
質問660の回答では、find-build-targets でMakefileと aws.mk のincludeまで確認すると複雑になり、読みづらくなる点が理由として挙げられています。build側ならAWSは aws.mk、GCPは gcp.mk という固有条件を自然に持てます。
| 責務 | 置くもの | 置かないもの |
|---|---|---|
| detect | 変更パス→対象名、JSON出力 | ECR、認証、make実行 |
| AWS build | Makefile/aws.mk確認、ECR push | 変更ファイル解析 |
| GCP build | Makefile/gcp.mk確認、Artifact Registry | 変更ファイル解析 |
少しrunner時間を使っても、検出ロジックの単純さとマルチクラウド拡張性を買う判断です。これは「最安」ではなく「変更しやすさとの総コスト最小化」です。
04汎用化の核心 — 依存マップを宣言する
サービス配下だけでなく、共有コードとbuild定義の波及先を決めます。
- uses: dorny/paths-filter@v4
id: changes
with:
filters: |
slack-metrics:
- 'services/slack-metrics/**'
- 'aws.mk'
- 'go.mod'
- 'go.sum'
db-migrator:
- 'ops/db-migrator/**'
- 'aws.mk'
- 'go.mod'
- 'go.sum'
同じ共有パスを複数フィルタへ含めれば、その変更時に changes 出力が両サービス名を返します。サービス名はbuildだけでなく、migrate・deploy・task definition登録の共通キーにもできます。
方針が必要: workflow変更を全buildのトリガーにするか、workflow構文検証だけにするか。暗黙にせずフィルタへ表します。
05壊れにくいjob契約 — JSON、空配列、status
動くhappy pathだけでなく、何も変わらない時を正しく表現します。
jobs:
detect:
outputs:
targets: ${{ steps.changes.outputs.changes }}
build:
needs: detect
if: needs.detect.outputs.targets != '[]'
strategy:
matrix:
service: ${{ fromJSON(needs.detect.outputs.targets) }}
[] を返し、build job自体を起動しない。[A, B] を返し、2つを並列build。[A] を返し、Aだけbuild。[B] を返し、Bだけbuild。[A, B] を返し、影響先すべてをbuild。required checkの注意: 下流jobのskipが集約上見落とされる構成があります。Branch Protectionで必須にするなら、if: always() で各job resultを検査する固定名のstatus jobも検討します。
06AIに頼むときの最小プロンプト
コード生成より先に、依存関係と受け入れ条件を渡します。
| AIへ渡す情報 | 渡さないと起きること |
|---|---|
| サービス一覧と各ルートパス | 候補の推測漏れ |
| 共有ファイルの波及先 | 必要なrebuildを取りこぼす |
| push / pull_requestのどちらか | 比較範囲を誤る |
| AWS/GCPの実行能力条件 | 存在しないmake targetを呼ぶ |
| 5つの期待結果 | happy pathだけで「完成」になる |
結論: AIでリファクタするのは合理的です。ただし、AIに推測させてはいけないのは依存マップと成功条件です。
結論
- 教材の手製方式で原理を理解し、汎用テンプレートでは
paths-filterへ置き換えられる。 - detectは影響判定、buildは実行能力、deploy/migrateは反映という責務を守る。
- AIには依存マップと5ケースを渡す。シェルの巧さではなく契約の明確さが再利用性を作る。