GitHub ActionsのJobを分ける理由【GitHub Actions実践記録 #3】
はじめに
こんにちは。
なかのひとカンパニーの archer です。
前回は、GitHub Actionsの、
- push
- pull_request
- workflow_dispatch
- schedule
をどう使い分けるかについて書きました。
今回は、
Jobを分ける理由
についてです。
GitHub Actionsを使い始めたころは、
jobs:
ci:
runs-on: ubuntu-latest
steps:
- run: npm run lint
- run: npm test
- run: npm run build
のように、一つのJobへ全部書いていました。
もちろん、これでも動きます。
ただ、Workflowが大きくなってくると、
lint
test
build
deploy
を本当に全部同じJobで実行するべきなのか、考えるようになりました。
今回は、一つのJobにまとめる場合と、複数のJobに分ける場合の違いを整理します。
Jobは大きな処理単位
前回の記事でも少し触れましたが、GitHub Actionsでは、
Workflow
└─ Job
└─ Step
という構造になっています。
たとえば、
jobs:
ci:
runs-on: ubuntu-latest
steps:
- name: Lint
run: npm run lint
- name: Test
run: npm test
- name: Build
run: npm run build
なら、
ci
という一つのJobの中に、
Lint
Test
Build
という三つのStepがあります。
Stepは同じJobの中で順番に実行されます。
つまり、
Lint
↓
Test
↓
Build
です。
一つのJobにまとめるメリット
小さなRepositoryなら、一つのJobにまとめるのも悪くありません。
設定が単純です。
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm ci
- run: npm run lint
- run: npm test
- run: npm run build
これだけです。
依存関係のInstallも一度で済みます。
Runnerも一つです。
Workflowも読みやすい。
自分も、処理が少ないうちはこの形で十分だと思っています。
一つのJobの問題
ただ、処理が増えると困ることもあります。
たとえば、
Lint
↓
Test
↓
Build
という順番で実行している場合。
Lintで失敗すると、
Lint ×
↓
Test 実行されない
↓
Build 実行されない
となります。
これは、
「Lintが失敗しているから、後ろを実行する意味がない」
という場合には合理的です。
一方で、
Lintも失敗している
Testも失敗している
のに、一回目のCIではLintしか分からないこともあります。
Lintを修正してpush。
次はTestが失敗。
また修正してpush。
となることがあります。
そこでJobを分けるという考え方が出てきます。
Jobを分けてみる
たとえば、
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm ci
- run: npm test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm ci
- run: npm run build
とします。
これで、
lint
test
build
が別々のJobになります。
Jobは依存関係がなければ並列で動く
Job同士に依存関係を設定していなければ、それぞれ独立して実行できます。
イメージとしては、
┌→ lint
開始 ──┼→ test
└→ build
です。
一つのJobなら、
lint
↓
test
↓
build
でした。
Jobを分ければ、それぞれを同時に進められます。
たとえば、
lint 1分
test 3分
build 4分
かかるとします。
一つのJobで順番に実行すると、単純計算で約8分です。
並列なら、環境の準備時間などは別として、最も長いbuildの4分前後が中心になります。
Workflow全体を短くできる可能性があります。
失敗箇所も分かりやすい
Jobを分けると、GitHub上でも、
lint ×
test ○
build ○
のように見えます。
これが個人的にはかなり便利です。
一つのJobだと、
CI ×
なので、Jobを開いて失敗したStepを見る必要があります。
Jobを分けておけば、
何の確認に失敗したのか
が一覧で分かります。
PRを見るときにも、
lintはOK
testはOK
buildだけ失敗
と判断しやすくなります。
ただし、Jobを分ければ必ず速いわけではない
ここは注意しています。
Jobを分けると、それぞれ別のRunnerで動きます。
つまり各Jobで、
Checkout
↓
Node.js Setup
↓
npm ci
を繰り返します。
先ほどの例でも、
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- run: npm ci
を三回書いています。
そのため、処理が非常に軽い場合は、
Jobを分けたことで準備時間の方が増える
こともあります。
Jobを細かく分ければ必ず速くなるわけではありません。
何でもJobに分けない
極端に、
install Job
lint Job
test Job
build Job
typecheck Job
と細かく分けることもできます。
ただ、細かくしすぎるとWorkflowが読みにくくなります。
自分の場合は、
結果を別々に見たいか
を一つの基準にしています。
たとえば、
lint
test
build
は、それぞれ別の意味を持っています。
一方、
Checkout
Node.js Setup
npm install
は、それ自体を独立した結果として確認したいわけではありません。
なのでStepとして扱います。
needs で実行順を作る
Jobを分けても、
「このJobが終わってから次を実行したい」
という場合があります。
そこで使うのが、
needs:
です。
たとえば、
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: npm test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- run: npm run deploy
とすると、
test
↓
成功
↓
deploy
という順番になります。
testが成功しなければdeployへ進みません。
複数のJobを待つ
本番デプロイなら、
lint
test
build
が全部成功してから実行したいことがあります。
その場合は、
jobs:
lint:
runs-on: ubuntu-latest
steps:
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- run: npm test
build:
runs-on: ubuntu-latest
steps:
- run: npm run build
deploy:
needs:
- lint
- test
- build
runs-on: ubuntu-latest
steps:
- run: npm run deploy
のようにできます。
流れとしては、
┌→ lint ─┐
開始 ──┼→ test ─┼→ deploy
└→ build ─┘
です。
これなら、
必要な確認を並列で行い、全部成功したらDeploy
という形にできます。
needs しているJobが失敗したら
たとえば、
lint ○
test ×
build ○
なら、
deploy
は実行されません。
本番Deployでは、この動きが便利です。
確認
↓
全部成功
↓
本番
という境界をWorkflowそのものに作れます。
人間が、
「Test失敗しているけどDeployボタンを押してしまった」
ということを防げます。
Job間でファイルはそのまま共有できない
Jobを分けるときに、もう一つ重要なのが、
別Jobは別Runner
という点です。
たとえばbuild Jobで、
dist/
を作ったとしても、
そのファイルがdeploy Jobにそのまま残っているわけではありません。
build Runner
↓
distを作る
↓
終了
deploy Runner
↓
新しい環境
だからです。
Job間で成果物を受け渡したい場合は、Artifactなどを使います。
Artifactについては、この連載の後半で改めて扱う予定です。
Jobを分けると利用時間も変わる
並列化するとWorkflowの完了は速くなる可能性があります。
ただし、Runnerの利用時間という意味では別です。
たとえば、
lint 2分
test 4分
build 5分
を別Jobで実行した場合、
Workflowの待ち時間は5分前後でも、
Runnerとしてはそれぞれ動いています。
そのため、
速く終わる
=
利用時間も必ず減る
ではありません。
特にPrivate RepositoryでGitHub-hosted runnerを使っている場合は、Actionsの利用量も意識する必要があります。
速度と料金は分けて考えた方がよさそうです。
自分ならどう分けるか
今のところ、自分なら最初は一つのJobから始めます。
CI
├─ lint
├─ test
└─ build
これで特に困らないなら、そのままです。
ただ、
CIに時間がかかる
どこで失敗したか見づらい
処理ごとにRunnerを変えたい
一部だけ再実行したい
と感じ始めたらJobを分けます。
たとえば、
lint
test
build
を独立。
最後に、
deploy
を needs でつなぐ。
このくらいから始めるのが分かりやすいと思っています。
一つにまとめるか、分けるか
ざっくり整理すると、
| 一つのJob | 複数Job |
|---|---|
| YAMLが簡単 | YAMLが増える |
| Setupが一度 | JobごとにSetup |
| Stepは順番に実行 | 独立Jobは並列化できる |
| CI全体として結果を見る | lint・testなど個別に結果が分かる |
| 小さいWorkflow向き | 処理が増えたWorkflow向き |
どちらが正解というわけではありません。
分けること自体を目的にしない
のが大事だと思っています。
まとめ
今回は、GitHub ActionsのJobを分ける理由について整理しました。
Jobを分けると、
- 独立した処理を並列実行できる
- lint・test・buildの結果が分かりやすい
-
needsでJob間の依存関係を作れる - 必要なJobが成功したあとだけDeployできる
というメリットがあります。
一方で、
- Runnerの準備がJobごとに必要
-
npm ciなどが重複する - YAMLが増える
- Runner利用時間が必ず減るわけではない
- Job間でファイルをそのまま共有できない
という点もあります。
GitHub Actionsを使い始めたころは、
Jobは分けた方がきれいなのでは
くらいに考えていました。
でも実際には、
分けることで何を独立させたいのか
を考える方が重要でした。
小さなWorkflowなら一つでいい。
大きくなったら必要な単位だけ分ける。
そして依存関係があるものだけ needs でつなぐ。
今のところ、このくらいが自分には分かりやすい使い方です。
次回は、
Matrixで複数環境のテストをまとめる
というテーマで書きます。
Node.jsの複数バージョンやOSごとに同じテストを実行するとき、同じJobを何個も書かずに済ませる方法をまとめる予定です。
