GitHub Actions 実行コストを見える化:Usage API でジョブ別分析ダッシュボード
GitHub Actions を活用していると、気づかないうちに実行時間やコストが増大していることがありますよね。特に複数のワークフローやジョブが並行して動いている場合、どこでどれだけのリソースが使われているのかを把握するのは困難です。
そこで今回は、GitHub Actions Usage API を使って実行コストを可視化し、ジョブ別の分析ダッシュボードを構築する方法をご紹介します。この記事を読めば、どのワークフローやジョブが最もコストを消費しているかを一目で把握でき、最適化のポイントが見えてくるでしょう。
背景
GitHub Actions は CI/CD パイプラインを構築する上で非常に便利なサービスですが、無料枠を超えると従量課金が発生します。特に Organization やチームで利用している場合、実行時間の管理は重要な課題となってきます。
GitHub Actions のコスト構造
GitHub Actions のコストは以下の要素で決まります。
| # | 要素 | 説明 | 課金対象 |
|---|---|---|---|
| 1 | 実行時間 | ジョブの実行にかかった時間 | Linux は 1 分あたり 1 単位 |
| 2 | OS タイプ | 実行環境の OS | Windows は 2 倍、macOS は 10 倍 |
| 3 | 同時実行数 | 並列で実行されるジョブ数 | プランによって上限あり |
| 4 | ストレージ | Artifacts やキャッシュの保存容量 | 月額課金 |
無料枠はプランによって異なりますが、Free プランでは月 2,000 分、Pro プランでは 3,000 分が提供されます。しかし、macOS での実行は 10 倍のカウントとなるため、実質的な実行時間は大幅に減少してしまうのです。
コスト管理の課題
GitHub の Web UI では総実行時間は確認できますが、以下のような詳細な分析には対応していません。
- ワークフロー別、ジョブ別の実行時間内訳
- 時系列での実行傾向の把握
- 特定期間のコスト分析
- OS タイプ別の実行時間比較
これらの情報を取得するには、GitHub Actions Usage API を活用する必要があります。
以下の図は、GitHub Actions のコスト発生から可視化までの全体フローを示しています。
mermaidflowchart TB
workflow["ワークフロー実行"] -->|実行時間記録| github["GitHub Actions<br/>システム"]
github -->|データ蓄積| usage["Usage データ"]
usage -->|API 経由| api["GitHub Usage API"]
api -->|JSON レスポンス| app["分析アプリ"]
app -->|集計・加工| db[("データベース")]
db -->|可視化| dashboard["ダッシュボード"]
dashboard -->|分析結果| user["開発者"]
この図から分かるように、ワークフロー実行データは GitHub のシステムに蓄積され、Usage API を通じて取得できます。取得したデータを分析アプリで処理し、ダッシュボードで可視化することで、初めてコストの全体像が見えてくるのです。
課題
GitHub Actions のコスト管理における主な課題は以下の 3 点です。
課題 1:データの取得が困難
GitHub の Web UI では総実行時間しか確認できず、詳細なジョブ別データを取得するには API を使う必要があります。しかし、Usage API のレスポンスは複雑で、そのまま利用するのは困難でしょう。
課題 2:データの整形と集計
API から取得したデータは JSON 形式で、ワークフロー実行ごとに分かれています。これを時系列やジョブ別に集計するには、データの整形処理が必要となります。
課題 3:継続的な監視
一度データを取得するだけでは不十分で、定期的にデータを収集し、傾向を監視する仕組みが求められます。手動での取得は現実的ではありませんね。
以下の図は、これらの課題がどのように発生するかを示しています。
mermaidflowchart LR
ui["GitHub Web UI"] -.->|総実行時間のみ| limit1["詳細データなし"]
api["Usage API"] -->|生データ| json["複雑な JSON"]
json -.->|加工必要| limit2["集計困難"]
manual["手動取得"] -.->|非効率| limit3["継続監視不可"]
limit1 --> problem["課題:<br/>コスト分析できない"]
limit2 --> problem
limit3 --> problem
これらの課題を解決するには、API からのデータ取得、整形、集計、可視化を自動化する仕組みが必要です。
解決策
GitHub Actions Usage API を活用し、データ取得から可視化までを自動化するダッシュボードシステムを構築します。このシステムは以下の 3 つのコンポーネントで構成されます。
全体アーキテクチャ
mermaidflowchart TB
subgraph collect["1. データ収集層"]
cron["定期実行<br/>GitHub Actions"] -->|API コール| fetcher["データ取得<br/>スクリプト"]
end
subgraph process["2. データ処理層"]
fetcher -->|生データ| parser["JSON パーサー"]
parser -->|整形| aggregator["集計エンジン"]
end
subgraph visualize["3. 可視化層"]
aggregator -->|集計データ| storage[("SQLite")]
storage -->|クエリ| dashboard["Next.js<br/>ダッシュボード"]
end
このアーキテクチャでは、データ収集、処理、可視化を独立したレイヤーとして分離することで、保守性と拡張性を確保しています。
解決策の 3 つのポイント
| # | ポイント | 内容 | メリット |
|---|---|---|---|
| 1 | API 自動取得 | GitHub Actions で定期実行 | 手動作業不要 |
| 2 | データ正規化 | TypeScript で型安全に処理 | バグ削減 |
| 3 | リアルタイム可視化 | Next.js でインタラクティブに表示 | 即座に分析可能 |
それでは、各コンポーネントの実装を見ていきましょう。
具体例
実際にダッシュボードシステムを構築していきます。段階的に実装を進めていきますので、一緒に作り上げていきましょう。
ステップ 1:GitHub Token の準備
まず、Usage API にアクセスするための Personal Access Token を作成します。
必要な権限は以下の通りです。
| # | スコープ | 用途 |
|---|---|---|
| 1 | repo | プライベートリポジトリの Actions データ取得 |
| 2 | workflow | ワークフロー実行履歴へのアクセス |
GitHub の Settings > Developer settings > Personal access tokens から新しいトークンを作成し、環境変数に設定してください。
typescript// .env.local
GITHUB_TOKEN = ghp_your_token_here;
GITHUB_OWNER = your - organization - or - username;
GITHUB_REPO = your - repository - name;
ステップ 2:Usage API からデータを取得
次に、Usage API を呼び出してワークフロー実行データを取得するスクリプトを作成します。
API クライアントの型定義
まず、API レスポンスの型を定義しましょう。
typescript// types/usage.ts
export interface WorkflowRun {
id: number;
name: string;
run_number: number;
created_at: string;
updated_at: string;
run_started_at: string;
status: 'completed' | 'in_progress' | 'queued';
conclusion:
| 'success'
| 'failure'
| 'cancelled'
| 'skipped'
| null;
workflow_id: number;
run_attempt: number;
}
typescript// types/usage.ts (続き)
export interface WorkflowJob {
id: number;
run_id: number;
workflow_name: string;
name: string;
status: 'completed' | 'in_progress' | 'queued';
conclusion:
| 'success'
| 'failure'
| 'cancelled'
| 'skipped'
| null;
started_at: string;
completed_at: string;
runner_name: string;
runner_group_name: string;
labels: string[];
}
typescript// types/usage.ts (続き)
export interface BillableTime {
total_ms: number;
UBUNTU?: number;
MACOS?: number;
WINDOWS?: number;
}
export interface WorkflowRunUsage {
billable: {
UBUNTU?: BillableTime;
MACOS?: BillableTime;
WINDOWS?: BillableTime;
};
run_duration_ms: number;
}
データ取得関数の実装
型定義ができたら、実際にデータを取得する関数を実装します。
typescript// lib/github-api.ts
import { Octokit } from '@octokit/rest';
import type {
WorkflowRun,
WorkflowJob,
WorkflowRunUsage,
} from '@/types/usage';
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN,
});
const owner = process.env.GITHUB_OWNER!;
const repo = process.env.GITHUB_REPO!;
ここでは Octokit を使用して GitHub API にアクセスします。Octokit は GitHub 公式の TypeScript クライアントで、型安全に API を呼び出せますよ。
typescript// lib/github-api.ts (続き)
export async function fetchWorkflowRuns(
perPage: number = 100,
page: number = 1
): Promise<WorkflowRun[]> {
const response =
await octokit.rest.actions.listWorkflowRunsForRepo({
owner,
repo,
per_page: perPage,
page,
});
return response.data.workflow_runs as WorkflowRun[];
}
この関数は、指定されたリポジトリのワークフロー実行履歴を取得します。デフォルトでは 100 件ずつ取得し、ページネーションに対応していますね。
typescript// lib/github-api.ts (続き)
export async function fetchWorkflowRunJobs(
runId: number
): Promise<WorkflowJob[]> {
const response =
await octokit.rest.actions.listJobsForWorkflowRun({
owner,
repo,
run_id: runId,
});
return response.data.jobs as WorkflowJob[];
}
各ワークフロー実行には複数のジョブが含まれます。この関数で個別のジョブ情報を取得できます。
typescript// lib/github-api.ts (続き)
export async function fetchWorkflowRunUsage(
runId: number
): Promise<WorkflowRunUsage> {
const response =
await octokit.rest.actions.getWorkflowRunUsage({
owner,
repo,
run_id: runId,
});
return response.data as WorkflowRunUsage;
}
最も重要なのがこの関数です。ワークフロー実行の Usage データを取得し、OS 別の実行時間(課金対象時間)を含んでいます。
ステップ 3:データの集計と正規化
取得した生データを、ダッシュボードで表示しやすい形式に加工します。
集計用の型定義
typescript// types/analytics.ts
export interface JobAnalytics {
jobName: string;
workflowName: string;
totalRuns: number;
successRate: number;
averageDurationMs: number;
totalBillableMs: number;
osBillable: {
ubuntu: number;
macos: number;
windows: number;
};
}
typescript// types/analytics.ts (続き)
export interface DailyUsage {
date: string;
totalRuns: number;
totalBillableMinutes: number;
ubuntuMinutes: number;
macosMinutes: number;
windowsMinutes: number;
estimatedCost: number;
}
データ集計ロジック
以下は、取得したデータを集計する関数です。
typescript// lib/analytics.ts
import type {
WorkflowRun,
WorkflowJob,
WorkflowRunUsage,
} from '@/types/usage';
import type {
JobAnalytics,
DailyUsage,
} from '@/types/analytics';
export function aggregateJobAnalytics(
runs: WorkflowRun[],
jobs: Map<number, WorkflowJob[]>,
usages: Map<number, WorkflowRunUsage>
): JobAnalytics[] {
const jobMap = new Map<string, JobAnalytics>();
// 各ワークフロー実行をループ
for (const run of runs) {
const runJobs = jobs.get(run.id) || [];
const usage = usages.get(run.id);
processRunJobs(run, runJobs, usage, jobMap);
}
return Array.from(jobMap.values());
}
この関数は、ワークフロー実行、ジョブ、使用状況データを受け取り、ジョブごとの分析データに集計します。Map を使用することで、同じジョブ名のデータを効率的に集約できますね。
typescript// lib/analytics.ts (続き)
function processRunJobs(
run: WorkflowRun,
jobs: WorkflowJob[],
usage: WorkflowRunUsage | undefined,
jobMap: Map<string, JobAnalytics>
): void {
for (const job of jobs) {
const key = `${run.name}::${job.name}`;
const existing = jobMap.get(key);
const duration = calculateDuration(
job.started_at,
job.completed_at
);
const isSuccess = job.conclusion === 'success';
if (existing) {
updateExistingJob(
existing,
duration,
isSuccess,
usage
);
} else {
createNewJob(
jobMap,
key,
run,
job,
duration,
isSuccess,
usage
);
}
}
}
各ジョブについて、既存のデータがあれば更新し、なければ新規作成します。この処理により、同じジョブの複数回の実行を集計できるのです。
typescript// lib/analytics.ts (続き)
function calculateDuration(
startedAt: string,
completedAt: string
): number {
if (!startedAt || !completedAt) return 0;
const start = new Date(startedAt).getTime();
const end = new Date(completedAt).getTime();
return end - start;
}
ジョブの実行時間を計算します。ISO 8601 形式の文字列をパースして、ミリ秒単位で差分を取得していますね。
typescript// lib/analytics.ts (続き)
function updateExistingJob(
job: JobAnalytics,
duration: number,
isSuccess: boolean,
usage: WorkflowRunUsage | undefined
): void {
job.totalRuns++;
job.averageDurationMs =
(job.averageDurationMs * (job.totalRuns - 1) +
duration) /
job.totalRuns;
if (isSuccess) {
job.successRate =
(job.successRate * (job.totalRuns - 1) + 1) /
job.totalRuns;
} else {
job.successRate =
(job.successRate * (job.totalRuns - 1)) /
job.totalRuns;
}
addBillableTime(job, usage);
}
既存のジョブデータを更新します。平均実行時間と成功率を再計算し、課金対象時間を加算しています。
typescript// lib/analytics.ts (続き)
function createNewJob(
jobMap: Map<string, JobAnalytics>,
key: string,
run: WorkflowRun,
job: WorkflowJob,
duration: number,
isSuccess: boolean,
usage: WorkflowRunUsage | undefined
): void {
const analytics: JobAnalytics = {
jobName: job.name,
workflowName: run.name,
totalRuns: 1,
successRate: isSuccess ? 1 : 0,
averageDurationMs: duration,
totalBillableMs: 0,
osBillable: {
ubuntu: 0,
macos: 0,
windows: 0,
},
};
addBillableTime(analytics, usage);
jobMap.set(key, analytics);
}
新しいジョブのデータを作成します。初期値を設定し、課金対象時間を計算して Map に追加しています。
typescript// lib/analytics.ts (続き)
function addBillableTime(
job: JobAnalytics,
usage: WorkflowRunUsage | undefined
): void {
if (!usage) return;
const billable = usage.billable;
if (billable.UBUNTU) {
job.osBillable.ubuntu += billable.UBUNTU.total_ms;
job.totalBillableMs += billable.UBUNTU.total_ms;
}
if (billable.MACOS) {
job.osBillable.macos += billable.MACOS.total_ms;
job.totalBillableMs += billable.MACOS.total_ms;
}
if (billable.WINDOWS) {
job.osBillable.windows += billable.WINDOWS.total_ms;
job.totalBillableMs += billable.WINDOWS.total_ms;
}
}
課金対象時間を OS 別に加算します。GitHub Actions では OS によって課金倍率が異なるため、この情報は非常に重要ですね。
日別使用状況の集計
次は、日別の使用状況を集計する関数です。
typescript// lib/analytics.ts (続き)
export function aggregateDailyUsage(
runs: WorkflowRun[],
usages: Map<number, WorkflowRunUsage>
): DailyUsage[] {
const dailyMap = new Map<string, DailyUsage>();
for (const run of runs) {
const date = run.created_at.split('T')[0];
const usage = usages.get(run.id);
if (!dailyMap.has(date)) {
dailyMap.set(date, createDailyUsage(date));
}
updateDailyUsage(dailyMap.get(date)!, usage);
}
return Array.from(dailyMap.values()).sort((a, b) =>
a.date.localeCompare(b.date)
);
}
この関数は日付をキーにしてデータを集約し、時系列で並べ替えます。これにより、日々のコスト推移を追跡できますよ。
typescript// lib/analytics.ts (続き)
function createDailyUsage(date: string): DailyUsage {
return {
date,
totalRuns: 0,
totalBillableMinutes: 0,
ubuntuMinutes: 0,
macosMinutes: 0,
windowsMinutes: 0,
estimatedCost: 0,
};
}
typescript// lib/analytics.ts (続き)
function updateDailyUsage(
daily: DailyUsage,
usage: WorkflowRunUsage | undefined
): void {
daily.totalRuns++;
if (!usage) return;
const billable = usage.billable;
if (billable.UBUNTU) {
const minutes = billable.UBUNTU.total_ms / 1000 / 60;
daily.ubuntuMinutes += minutes;
daily.totalBillableMinutes += minutes;
daily.estimatedCost += minutes * 0.008; // $0.008 per minute
}
if (billable.MACOS) {
const minutes = billable.MACOS.total_ms / 1000 / 60;
daily.macosMinutes += minutes;
daily.totalBillableMinutes += minutes;
daily.estimatedCost += minutes * 0.08; // $0.08 per minute (10x)
}
if (billable.WINDOWS) {
const minutes = billable.WINDOWS.total_ms / 1000 / 60;
daily.windowsMinutes += minutes;
daily.totalBillableMinutes += minutes;
daily.estimatedCost += minutes * 0.016; // $0.016 per minute (2x)
}
}
課金対象時間を分単位に変換し、OS ごとの料金を計算します。macOS は Ubuntu の 10 倍、Windows は 2 倍の料金であることが分かりますね。
ステップ 4:データ収集の自動化
定期的にデータを収集するために、GitHub Actions ワークフローを作成します。
yaml# .github/workflows/collect-usage.yml
name: Collect Usage Data
on:
schedule:
# 毎日午前 9 時(JST)に実行
- cron: '0 0 * * *'
workflow_dispatch: # 手動実行も可能
jobs:
collect:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
このワークフローは毎日自動実行され、手動でもトリガーできます。cron 式で実行タイミングを制御していますね。
yaml# .github/workflows/collect-usage.yml (続き)
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
Node.js 環境をセットアップし、依存パッケージをインストールします。キャッシュを有効にすることで、実行時間を短縮できますよ。
yaml# .github/workflows/collect-usage.yml (続き)
- name: Collect usage data
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_OWNER: ${{ github.repository_owner }}
GITHUB_REPO: ${{ github.event.repository.name }}
run: yarn collect-usage
- name: Commit and push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add data/
git commit -m "chore: update usage data [skip ci]" || exit 0
git push
収集したデータを Git リポジトリにコミットします。[skip ci] を含めることで、無限ループを防いでいます。
データ収集スクリプト
ワークフローから呼び出されるスクリプトを実装します。
typescript// scripts/collect-usage.ts
import { writeFileSync } from 'fs';
import { join } from 'path';
import {
fetchWorkflowRuns,
fetchWorkflowRunJobs,
fetchWorkflowRunUsage,
} from '@/lib/github-api';
import {
aggregateJobAnalytics,
aggregateDailyUsage,
} from '@/lib/analytics';
async function main() {
console.log('Fetching workflow runs...');
const runs = await fetchWorkflowRuns(100);
console.log(`Found ${runs.length} workflow runs`);
const jobs = new Map();
const usages = new Map();
// 各ワークフロー実行のジョブと使用状況を取得
for (const run of runs) {
const runJobs = await fetchWorkflowRunJobs(run.id);
const usage = await fetchWorkflowRunUsage(run.id);
jobs.set(run.id, runJobs);
usages.set(run.id, usage);
// API レート制限対策
await sleep(100);
}
console.log('Aggregating data...');
const jobAnalytics = aggregateJobAnalytics(
runs,
jobs,
usages
);
const dailyUsage = aggregateDailyUsage(runs, usages);
// データを JSON ファイルに保存
saveData('job-analytics.json', jobAnalytics);
saveData('daily-usage.json', dailyUsage);
console.log('Done!');
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function saveData(filename: string, data: unknown): void {
const path = join(process.cwd(), 'data', filename);
writeFileSync(path, JSON.stringify(data, null, 2));
console.log(`Saved ${path}`);
}
main().catch(console.error);
このスクリプトは、API からデータを取得し、集計して JSON ファイルに保存します。API レート制限を考慮して、リクエスト間に 100ms の待機時間を設けていますね。
ステップ 5:ダッシュボードの構築
最後に、収集したデータを可視化するダッシュボードを Next.js で構築します。
ダッシュボードのレイアウト
typescript// app/dashboard/page.tsx
import { readFileSync } from 'fs';
import { join } from 'path';
import type {
JobAnalytics,
DailyUsage,
} from '@/types/analytics';
import { JobAnalyticsTable } from '@/components/JobAnalyticsTable';
import { DailyUsageChart } from '@/components/DailyUsageChart';
import { CostSummary } from '@/components/CostSummary';
export default function DashboardPage() {
const jobAnalytics = loadData<JobAnalytics[]>(
'job-analytics.json'
);
const dailyUsage = loadData<DailyUsage[]>(
'daily-usage.json'
);
return (
<main className='container mx-auto px-4 py-8'>
<h1 className='text-3xl font-bold mb-8'>
GitHub Actions Usage Dashboard
</h1>
<CostSummary dailyUsage={dailyUsage} />
<DailyUsageChart data={dailyUsage} />
<JobAnalyticsTable data={jobAnalytics} />
</main>
);
}
function loadData<T>(filename: string): T {
const path = join(process.cwd(), 'data', filename);
const content = readFileSync(path, 'utf-8');
return JSON.parse(content);
}
ダッシュボードページは 3 つのコンポーネントで構成されます。コストサマリー、日別使用状況グラフ、ジョブ分析テーブルを表示します。
コストサマリーコンポーネント
typescript// components/CostSummary.tsx
import type { DailyUsage } from '@/types/analytics';
interface Props {
dailyUsage: DailyUsage[];
}
export function CostSummary({ dailyUsage }: Props) {
const totalCost = dailyUsage.reduce(
(sum, day) => sum + day.estimatedCost,
0
);
const totalMinutes = dailyUsage.reduce(
(sum, day) => sum + day.totalBillableMinutes,
0
);
const totalRuns = dailyUsage.reduce(
(sum, day) => sum + day.totalRuns,
0
);
return (
<div className='grid grid-cols-1 md:grid-cols-3 gap-4 mb-8'>
<SummaryCard
title='総実行回数'
value={totalRuns.toLocaleString()}
unit='回'
/>
<SummaryCard
title='総実行時間'
value={Math.round(totalMinutes).toLocaleString()}
unit='分'
/>
<SummaryCard
title='推定コスト'
value={`$${totalCost.toFixed(2)}`}
unit=''
/>
</div>
);
}
typescript// components/CostSummary.tsx (続き)
interface SummaryCardProps {
title: string;
value: string;
unit: string;
}
function SummaryCard({
title,
value,
unit,
}: SummaryCardProps) {
return (
<div className='bg-white rounded-lg shadow p-6'>
<h3 className='text-gray-500 text-sm font-medium mb-2'>
{title}
</h3>
<p className='text-3xl font-bold text-gray-900'>
{value}
<span className='text-lg ml-1'>{unit}</span>
</p>
</div>
);
}
サマリーカードは、重要な指標を大きく表示します。一目で全体のコストが把握できますね。
日別使用状況グラフ
グラフの描画には Recharts ライブラリを使用します。
typescript// components/DailyUsageChart.tsx
'use client';
import {
LineChart,
Line,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
Legend,
ResponsiveContainer,
} from 'recharts';
import type { DailyUsage } from '@/types/analytics';
interface Props {
data: DailyUsage[];
}
export function DailyUsageChart({ data }: Props) {
return (
<div className='bg-white rounded-lg shadow p-6 mb-8'>
<h2 className='text-xl font-bold mb-4'>
日別実行時間推移
</h2>
<ResponsiveContainer width='100%' height={400}>
<LineChart data={data}>
<CartesianGrid strokeDasharray='3 3' />
<XAxis dataKey='date' />
<YAxis
label={{
value: '分',
angle: -90,
position: 'insideLeft',
}}
/>
<Tooltip />
<Legend />
<Line
type='monotone'
dataKey='ubuntuMinutes'
stroke='#8b5cf6'
name='Ubuntu'
/>
<Line
type='monotone'
dataKey='macosMinutes'
stroke='#3b82f6'
name='macOS'
/>
<Line
type='monotone'
dataKey='windowsMinutes'
stroke='#10b981'
name='Windows'
/>
</LineChart>
</ResponsiveContainer>
</div>
);
}
折れ線グラフで OS 別の実行時間推移を可視化します。macOS の使用量が突出している場合は、最適化の余地があることが一目で分かるでしょう。
ジョブ分析テーブル
typescript// components/JobAnalyticsTable.tsx
'use client';
import { useMemo, useState } from 'react';
import type { JobAnalytics } from '@/types/analytics';
interface Props {
data: JobAnalytics[];
}
type SortKey = keyof JobAnalytics | 'totalBillableMinutes';
type SortOrder = 'asc' | 'desc';
export function JobAnalyticsTable({ data }: Props) {
const [sortKey, setSortKey] = useState<SortKey>(
'totalBillableMs'
);
const [sortOrder, setSortOrder] =
useState<SortOrder>('desc');
const sortedData = useMemo(() => {
return [...data].sort((a, b) => {
let aValue: number;
let bValue: number;
if (sortKey === 'totalBillableMinutes') {
aValue = a.totalBillableMs / 1000 / 60;
bValue = b.totalBillableMs / 1000 / 60;
} else {
aValue = a[sortKey] as number;
bValue = b[sortKey] as number;
}
return sortOrder === 'asc'
? aValue - bValue
: bValue - aValue;
});
}, [data, sortKey, sortOrder]);
const handleSort = (key: SortKey) => {
if (sortKey === key) {
setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
} else {
setSortKey(key);
setSortOrder('desc');
}
};
return (
<div className='bg-white rounded-lg shadow overflow-hidden'>
<h2 className='text-xl font-bold p-6 pb-4'>
ジョブ別分析
</h2>
<div className='overflow-x-auto'>
<table className='min-w-full divide-y divide-gray-200'>
<TableHeader
onSort={handleSort}
sortKey={sortKey}
sortOrder={sortOrder}
/>
<TableBody data={sortedData} />
</table>
</div>
</div>
);
}
テーブルはソート機能を持ち、任意の列でデータを並び替えられます。useMemo でソート処理を最適化していますね。
typescript// components/JobAnalyticsTable.tsx (続き)
interface TableHeaderProps {
onSort: (key: SortKey) => void;
sortKey: SortKey;
sortOrder: SortOrder;
}
function TableHeader({
onSort,
sortKey,
sortOrder,
}: TableHeaderProps) {
const headers: { key: SortKey; label: string }[] = [
{ key: 'workflowName', label: 'ワークフロー' },
{ key: 'jobName', label: 'ジョブ' },
{ key: 'totalRuns', label: '実行回数' },
{ key: 'successRate', label: '成功率' },
{ key: 'averageDurationMs', label: '平均時間' },
{ key: 'totalBillableMinutes', label: '課金時間' },
];
return (
<thead className='bg-gray-50'>
<tr>
{headers.map(({ key, label }) => (
<th
key={key}
className='px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider cursor-pointer hover:bg-gray-100'
onClick={() => onSort(key)}
>
{label}
{sortKey === key && (
<span className='ml-1'>
{sortOrder === 'asc' ? '↑' : '↓'}
</span>
)}
</th>
))}
</tr>
</thead>
);
}
テーブルヘッダーはクリック可能で、ソート方向を示す矢印が表示されます。
typescript// components/JobAnalyticsTable.tsx (続き)
interface TableBodyProps {
data: JobAnalytics[];
}
function TableBody({ data }: TableBodyProps) {
return (
<tbody className='bg-white divide-y divide-gray-200'>
{data.map((job, index) => (
<tr key={index} className='hover:bg-gray-50'>
<td className='px-6 py-4 whitespace-nowrap text-sm text-gray-900'>
{job.workflowName}
</td>
<td className='px-6 py-4 whitespace-nowrap text-sm text-gray-900'>
{job.jobName}
</td>
<td className='px-6 py-4 whitespace-nowrap text-sm text-gray-500'>
{job.totalRuns}
</td>
<td className='px-6 py-4 whitespace-nowrap text-sm text-gray-500'>
{(job.successRate * 100).toFixed(1)}%
</td>
<td className='px-6 py-4 whitespace-nowrap text-sm text-gray-500'>
{formatDuration(job.averageDurationMs)}
</td>
<td className='px-6 py-4 whitespace-nowrap text-sm text-gray-500'>
{formatMinutes(job.totalBillableMs / 1000 / 60)}
</td>
</tr>
))}
</tbody>
);
}
function formatDuration(ms: number): string {
const seconds = Math.round(ms / 1000);
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}分${remainingSeconds}秒`;
}
function formatMinutes(minutes: number): string {
return `${Math.round(minutes)}分`;
}
各行にはジョブの詳細情報が表示され、時間は分秒形式でフォーマットされます。ホバー効果で視認性も向上していますよ。
ステップ 6:パッケージのインストール
必要な依存パッケージをインストールしましょう。
bashyarn add @octokit/rest recharts
yarn add -D @types/node
これで、GitHub API クライアントとグラフ描画ライブラリが利用可能になります。
動作確認
以下の手順でダッシュボードを起動できます。
bash# 初回データ収集
yarn collect-usage
# 開発サーバー起動
yarn dev
ブラウザで http://localhost:3000/dashboard を開くと、以下のような情報が表示されます。
図で理解できる要点:
- コストサマリーで総実行回数、総実行時間、推定コストを一覧表示
- 日別グラフで OS ごとの実行時間推移を折れ線グラフで可視化
- ジョブテーブルでワークフロー・ジョブごとの詳細データをソート可能な表で表示
以下の図は、実際のダッシュボード画面でのデータフローを示しています。
mermaidflowchart LR
json[("JSON<br/>ファイル")] -->|読込| page["Next.js<br/>ページ"]
page -->|Props| summary["CostSummary<br/>コンポーネント"]
page -->|Props| chart["DailyUsageChart<br/>コンポーネント"]
page -->|Props| table["JobAnalyticsTable<br/>コンポーネント"]
summary -->|表示| browser["ブラウザ"]
chart -->|描画| browser
table -->|描画| browser
この図から分かるように、JSON ファイルからデータを読み込み、各コンポーネントに Props として渡すことで、疎結合で保守しやすい設計になっています。
まとめ
GitHub Actions の実行コストを可視化するダッシュボードシステムを構築しました。このシステムの主なポイントは以下の通りです。
実現できたこと
| # | 機能 | メリット |
|---|---|---|
| 1 | 自動データ収集 | GitHub Actions で毎日自動実行 |
| 2 | ジョブ別分析 | どのジョブが最もコストを消費しているか把握 |
| 3 | OS 別集計 | macOS など高コストな環境の使用状況を監視 |
| 4 | 時系列可視化 | コストの推移をグラフで追跡 |
| 5 | インタラクティブ表示 | ソート機能で多角的に分析可能 |
最適化のヒント
ダッシュボードでデータを分析すると、以下のような最適化ポイントが見えてきます。
- macOS ジョブを Linux に移行できないか検討する
- 実行時間の長いジョブを分割または並列化する
- 失敗率の高いジョブを改善する
- キャッシュを活用して実行時間を短縮する
- 不要なワークフロー実行を減らす(トリガー条件の見直し)
今後の拡張案
このシステムはさらに拡張できます。
- Slack や Discord への通知機能追加
- アラート設定(コストが閾値を超えたら通知)
- より詳細な分析(ステップ別の実行時間など)
- コスト予測機能(過去のトレンドから将来のコストを予測)
- 複数リポジトリの統合ダッシュボード
GitHub Actions のコストは放置すると予想外に増大することがあります。しかし、Usage API を活用してデータを可視化すれば、どこに改善の余地があるのかが明確になりますね。
ぜひこのダッシュボードを活用して、効率的な CI/CD パイプラインを構築してください。コスト最適化により、より多くのリソースを本質的な開発作業に集中させることができるでしょう。
関連リンク
articleGitHub Actions 実行コストを見える化:Usage API でジョブ別分析ダッシュボード
articleGitHub Actions でゼロダウンタイムリリース:canary/blue-green をパイプライン実装
articleGitHub Actions 条件式チートシート:if/contains/startsWith/always/success/failure
articleGitHub Actions を macOS ランナーで使いこなす:Xcode/コード署名/キーチェーン設定
articleGitHub Actions 部分実行の比較:paths-filter vs if 条件 vs sparse-checkout
articleGitHub Actions が突然失敗するときの切り分け術:ログレベル・re-run・debug secrets
articleJotai 運用ガイド:命名規約・debugLabel・依存グラフ可視化の標準化
articleZod vs Ajv/Joi/Valibot/Superstruct:DX・速度・サイズを本気でベンチ比較
articleYarn でモノレポ設計:パッケージ分割、共有ライブラリ、リリース戦略
articleJest を可観測化する:JUnit/SARIF/OpenTelemetry で CI ダッシュボードを構築
articleGitHub Copilot 利用可視化ダッシュボード:受容率/却下率/生成差分を KPI 化
articleWeb Components vs Lit:素の実装とフレームワーク補助の DX/サイズ/速度を実測比較
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来