GitHub Actions を並列で動かして CI を最適化する

この記事はアドベントカレンダー アイソルート Advent Calendar 2025 の 19 日目の記事です。
はじめに
こんにちは、普段は TypeScript を中心とした Web アプリケーション開発を行っている murakami.s です。
今回は、GitHub Actions を使った CI を高速化する方法について紹介します。
目次
ベースとなるプロジェクト構成
まず、今回紹介する手法を適用するベースとなるプロジェクト構成について説明します。
今回の例では、サンプルとして GitHub Copilot に以下のような構成のサンタクロース向け在庫管理システムをモノレポ構成で構築してもらっています。
santas-workshop/
├─ README.md
├─ package.json
├─ pnpm-lock.yaml
├─ pnpm-workspace.yaml
├─ biome.jsonc
├─ rust-toolchain.toml
├─ .github/ # GitHub Actions ワークフローなど(詳細は後述)
│ ├─ actions/
│ ├─ scripts/
│ └─ workflows/
└─ apps/
├─ node-service/ # Node.js API サービス
├─ web/ # Next.js Web アプリケーション
└─ rust-service/ # Rust API サービス
GitHub Actions のワークフローは以下のようになっています。
使用している言語単位でだけ job を分割し、ステップは逐次実行しています。
name: "CI"
on:
pull_request:
branches:
- master
- develop
push:
branches:
- master
- develop
jobs:
node-ci:
name: "CI for Node.js apps"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
# ========== Node SETUP ==========
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
cache: "pnpm"
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
# ========== License Check ==========
- name: Generate licenses.json
run: pnpm run license:gen
- name: Check licenses
run: pnpm run license:check
# ========== Lint / Typecheck / Test / Build ==========
- name: Lint
run: pnpm run lint
- name: Typecheck
run: pnpm run typecheck
- name: Test
run: pnpm run test
- name: Install Playwright browsers
run: pnpm --filter web exec playwright install --with-deps chromium
- name: E2E Test
run: pnpm run test:e2e
- name: Build
run: pnpm run build
rust-ci:
name: "CI for Rust apps"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
# ========== Setup Rust ==========
- name: Setup Rust
uses: dtolnay/rust-toolchain@6d9817901c499d6b02debbb57edb38d33daa680b # stable
with:
components: "rustfmt"
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "./apps/rust-service -> target"
- name: Install Rust dependencies
working-directory: apps/rust-service
run: cargo fetch
# ========== License Check ==========
- name: Install cargo-deny
run: cargo install cargo-deny
- name: Check Rust licenses
working-directory: apps/rust-service
run: cargo deny check licenses
# ========== Lint / Typecheck / Test / Build ==========
- name: Rust fmt
working-directory: apps/rust-service
run: cargo fmt --all -- --check
- name: Rust lint
working-directory: apps/rust-service
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Rust build
working-directory: apps/rust-service
run: cargo build --all-features --workspace --release
- name: Rust test
working-directory: apps/rust-service
run: cargo test --all-features --workspace
上記のようなワークフローでは、Node.js と Rust の CI がそれぞれ 1 つの job にまとまっており、各ステップが逐次実行されるため、CI の実行時間が長くなってしまいます。
そこで、今から説明する以下の手法を用いて CI の高速化を図ります。
Step1. job を並列化
まず、第一に各言語で実行している job を Lint、Typecheck、Test、Build などの細かい job に分割し、並列で実行できるようにします。
name: "CI"
on:
pull_request:
branches:
- master
- develop
push:
branches:
- master
- develop
jobs:
node-license-check:
name: "[node] License Check"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
cache: "pnpm"
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Generate licenses.json
run: pnpm run license:gen
- name: Check licenses
run: pnpm run license:check
node-lint:
name: "[node] Linting"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
cache: "pnpm"
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm run lint
node-typecheck-and-build:
name: "[node] Typechecking"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
cache: "pnpm"
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Typecheck
run: pnpm run typecheck
- name: Build
run: pnpm run build
node-test:
name: "[node] Testing"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
cache: "pnpm"
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Test
run: pnpm run test
node-test-e2e:
name: "[node] E2E Testing"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24"
cache: "pnpm"
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Playwright browsers
run: pnpm --filter web exec playwright install --with-deps chromium
- name: E2E Test
run: pnpm run test:e2e
rust-license-check:
name: "[rust] License Check"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Rust
uses: dtolnay/rust-toolchain@6d9817901c499d6b02debbb57edb38d33daa680b # stable
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "./apps/rust-service -> target"
- name: Install Rust dependencies
working-directory: apps/rust-service
run: cargo fetch
- name: Install cargo-deny
run: cargo install cargo-deny
- name: Check Rust licenses
working-directory: apps/rust-service
run: cargo deny check licenses
rust-lint:
name: "[rust] Linting"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Rust
uses: dtolnay/rust-toolchain@6d9817901c499d6b02debbb57edb38d33daa680b # stable
with:
components: "rustfmt clippy"
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "./apps/rust-service -> target"
- name: Install Rust dependencies
working-directory: apps/rust-service
run: cargo fetch
- name: Rust fmt
working-directory: apps/rust-service
run: cargo fmt --all -- --check
- name: Rust lint
working-directory: apps/rust-service
run: cargo clippy --all-targets --all-features -- -D warnings
rust-build:
name: "[rust] Build"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Rust
uses: dtolnay/rust-toolchain@6d9817901c499d6b02debbb57edb38d33daa680b # stable
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "./apps/rust-service -> target"
- name: Install Rust dependencies
working-directory: apps/rust-service
run: cargo fetch
- name: Rust build
working-directory: apps/rust-service
run: cargo build --all-features --workspace --release
rust-test:
name: "[rust] Test"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- name: Setup Rust
uses: dtolnay/rust-toolchain@6d9817901c499d6b02debbb57edb38d33daa680b # stable
- name: Cache Rust dependencies
uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "./apps/rust-service -> target"
- name: Rust test
working-directory: apps/rust-service
run: cargo test --all-features --workspace
これで各 job が細かく分割され、並列で実行されるようになりました。
これにより、CI 全体の実行時間が大幅に短縮されます。
Before

After

今回は小規模なコードベースのため、大きな効果は見られませんが、より大規模なコードベースや複数のサービスを含むモノレポ構成の場合、CI の実行時間を劇的に短縮できます。
しかし、見ての通り job 数の増加による、ワークフローの定義が冗長になってしまいました。
セットアップ処理などの重複も多く、メンテナンス性が低下してしまいます。
そこで、次に紹介する手法でワークフローの共通化を図ります。
Step2. Composite Action で共通化
Composite Action とは
複数の step を別ファイルに切り出し、他のワークフローから呼び出して再利用できる機能です。
これにより、ワークフローの共通化やメンテナンス性の向上が図れます。
呼び出し先では uses を使用して Composite Action を指定します。
Composite Action を使用して共通化する
以下の構成で Composite Action を作成し、Node.js と Rust のセットアップ処理を共通化します。
.github/
├─ actions/
│ ├─ setup-node/
│ │ └─ action.yml
│ └─ setup-rust/
│ └─ action.yml
└─ workflows/
└─ ci.yml
.github/actions/setup-node/action.yml (Node.js のセットアップ用)
name: "Setup Node.js Environment"
description: "Setup Node.js with caching and dependency installation"
inputs:
node-version:
description: "Node.js version to install"
required: false
default: "24"
install-deps:
description: "Whether to install dependencies"
required: false
default: "true"
runs:
using: "composite"
steps:
- name: Setup pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
cache: "pnpm"
cache-dependency-path: pnpm-lock.yaml
- name: Install dependencies
if: inputs.install-deps == 'true'
run: pnpm install --frozen-lockfile
shell: bash
.github/actions/setup-rust/action.yml (Rust のセットアップ用)
name: "Setup Rust Environment"
description: "Setup Rust toolchain with caching and components"
inputs:
components:
description: "Additional components to install"
required: false
default: ""
runs:
using: "composite"
steps:
- uses: dtolnay/rust-toolchain@6d9817901c499d6b02debbb57edb38d33daa680b # stable
- uses: Swatinem/rust-cache@779680da715d629ac1d338a641029a2f4372abb5 # v2.8.2
with:
workspaces: "./apps/rust-service -> target"
- name: Install Rust dependencies
working-directory: apps/rust-service
run: cargo fetch
shell: bash
- name: Install Rust components
if: inputs.components != ''
run: rustup component add ${{ inputs.components }}
shell: bash
.github/workflows/ci.yml (CI ワークフロー) ※一部のみ抜粋
jobs:
# 省略...
node-lint:
name: "[node] Linting"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-node
- name: Lint
run: pnpm run lint
rust-lint:
name: "[rust] Linting"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-rust
with:
components: "rustfmt clippy"
- name: Rust fmt
working-directory: apps/rust-service
run: cargo fmt --all -- --check
- name: Rust lint
working-directory: apps/rust-service
run: cargo clippy --all-targets --all-features -- -D warnings
これでセットアップ処理が共通化され、ワークフローの定義が簡潔になりました。
step 1. では約 250 行あったワークフロー定義が、step 2. では約 150 行に削減されました。
Step3. paths-filter で必要な job のみ実行
並列化は完了しましたが、まだ改善の余地があります。
例えば、Node.js のソースのみに変更が加えられた場合でも、Rust の CI が実行されてしまいます。
このような無駄な CI の実行を避けるために、変更されたファイルに応じて必要な job のみを実行するようにしましょう。
dorny/paths-filter を使用して、変更されたファイルに応じて必要な job のみを実行するようにします。
まず、ワークフローの最初に paths-filter を使用して、変更されたファイルに基づいてフィルターを定義します。
jobs:
change-detection:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
node-changed: ${{ steps.changes.outputs.node }} # Node.js 関連の変更
node-deps-changed: ${{ steps.changes.outputs['node-deps'] }} # Node.js 依存関係の変更
rust-changed: ${{ steps.changes.outputs.rust }} # Rust 関連の変更
rust-deps-changed: ${{ steps.changes.outputs['rust-deps'] }} # Rust 依存関係の変更
github-actions-changed: ${{ steps.changes.outputs['github-actions'] }} # GitHub Actions 関連の変更
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Check changed files
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: changes
with:
filters: |
node:
- 'apps/node-service/**'
- 'apps/web/**'
- 'biome*.json*'
- 'tsconfig*.json'
- 'package*.json'
- 'pnpm-lock.yaml'
rust:
- 'apps/rust-service/**'
- 'Cargo.*'
- 'rust-toolchain.toml'
node-deps:
- 'package*.json'
- 'pnpm-lock.yaml'
rust-deps:
- 'Cargo.*'
- 'rust-toolchain.toml'
github-actions:
- '.github/workflows/**'
- '.github/actions/**'
- 'scripts/**'
- '.github/scripts/**'
各 job 側は以下のように変更します。
jobs セクションで needs と if を使用して、変更されたファイルに基づいて必要な job のみを実行するようにします。
jobs:
# 省略...
node-license-check:
needs: change-detection
if: needs.change-detection.outputs.node-deps-changed == 'true' || needs.change-detection.outputs.github-actions-changed == 'true' # Node.js 関連の依存関係または GitHub Actions に変更があった場合に実行
name: "[node] License Check"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-node
- name: Generate licenses.json
run: pnpm run license:gen
- name: Check licenses
run: pnpm run license:check
node-lint:
needs: change-detection
if: needs.change-detection.outputs.node-changed == 'true' || needs.change-detection.outputs.github-actions-changed == 'true' # Node.js 関連のコードまたは GitHub Actions に変更があった場合に実行
name: "[node] Linting"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-node
- name: Lint
run: pnpm run lint
これで、変更されたファイルに基づいて必要な job のみが実行されるようになり、無駄な CI の実行を避けることができます。
特に、今回のようにモノレポ構成の場合、変更内容に応じて特定の言語やサービスの CI のみを実行することで、CI 全体の実行時間を短縮できる場合があります。

変更がドキュメントのみの場合、CI はスキップされます。

Step4. Merge Gate パターンで PR のマージを制御
おまけとして、Merge Gate パターンを使用して、CI が成功した場合にのみ PR をマージできるようにする方法を紹介します。
GitHub のブランチ保護ルールによって、特定のステータスチェックが成功した場合にのみ PR をマージできるように設定しているプロジェクトは多いと思います。
しかし、job を細かく分割している場合、すべての job をステータスチェックに追加すると、ブランチ保護の設定が煩雑になってしまいます。
CI 追加時の設定漏れも発生しやすくなります。
そこで、 Merge Gate パターンを使用して、すべての job の結果を集約する専用の job を作成し、その job のみをステータスチェックに追加します。
Merge Gate とは?
プルリクエストをマージして良いかどうかを判断するためのゲートキーパーのような役割を指します。
GitHub 公式の用語ではありませんが、今回はこの手法を Merge Gate パターンと呼びます。
以下のように、すべての job の結果を集約し、いずれかの job が失敗またはキャンセルされた場合に Merge Gate job を追加します。
jobs:
# 省略...
merge-gate:
permissions:
contents: read
name: "Merge Gate"
runs-on: ubuntu-latest
if: ${{ always() }}
needs:
- change-detection
- node-license-check
- node-lint
- node-typecheck-and-build
- node-test
- node-test-e2e
- rust-license-check
- rust-lint
- rust-build
- rust-test
steps:
- name: Summary
run: |
echo "=== job results ==="
echo "change-detection: ${{ needs.change-detection.result }}"
echo "node-license-check: ${{ needs.node-license-check.result }}"
echo "node-lint: ${{ needs.node-lint.result }}"
echo "node-typecheck-and-build: ${{ needs.node-typecheck-and-build.result }}"
echo "node-test: ${{ needs.node-test.result }}"
echo "node-test-e2e: ${{ needs.node-test-e2e.result }}"
echo "rust-license-check: ${{ needs.rust-license-check.result }}"
echo "rust-lint: ${{ needs.rust-lint.result }}"
echo "rust-build: ${{ needs.rust-build.result }}"
echo "rust-test: ${{ needs.rust-test.result }}"
- name: Gate (fail on failure/cancelled)
shell: bash
run: |
set -euo pipefail
fail=0
check () {
local name="$1"
local result="$2"
case "$result" in
success|skipped)
echo "OK - $name ($result)"
;;
*)
echo "NG - $name ($result)"
fail=1
;;
esac
}
check "change-detection" "${{ needs.change-detection.result }}"
check "node-license-check" "${{ needs.node-license-check.result }}"
check "node-lint" "${{ needs.node-lint.result }}"
check "node-typecheck-and-build" "${{ needs.node-typecheck-and-build.result }}"
check "node-test" "${{ needs.node-test.result }}"
check "node-test-e2e" "${{ needs.node-test-e2e.result }}"
check "rust-license-check" "${{ needs.rust-license-check.result }}"
check "rust-lint" "${{ needs.rust-lint.result }}"
check "rust-build" "${{ needs.rust-build.result }}"
check "rust-test" "${{ needs.rust-test.result }}"
if [ "$fail" -ne 0 ]; then
echo "Merge gate failed."
exit 1
fi
echo "Merge gate passed."
リポジトリ設定の Require status checks to pass には Merge Gate を設定しておきます。
PRを作成すると、特定の job に失敗した場合は以下のように Merge Gate job が失敗し、PR のマージがブロックされます。

すべての job が成功した場合は Merge Gate job も成功し、PR のマージが可能になります。

これで、job の追加や変更があっても Merge Gate job のみがステータスチェックに追加されていればよいため、
PR のマージ制御が簡単になりました。
これで終わりでも良いのですが、 merge-gate 自体が複雑になってしまっているため、
TypeScript で記述してより保守性を高めましょう。
.github/scripts/merge-gate.ts
type NeedResult = "success" | "failure" | "cancelled" | "skipped" | string;
type NeedsObject = Record<
string,
{
result?: NeedResult;
outputs?: Record;
}
>;
function parseNeedsJson(): NeedsObject {
const raw = process.env.NEEDS_JSON;
if (!raw) {
throw new Error(
"NEEDS_JSON is not set. Pass env: NEEDS_JSON: ${{ toJson(needs) }}"
);
}
try {
return JSON.parse(raw) as NeedsObject;
} catch (e) {
throw new Error(`Failed to parse NEEDS_JSON: ${(e as Error).message}`);
}
}
function main() {
const needs = parseNeedsJson();
const targets = Object.keys(needs);
if (targets.length === 0) {
throw new Error(
"No jobs found in NEEDS_JSON. Check merge-gate job's 'needs:' in the workflow."
);
}
let failed = false;
console.log("=== job results ===");
for (const name of targets) {
const result = needs[name]?.result ?? "unknown";
console.log(`${name}: ${result}`);
// success と skipped はOK、それ以外はNG(failure/cancelled/unknown 等)
if (result !== "success" && result !== "skipped") {
console.error(`NG - ${name} (${result})`);
failed = true;
} else {
console.log(`OK - ${name} (${result})`);
}
}
if (failed) {
console.error("Merge gate failed.");
process.exit(1);
}
console.log("Merge gate passed.");
}
main();
merge-gate を以下のように書き換えます。
jobs:
# 省略...
merge-gate:
permissions:
contents: read
name: "Merge Gate"
runs-on: ubuntu-latest
if: ${{ always() }}
needs:
- change-detection
- node-license-check
- node-lint
- node-typecheck-and-build
- node-test
- node-test-e2e
- rust-license-check
- rust-lint
- rust-build
- rust-test
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: "24" # デフォルトだと v20 で TS を実行できないため明示指定(JS で実行する場合はなくても良い)
- name: Gate (fail on failure/cancelled)
env:
NEEDS_JSON: ${{ toJson(needs) }}
run: node .github/scripts/merge-gate.ts
これで needs の管理のみで Merge Gate job をメンテナンスできるようになりました。
もし、 needs の追加漏れを防止したい場合は、期待する job 名のリストを受け取り、
スクリプト内でそれらの job がすべて含まれているかバリーデーションするロジックを追加すると良いでしょう。
完成したワークフロー
最終的には以下のようなワークフローになります。
name: "CI"
on:
pull_request:
branches:
- master
- develop
push:
branches:
- master
- develop
jobs:
change-detection:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
outputs:
node-changed: ${{ steps.changes.outputs.node }}
node-deps-changed: ${{ steps.changes.outputs['node-deps'] }}
rust-changed: ${{ steps.changes.outputs.rust }}
rust-deps-changed: ${{ steps.changes.outputs['rust-deps'] }}
github-actions-changed: ${{ steps.changes.outputs['github-actions'] }}
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Check changed files
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: changes
with:
filters: |
node:
- 'apps/node-service/**'
- 'apps/web/**'
- 'biome*.json*'
- 'tsconfig*.json'
- 'package*.json'
- 'pnpm-lock.yaml'
rust:
- 'apps/rust-service/**'
- 'Cargo.*'
- 'rust-toolchain.toml'
node-deps:
- 'package*.json'
- 'pnpm-lock.yaml'
rust-deps:
- 'Cargo.*'
- 'rust-toolchain.toml'
github-actions:
- '.github/workflows/**'
- '.github/actions/**'
- 'scripts/**'
node-license-check:
needs: change-detection
if: needs.change-detection.outputs.node-deps-changed == 'true' || needs.change-detection.outputs.github-actions-changed == 'true'
name: "[node] License Check"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-node
- name: Generate licenses.json
run: pnpm run license:gen
- name: Check licenses
run: pnpm run license:check
node-lint:
needs: change-detection
if: needs.change-detection.outputs.node-changed == 'true' || needs.change-detection.outputs.github-actions-changed == 'true'
name: "[node] Linting"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-node
- name: Lint
run: pnpm run lint
node-typecheck-and-build:
needs: change-detection
if: needs.change-detection.outputs.node-changed == 'true' || needs.change-detection.outputs.github-actions-changed == 'true'
name: "[node] Typechecking and Building"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-node
- name: Typecheck
run: pnpm run typecheck
- name: Build
run: pnpm run build
node-test:
needs: change-detection
if: needs.change-detection.outputs.node-changed == 'true' || needs.change-detection.outputs.github-actions-changed == 'true'
name: "[node] Testing"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-node
- name: Test
run: pnpm run test
node-test-e2e:
needs: change-detection
if: needs.change-detection.outputs.node-changed == 'true' || needs.change-detection.outputs.rust-changed == 'true' || needs.change-detection.outputs.github-actions-changed == 'true'
name: "[node] E2E Testing"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-node
- name: Install Playwright browsers
run: pnpm --filter web exec playwright install --with-deps chromium
- name: E2E Test
run: pnpm run test:e2e
rust-license-check:
needs: change-detection
if: needs.change-detection.outputs.rust-deps-changed == 'true' || needs.change-detection.outputs.github-actions-changed == 'true'
name: "[rust] License Check"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-rust
- name: Install cargo-deny
run: cargo install cargo-deny
- name: Check Rust licenses
working-directory: apps/rust-service
run: cargo deny check licenses
rust-lint:
needs: change-detection
if: needs.change-detection.outputs.rust-changed == 'true' || needs.change-detection.outputs.github-actions-changed == 'true'
name: "[rust] Linting"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-rust
with:
components: "rustfmt clippy"
- name: Rust fmt
working-directory: apps/rust-service
run: cargo fmt --all -- --check
- name: Rust lint
working-directory: apps/rust-service
run: cargo clippy --all-targets --all-features -- -D warnings
rust-build:
needs: change-detection
if: needs.change-detection.outputs.rust-changed == 'true' || needs.change-detection.outputs.github-actions-changed == 'true'
name: "[rust] Build"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-rust
- name: Rust build
working-directory: apps/rust-service
run: cargo build --all-features --workspace --release
rust-test:
needs: change-detection
if: needs.change-detection.outputs.rust-changed == 'true' || needs.change-detection.outputs.github-actions-changed == 'true'
name: "[rust] Test"
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- uses: ./.github/actions/setup-rust
- name: Rust test
working-directory: apps/rust-service
run: cargo test --all-features --workspace
merge-gate:
permissions:
contents: read
name: "Merge Gate"
runs-on: ubuntu-latest
if: ${{ always() }}
needs:
- change-detection
- node-license-check
- node-lint
- node-typecheck-and-build
- node-test
- node-test-e2e
- rust-license-check
- rust-lint
- rust-build
- rust-test
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: "24" # デフォルトだと v20 で TS を実行できないため明示指定(JS で実行する場合はなくても良い)
- name: Gate (fail on failure/cancelled)
env:
NEEDS_JSON: ${{ toJson(needs) }}
run: node .github/scripts/merge-gate.ts
まとめ
これにて CI の高速化とメンテナンス性の向上が完了しました。
CI の高速化とメンテナンス性向上のために以下の手法を紹介しました。
-
job の並列化
-
Composite Action で共通化
-
paths-filter で必要な job のみ実行
-
Merge Gate パターンで PR のマージを制御
GitHub Actions は開発体験を大きく向上させる強力なツールです。
ぜひ、今回紹介した手法を活用して、効率的で快適な GitHub Actions ライフを!!








