「先週何が終わったか、朝会で確認してから気づく」「スプリント後半になるまでリスクが見えない」——GitHub Projects を使っているチームでよく聞く悩みです。
この記事では、GitHub Projects の進捗を毎日自動取得し、前営業日との差分を Claude Code Skills で分析して Slack に自動報告する仕組みの作り方を解説します。
ポイントは「スナップショットの蓄積」です。GitHub Projects API は現在の状態しか返さないため、履歴を持てません。日々の JSON スナップショットを S3 に保存して比較することで、「昨日から何が進んだか」「due date がいつ変わったか」「スプリントのリスクはどこか」を定量的に把握できるようになります。Claude Code Skills の使い方はSkills入門・Skills設計ガイドも合わせて参照してください。
- この仕組みで解決できること
- アーキテクチャ全体像
- 前提設定① GitHub App の作成
- 前提設定② AWS 設定(Bedrock + S3 + OIDC)
- Step 1: GitHub Projects データ取得スクリプト(fetch_progress.sh)
- Step 2: スナップショット差分比較スクリプト(diff.py)
- Step 3: Claude Code Skill の設計(check-pbi-progress)
- Step 4: GitHub Actions ワークフロー(daily-progress.yml)
- Step 5: Slack 通知メッセージの実装
- 応用・カスタマイズパターン
- セットアップチェックリスト
- よくある質問
- まとめ
この仕組みで解決できること
毎日の朝会・スプリントレビュー前に自動で Slack 通知が届く状態を作ります。
| 課題 | 解決方法 | 効果 |
|---|---|---|
| 昨日から何が進んだか分からない | DeepDiff で前日スナップショットと比較 | 完了・着手・差し戻しを自動検出 |
| due date 変更に気づかない | フィールド値の変更を全件差分抽出 | 期限変更を見逃さず早期対応 |
| スプリント達成リスクが見えにくい | Claude が残タスク数・日数から分析 | リスクアラートを定量的に通知 |
| 朝会前に進捗確認に時間がかかる | GitHub Actions で平日自動実行 | 手動確認ゼロでレポートが届く |
アーキテクチャ全体像
全体の流れを先に整理します。
| ステップ | ツール | 役割 |
|---|---|---|
| ① 進捗取得 | gh CLI + GraphQL | GitHub Projects から当日の PBI 情報を JSON で取得 |
| ② スナップショット保存 | AWS S3 | 当日の JSON を日付付きで保存。前日分との比較に使用 |
| ③ 差分比較 | diff.py (DeepDiff + tabulate) | 当日と前日の JSON を比較して変更点をテキスト出力 |
| ④ AI 分析 | Claude Code Skill (check-pbi-progress) | 差分テキストを受け取り、進捗サマリーと達成リスクを JSON で返す |
| ⑤ Slack 通知 | GitHub Actions + curl | 分析結果を Slack Incoming Webhook で投稿 |
.
├── .claude/
│ └── skills/
│ └── check-pbi-progress/
│ ├── SKILL.md # Claude Code Skill 定義
│ └── schema.json # 出力 JSON スキーマ
├── scripts/
│ ├── fetch_progress.sh # GitHub Projects データ取得
│ └── diff.py # スナップショット差分比較(PEP 723)
└── .github/
└── workflows/
└── daily-progress.yml # GitHub Actions ワークフロー
前提設定① GitHub App の作成
GitHub Projects(Organization Projects)には GITHUB_TOKEN ではアクセスできません。Organization レベルのリソースにアクセスするには GitHub App Token が必要です。
GitHub App に必要な権限
| 権限 | 種別 | 必要な理由 |
|---|---|---|
| Organization: Projects | Read-only | GitHub Projects V2 の読み取り |
| Organization: Members | Read-only | アサイン情報の取得 |
| Repository: Contents | Read-only | コード参照(任意) |
| Repository: Issues | Read-only | Issue タイトル・ステータスの取得 |
| Repository: Metadata | Read-only | 必須(基本情報) |
- GitHub の「Settings → Developer settings → GitHub Apps → New GitHub App」から作成
- 上記の権限を設定して保存
- 「Install App」でリポジトリ(または Organization)にインストール
- 「Generate a private key」で秘密鍵(.pem ファイル)をダウンロード
- App ID と秘密鍵の内容を GitHub Actions の Secrets/Variables に登録
# Secrets(外部流出すると危険なもの) SLACK_WEBHOOK_URL : Slack Incoming Webhook の URL APP_PEM : GitHub App の秘密鍵(.pem ファイルの内容全体) # Variables(設定値として管理するもの) APP_ID : GitHub App の App ID PROJECT_NUMBER : 対象 GitHub Projects の番号(URLの /projects/N の N) ORG_NAME : Organization 名 AWS_ROLE_ARN : GitHub Actions から AssumeRole する IAM Role ARN REPORT_BUCKET_NAME : スナップショット保存先 S3 バケット名
前提設定② AWS 設定(Bedrock + S3 + OIDC)
GitHub Actions から AWS に安全にアクセスするため、OIDC(OpenID Connect) を使います。アクセスキーをシークレットに登録する方法より安全で、一時的な認証情報を自動取得できます。
IAM ロールの信頼ポリシー
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:ORG_NAME/REPO_NAME:ref:refs/heads/main"
}
}
}
]
}
IAM ロールに付与するポリシー
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel",
"bedrock:InvokeModelWithResponseStream",
"bedrock:ListInferenceProfiles"
],
"Resource": [
"arn:aws:bedrock:*:*:inference-profile/*",
"arn:aws:bedrock:*:*:application-inference-profile/*",
"arn:aws:bedrock:*:*:foundation-model/*"
]
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:ListBucket",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::YOUR-BUCKET-NAME",
"arn:aws:s3:::YOUR-BUCKET-NAME/*"
]
}
]
}
token.actions.githubusercontent.com を OpenID Connect 形式で追加してください。サムプリントは自動取得されます。Step 1: GitHub Projects データ取得スクリプト(fetch_progress.sh)
gh CLI の GraphQL 機能を使って、現在のスプリントに属する PBI のタイトル・ステータス・due date・アサイン情報を取得し、JSON ファイルに保存します。
スプリント判定のしくみ
GitHub Projects API には「現在のスプリントを返す」エンドポイントが存在しません。Iteration フィールドから全スプリントの startDate と duration(週数)を取得し、今日の日付がどのスプリントの範囲内かを bash 側で計算します。
#!/usr/bin/env bash
# GitHub Projects V2 からスプリント進捗を取得して JSON 保存
# Usage: bash scripts/fetch_progress.sh ORG_NAME PROJECT_NUMBER OUTPUT_DIR
set -euo pipefail
ORG="$1"
PROJECT_NUM="$2"
OUTPUT_DIR="${3:-.}"
TODAY=$(date +%Y-%m-%d)
OUTPUT_FILE="${OUTPUT_DIR}/progress_${TODAY}.json"
echo "=== プロジェクト情報を取得中: org=${ORG}, project=${PROJECT_NUM} ==="
# 1. プロジェクト Node ID を取得
PROJECT_ID=$(gh api graphql -f query='
query($org: String!, $num: Int!) {
organization(login: $org) {
projectV2(number: $num) {
id
title
}
}
}
' -F org="$ORG" -F num="$PROJECT_NUM" --jq '.data.organization.projectV2.id')
echo "Project ID: ${PROJECT_ID}"
# 2. Iteration フィールドのメタデータを取得(全スプリント一覧)
ITERATIONS=$(gh api graphql -f query='
query($projectId: ID!) {
node(id: $projectId) {
... on ProjectV2 {
fields(first: 20) {
nodes {
... on ProjectV2IterationField {
id
name
configuration {
iterations {
id
title
startDate
duration
}
completedIterations {
id
title
startDate
duration
}
}
}
}
}
}
}
}
' -F projectId="$PROJECT_ID" --jq '.data.node.fields.nodes[] | select(.configuration != null)')
# 3. 今日の日付を UNIX time に変換して現在スプリントを特定
TODAY_UNIX=$(date -d "$TODAY" +%s 2>/dev/null || date -jf "%Y-%m-%d" "$TODAY" +%s)
CURRENT_SPRINT_ID=""
while IFS= read -r iter; do
START=$(echo "$iter" | jq -r '.startDate')
DURATION=$(echo "$iter" | jq -r '.duration') # 週数
DURATION=${DURATION:-2} # null/0 の場合は 2 週間をデフォルトに
[ "$DURATION" -eq 0 ] 2>/dev/null && DURATION=2
ITER_ID=$(echo "$iter" | jq -r '.id')
START_UNIX=$(date -d "$START" +%s 2>/dev/null || date -jf "%Y-%m-%d" "$START" +%s)
END_UNIX=$(( START_UNIX + DURATION * 7 * 86400 ))
if [ "$TODAY_UNIX" -ge "$START_UNIX" ] && [ "$TODAY_UNIX" -lt "$END_UNIX" ]; then
CURRENT_SPRINT_ID="$ITER_ID"
CURRENT_SPRINT_TITLE=$(echo "$iter" | jq -r '.title')
CURRENT_SPRINT_START="$START"
echo "現在のスプリント: ${CURRENT_SPRINT_TITLE} (${START} ~ + ${DURATION}週)"
break
fi
done < <(echo "$ITERATIONS" | jq -c '.configuration.iterations[]')
if [ -z "$CURRENT_SPRINT_ID" ]; then
echo "警告: 現在のスプリントが見つかりません。全アイテムを取得します。"
fi
# 4. プロジェクトアイテムを取得(ステータス・due date・イテレーション含む)
ITEMS=$(gh api graphql -f query='
query($projectId: ID!) {
node(id: $projectId) {
... on ProjectV2 {
items(first: 100) {
nodes {
id
fieldValues(first: 20) {
nodes {
... on ProjectV2ItemFieldSingleSelectValue {
name
field { ... on ProjectV2FieldCommon { name } }
}
... on ProjectV2ItemFieldDateValue {
date
field { ... on ProjectV2FieldCommon { name } }
}
... on ProjectV2ItemFieldIterationValue {
iterationId
title
startDate
duration
field { ... on ProjectV2FieldCommon { name } }
}
... on ProjectV2ItemFieldNumberValue {
number
field { ... on ProjectV2FieldCommon { name } }
}
... on ProjectV2ItemFieldTextValue {
text
field { ... on ProjectV2FieldCommon { name } }
}
}
}
content {
... on Issue {
number
title
state
url
assignees(first: 5) {
nodes { login }
}
labels(first: 10) {
nodes { name color }
}
}
... on DraftIssue {
title
body
}
}
}
}
}
}
}
' -F projectId="$PROJECT_ID" --jq '.data.node.items.nodes')
# 5. 現在のスプリントに属するアイテムだけを抽出
if [ -n "$CURRENT_SPRINT_ID" ]; then
FILTERED=$(echo "$ITEMS" | jq --arg sprintId "$CURRENT_SPRINT_ID" '
[.[] | select(
.fieldValues.nodes[]? |
select(type == "object" and .iterationId? == $sprintId)
)]
')
else
FILTERED="$ITEMS"
fi
# 6. JSON 整形してファイルに保存
echo "$FILTERED" | jq --arg date "$TODAY" --arg sprint "${CURRENT_SPRINT_TITLE:-unknown}" '
{
snapshot_date: $date,
sprint: $sprint,
items: [.[] | {
id: .id,
title: (.content.title // "(タイトルなし)"),
issue_number: (.content.number // null),
url: (.content.url // null),
state: (.content.state // "DRAFT"),
assignees: [.content.assignees?.nodes[]?.login // empty],
status: (
.fieldValues.nodes[] |
select(type == "object" and .field?.name? == "Status") |
.name
) // null,
due_date: (
.fieldValues.nodes[] |
select(type == "object" and .field?.name? == "Due Date") |
.date
) // null,
sprint_name: (
.fieldValues.nodes[] |
select(type == "object" and (.field?.name? | test("sprint|iteration"; "i"))) |
.title
) // null
}]
}
' > "$OUTPUT_FILE"
echo "保存完了: ${OUTPUT_FILE}"
echo "取得アイテム数: $(jq '.items | length' "$OUTPUT_FILE")"
# 7. スプリント残り日数を計算して /tmp/sprint_meta.txt に書き出す
# (SKILL.md の動的コンテキスト注入で参照)
if [ -n "$CURRENT_SPRINT_ID" ] && [ -n "$CURRENT_SPRINT_START" ]; then
SPRINT_END_UNIX=$(( $(date -d "$CURRENT_SPRINT_START" +%s 2>/dev/null || date -jf "%Y-%m-%d" "$CURRENT_SPRINT_START" +%s) + DURATION * 7 * 86400 ))
REMAINING_DAYS=$(( ( SPRINT_END_UNIX - TODAY_UNIX ) / 86400 ))
echo "$REMAINING_DAYS" > /tmp/sprint_meta.txt
echo "スプリント残り: ${REMAINING_DAYS}日"
fi
date -d は Linux 向け、date -jf は macOS 向けです。GitHub Actions(ubuntu-latest)では date -d が使えます。ローカルの macOS で実行する場合は、スクリプト内の date -d 呼び出しを date -jf "%Y-%m-%d" 形式に統一してください。Step 2: スナップショット差分比較スクリプト(diff.py)
当日と前日のスナップショット JSON を比較し、変更点を人間が読みやすい形式で出力します。PEP 723 形式で依存関係を埋め込んでいるため、uv run scripts/diff.py だけで実行できます(pip install 不要)。
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "deepdiff>=8.0",
# "tabulate>=0.9",
# ]
# ///
"""
GitHub Projects スナップショット差分比較スクリプト
Usage:
uv run scripts/diff.py <old_file> <new_file>
Output:
変更点をテキスト形式で stdout に出力
"""
import json
import sys
from deepdiff import DeepDiff
from tabulate import tabulate
def load_json(path: str) -> dict:
with open(path, encoding="utf-8") as f:
return json.load(f)
def build_item_index(snapshot: dict) -> dict[str, dict]:
"""issue番号をキーにしたアイテム辞書を作成"""
index = {}
for item in snapshot.get("items", []):
# issue番号があればそれをキーに、なければタイトルをキーに
key = str(item.get("issue_number") or item.get("title", "unknown"))
index[key] = item
return index
def compare_snapshots(old_file: str, new_file: str) -> str:
old = load_json(old_file)
new = load_json(new_file)
old_index = build_item_index(old)
new_index = build_item_index(new)
lines = []
lines.append(f"## スプリント: {new.get('sprint', 'unknown')}")
lines.append(f"## 比較: {old.get('snapshot_date', '?')} → {new.get('snapshot_date', '?')}")
lines.append("")
# ステータス変更
status_changes = []
due_changes = []
new_items = []
removed_items = []
for key, new_item in new_index.items():
if key not in old_index:
new_items.append([
f"#{key}" if new_item.get("issue_number") else key,
new_item.get("title", "")[:50],
new_item.get("status", "-"),
new_item.get("due_date", "-"),
", ".join(new_item.get("assignees", [])) or "-",
])
continue
old_item = old_index[key]
# ステータス変更の検出
old_status = old_item.get("status")
new_status = new_item.get("status")
if old_status != new_status:
status_changes.append([
f"#{key}" if new_item.get("issue_number") else key,
new_item.get("title", "")[:40],
old_status or "-",
new_status or "-",
])
# due date 変更の検出
old_due = old_item.get("due_date")
new_due = new_item.get("due_date")
if old_due != new_due:
due_changes.append([
f"#{key}" if new_item.get("issue_number") else key,
new_item.get("title", "")[:40],
old_due or "未設定",
new_due or "未設定",
])
# 削除されたアイテム
for key, old_item in old_index.items():
if key not in new_index:
removed_items.append([
f"#{key}" if old_item.get("issue_number") else key,
old_item.get("title", "")[:50],
old_item.get("status", "-"),
])
# 現在のサマリー
total = len(new_index)
done = sum(1 for i in new_index.values() if (i.get("status") or "").lower() in ["done", "完了", "closed"])
in_progress = sum(1 for i in new_index.values() if (i.get("status") or "").lower() in ["in progress", "進行中", "wip"])
todo = total - done - in_progress
lines.append("### 現在の状況")
lines.append(f"総 PBI 数: {total} 完了: {done} 進行中: {in_progress} 未着手: {todo}")
if total > 0:
lines.append(f"達成率: {done / total * 100:.1f}%")
lines.append("")
if status_changes:
lines.append("### ステータス変更")
lines.append(tabulate(
status_changes,
headers=["#", "タイトル", "旧ステータス", "新ステータス"],
tablefmt="simple",
))
lines.append("")
if due_changes:
lines.append("### due date 変更")
lines.append(tabulate(
due_changes,
headers=["#", "タイトル", "旧 due date", "新 due date"],
tablefmt="simple",
))
lines.append("")
if new_items:
lines.append("### 新規追加 PBI")
lines.append(tabulate(
new_items,
headers=["#", "タイトル", "ステータス", "due date", "担当"],
tablefmt="simple",
))
lines.append("")
if removed_items:
lines.append("### スプリントから除外された PBI")
lines.append(tabulate(
removed_items,
headers=["#", "タイトル", "最終ステータス"],
tablefmt="simple",
))
lines.append("")
if not any([status_changes, due_changes, new_items, removed_items]):
lines.append("変更なし(前日と同一)")
return "\n".join(lines)
if __name__ == "__main__":
if len(sys.argv) < 3:
print("Usage: uv run scripts/diff.py <old_file> <new_file>", file=sys.stderr)
sys.exit(1)
result = compare_snapshots(sys.argv[1], sys.argv[2])
print(result)
# /// script ... # /// ブロックに依存関係を記述しておくと、uv run が自動で仮想環境を作成・依存インストールして実行します。CI 環境では pip install uv && uv run scripts/diff.py だけで動き、requirements.txt のメンテナンスが不要になります。Step 3: Claude Code Skill の設計(check-pbi-progress)
差分テキストを受け取り、スプリントの進捗サマリーとリスク分析を JSON 形式で返すスキルを定義します。Skills 設計ガイドで解説している「実行層スキル」パターンに従い、書き込み権限を持たせない構成にします。
出力 JSON スキーマの定義
先に出力形式を決めておくことで、後続の GitHub Actions ステップから structured_output として参照しやすくなります。
{
"type": "object",
"properties": {
"sprint": {
"type": "string",
"description": "現在のスプリント名"
},
"snapshot_date": {
"type": "string",
"description": "本日の日付(YYYY-MM-DD)"
},
"summary": {
"type": "string",
"description": "進捗状況の日本語サマリー(3〜5文)"
},
"completion_rate": {
"type": "number",
"description": "完了率(0〜100の整数)"
},
"risks": {
"type": "array",
"items": { "type": "string" },
"description": "達成リスクのリスト(リスクがない場合は空配列)"
},
"completed_today": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"title": { "type": "string" }
}
},
"description": "当日完了した PBI のリスト"
},
"schedule_changes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"title": { "type": "string" },
"change": { "type": "string" }
}
},
"description": "スケジュール変更(due date 変更・スプリント追加・除外)"
},
"slack_message": {
"type": "string",
"description": "Slack に投稿するための整形済みメッセージ(Markdown形式)"
}
},
"required": ["sprint", "snapshot_date", "summary", "completion_rate", "risks", "completed_today", "schedule_changes", "slack_message"]
}
SKILL.md の定義
---
name: check-pbi-progress
description: GitHub Projects のスプリント進捗を分析して達成リスクと当日の変更点を JSON で返す。毎朝の定期実行で前日スナップショットとの差分を受け取り分析する用途に使用。
context: fork
allowed-tools: Bash(uv *), Bash(cat *), Bash(ls *), Read
---
## 実行日: !`date +%Y-%m-%d`
## スプリント残り日数の目安: !`python3 -c "from datetime import date; import sys; args=open('/tmp/sprint_meta.txt').read().split() if __import__('os').path.exists('/tmp/sprint_meta.txt') else []; print(args[0] if args else '不明')"`
## タスク
前日と当日のスプリントスナップショット差分を分析し、以下の観点でサマリーを作成する。
### 入力の読み込み
1. `cat /tmp/progress_diff.txt` で差分テキストを読み込む
2. `cat /tmp/progress_today.json` で当日スナップショット全体を確認する
### 分析の観点
#### 進捗評価
- 現在の完了率は目標に対して適切か
- スプリント終了日までに残タスクを消化できそうか(日数 vs 残タスク数)
- 複数日連続でステータスが変わっていない PBI がないか(停滞の兆候)
#### リスク判定(以下の条件でリスクと判断する)
- 完了率がスプリント経過日数の割合を下回っている(例:スプリント50%消化で完了率30%)
- due date が変更された PBI がある
- 長期間「進行中」のままステータスが変わっていない PBI がある
- スプリント残り2日以内で未着手 PBI が3件以上ある
#### Slack メッセージの形式
Slack 通知用のメッセージを以下の形式で生成する:
- 1行目: 絵文字 + スプリント名 + 達成率(例: ✅ Sprint 12 | 達成率 65%)
- 2行目以降: 当日完了した PBI のリスト(完了があれば)
- リスクがある場合: ⚠️ から始まるリスク一覧
- スケジュール変更があれば?から始まる変更内容
### 出力
`.claude/skills/check-pbi-progress/schema.json` のスキーマに従って JSON を出力する。
context: fork を指定すると、Claude Code はメイン会話の履歴を引き継がない分離されたコンテキストでスキルを実行します。GitHub Actions のような「会話の外から実行する」ユースケースでは、既存の会話コンテキストがないため context: fork の有無による動作差異はほぼありませんが、明示することでスキルの独立性を設計として表現できます。詳細はSubagents ガイドを参照してください。動的コンテキスト注入について
SKILL.md 内の !`command` 構文は Claude Code の独自記法です。スキルが読み込まれる際にシェルコマンドが評価され、その出力がプロンプトに展開されます。Claude が受け取るプロンプトには、実行時点の日付や環境情報が展開済みの状態で渡されます。
| 記法 | 展開される内容 | 活用例 |
|---|---|---|
!`date +%Y-%m-%d` |
2026-03-26 | 今日の日付をコンテキストに含める |
!`git log --oneline -3` |
直近のコミット3件 | コードレビュースキルで変更内容を伝える |
!`cat README.md | head -20` |
README の冒頭20行 | プロジェクト概要をスキルに伝える |
!`wc -l src/**/*.ts` |
各ファイルの行数 | コードベースの規模情報 |
Step 4: GitHub Actions ワークフロー(daily-progress.yml)
GitHub Actions で平日の朝 10:00 JST(UTC 01:00)に自動実行するワークフローを定義します。
name: Daily Sprint Progress Analysis
on:
schedule:
# 平日 10:00 JST(UTC 01:00)に実行
- cron: '0 1 * * 1-5'
workflow_dispatch:
inputs:
target_date:
description: '比較対象日(YYYY-MM-DD、省略で昨日)'
required: false
permissions:
id-token: write # AWS OIDC 認証に必要
contents: read
jobs:
analyze-progress:
runs-on: ubuntu-latest
env:
ORG_NAME: ${{ vars.ORG_NAME }}
PROJECT_NUMBER: ${{ vars.PROJECT_NUMBER }}
BUCKET: ${{ vars.REPORT_BUCKET_NAME }}
steps:
# ── 1. チェックアウト ──
- uses: actions/checkout@v4
# ── 2. ツールセットアップ ──
- name: Setup uv
uses: astral-sh/setup-uv@v5
# ── 3. AWS 認証(OIDC)──
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: us-east-1
# ── 4. GitHub App Token 発行 ──
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v2
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PEM }}
owner: ${{ env.ORG_NAME }}
# ── 5. S3 から過去スナップショットを同期 ──
- name: Sync snapshots from S3
run: |
mkdir -p /tmp/snapshots
aws s3 sync "s3://${BUCKET}/snapshots/" /tmp/snapshots/ --quiet
echo "同期済みファイル数: $(ls /tmp/snapshots/ 2>/dev/null | wc -l)"
# ── 6. 当日の進捗を取得 ──
- name: Fetch today's progress
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
bash scripts/fetch_progress.sh "$ORG_NAME" "$PROJECT_NUMBER" /tmp/snapshots
# ── 7. 前日スナップショットを特定 ──
- name: Find previous snapshot
id: find-prev
run: |
TODAY=$(date +%Y-%m-%d)
TODAY_FILE="/tmp/snapshots/progress_${TODAY}.json"
# 前日スナップショットを日付順で検索(土日は金曜を使用)
PREV_FILE=$(ls /tmp/snapshots/progress_*.json 2>/dev/null | grep -v "progress_${TODAY}" | sort | tail -1)
echo "today_file=${TODAY_FILE}" >> "$GITHUB_OUTPUT"
echo "prev_file=${PREV_FILE:-$TODAY_FILE}" >> "$GITHUB_OUTPUT"
if [ -z "$PREV_FILE" ]; then
echo "警告: 前日スナップショットが見つかりません。当日データのみで分析します。"
cp "$TODAY_FILE" "/tmp/prev_fallback.json"
echo "prev_file=/tmp/prev_fallback.json" >> "$GITHUB_OUTPUT"
fi
# ── 8. 差分を計算 ──
- name: Calculate diff
run: |
uv run scripts/diff.py "${{ steps.find-prev.outputs.prev_file }}" "${{ steps.find-prev.outputs.today_file }}" > /tmp/progress_diff.txt
echo "=== 差分サマリー ==="
cat /tmp/progress_diff.txt
# スキルが参照できるよう当日ファイルもコピー
cp "${{ steps.find-prev.outputs.today_file }}" /tmp/progress_today.json
# ── 9. Claude Code Skills で分析 ──
- name: Analyze with Claude Code
id: claude
uses: anthropics/claude-code-action@v1
with:
use_bedrock: "true"
github_token: ${{ steps.app-token.outputs.token }}
prompt: |
/check-pbi-progress
claude_args: |
--max-turns 10
--json-schema .claude/skills/check-pbi-progress/schema.json
env:
AWS_REGION: us-east-1
# ── 10. 当日スナップショットを S3 に保存 ──
- name: Upload today's snapshot to S3
run: |
TODAY=$(date +%Y-%m-%d)
aws s3 cp "/tmp/snapshots/progress_${TODAY}.json" "s3://${BUCKET}/snapshots/progress_${TODAY}.json"
# ── 11. Slack 通知 ──
- name: Notify Slack
if: always()
run: |
OUTPUT='${{ steps.claude.outputs.structured_output }}'
if [ -z "$OUTPUT" ] || [ "$OUTPUT" = "null" ]; then
# Claude の分析が失敗した場合は差分テキストをそのまま送る
MESSAGE="*スプリント進捗レポート($(date +%Y-%m-%d))*\n$(cat /tmp/progress_diff.txt | head -30)"
else
MESSAGE=$(echo "$OUTPUT" | jq -r '.slack_message')
fi
curl -s -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" -H 'Content-type: application/json' --data "$(jq -n --arg text "$MESSAGE" '{text: $text}')"
GITHUB_TOKEN ではなく GitHub App Token を使う:ステップ 9 の github_token には steps.app-token.outputs.token を渡しています。secrets.GITHUB_TOKEN を使うと Organization Projects への読み取りアクセスが拒否されます。GitHub App Token の有効期限は最大 1 時間のため、1 回のワークフロー実行であれば問題ありません。Step 5: Slack 通知メッセージの実装
Claude が返す slack_message フィールドをそのまま送る方式が最もシンプルです。ただし、Slack Block Kit を使って視覚的に整えたい場合の実装例も紹介します。
シンプルな通知(推奨)
Claude に slack_message フィールドで整形済みメッセージを返させ、curl でそのまま送信します。
- name: Notify Slack
run: |
MESSAGE='${{ fromJSON(steps.claude.outputs.structured_output).slack_message }}'
RATE='${{ fromJSON(steps.claude.outputs.structured_output).completion_rate }}'
curl -s -X POST "${{ secrets.SLACK_WEBHOOK_URL }}" -H 'Content-type: application/json' --data "$(jq -n --arg text "$MESSAGE" --argjson rate "$RATE" '{
text: $text,
username: "Sprint Bot"
}'
)"
Block Kit を使ったリッチな通知(任意)
進捗率を視覚的なバーで表示したい場合、structured_output の各フィールドを使って Block Kit メッセージを組み立てられます。
#!/usr/bin/env bash
# Slack Block Kit メッセージを組み立てる
OUTPUT="$1" # structured_output の JSON
SPRINT=$(echo "$OUTPUT" | jq -r '.sprint')
RATE=$(echo "$OUTPUT" | jq -r '.completion_rate')
SUMMARY=$(echo "$OUTPUT" | jq -r '.summary')
RISKS=$(echo "$OUTPUT" | jq -r '.risks[]' 2>/dev/null | sed 's/^/• /' | head -5)
DATE=$(echo "$OUTPUT" | jq -r '.snapshot_date')
# 達成率バー(■ □ で10段階表示)
BAR_FILLED=$(( RATE / 10 ))
BAR=""
for i in $(seq 1 10); do
if [ $i -le $BAR_FILLED ]; then
BAR="${BAR}■"
else
BAR="${BAR}□"
fi
done
EMOJI=":white_check_mark:"
[ "$RATE" -lt 80 ] && EMOJI=":hourglass_flowing_sand:"
[ "$RATE" -lt 50 ] && EMOJI=":warning:"
jq -n --arg sprint "$SPRINT" --arg rate "$RATE" --arg bar "$BAR" --arg summary "$SUMMARY" --arg risks "$RISKS" --arg date "$DATE" --arg emoji "$EMOJI" '{
blocks: [
{
type: "header",
text: {
type: "plain_text",
text: ($emoji + " " + $sprint + " 進捗レポート (" + $date + ")")
}
},
{
type: "section",
fields: [
{ type: "mrkdwn", text: ("*完了率*
" + $bar + " " + $rate + "%") },
{ type: "mrkdwn", text: ("*サマリー*
" + $summary) }
]
},
(if $risks != "" then {
type: "section",
text: {
type: "mrkdwn",
text: ("*⚠️ リスク*
" + $risks)
}
} else empty end)
]
}'
応用・カスタマイズパターン
① S3 なしで Git ブランチに保存する
S3 の代わりに Git リポジトリのブランチにスナップショットを保存することもできます。
# S3 同期の代わりに専用ブランチから取得
- name: Checkout snapshot branch
uses: actions/checkout@v4
with:
ref: snapshots
path: snapshots-repo
# このブランチは README.md なしで初期化しておく
# ワークフロー後半でブランチにコミット
- name: Commit snapshot
run: |
TODAY=$(date +%Y-%m-%d)
cp "/tmp/snapshots/progress_${TODAY}.json" snapshots-repo/
cd snapshots-repo
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add "progress_${TODAY}.json"
git commit -m "snapshot: ${TODAY}" || echo "変更なし"
git push origin snapshots
② 週次サマリーを月曜日に送る
月曜日だけ「先週のスプリントまとめ」を送るよう、cron と条件分岐を組み合わせます。
on:
schedule:
- cron: '0 1 * * 1-5' # 平日 10:00 JST
jobs:
analyze-progress:
steps:
# ...(通常の日次処理)...
- name: Weekly summary(月曜のみ)
if: ${{ github.event.schedule == '0 1 * * 1' || (github.event_name == 'workflow_dispatch' && github.event.inputs.weekly == 'true') }}
uses: anthropics/claude-code-action@v1
with:
use_bedrock: "true"
prompt: |
先週1週間分のスナップショット(/tmp/snapshots/内の直近5ファイル)を読み込み、
スプリント全体の週次サマリーを日本語で作成してください。
先週完了した PBI 数・残タスク・主要なスケジュール変更を含めてください。
③ 特定ラベルの PBI だけを対象にする
diff.py に --label オプションを追加することで、特定ラベル(例: priority:high)の PBI だけを分析対象に絞れます。
import argparse
parser = argparse.ArgumentParser()
parser.add_argument("old_file")
parser.add_argument("new_file")
parser.add_argument("--label", help="フィルタするラベル名(例: priority:high)")
args = parser.parse_args()
def load_and_filter(path, label_filter=None):
data = load_json(path)
if label_filter:
data["items"] = [
item for item in data.get("items", [])
if any(l.get("name") == label_filter
for l in item.get("labels", []))
]
return data
④ 複数プロジェクトを並列分析する
ワークフローガイドで解説している matrix strategy を使うと、複数の GitHub Projects を並列で分析できます。
jobs:
analyze-projects:
strategy:
matrix:
project:
- { name: frontend, number: 12 }
- { name: backend, number: 15 }
- { name: infra, number: 8 }
fail-fast: false # 1つ失敗しても他は継続
runs-on: ubuntu-latest
steps:
- name: Fetch progress
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
bash scripts/fetch_progress.sh "$ORG_NAME" "${{ matrix.project.number }}" /tmp/snapshots/${{ matrix.project.name }}
セットアップチェックリスト
実際に動かすまでに必要な設定をまとめます。
| 分類 | 設定項目 | 確認方法 |
|---|---|---|
| GitHub App | App 作成・Organization にインストール | Settings → GitHub Apps |
| GitHub App | Projects read-only / Members read-only 権限 | App の Permissions タブ |
| GitHub App | App ID を Variables に登録 | vars.APP_ID |
| GitHub App | 秘密鍵(PEM)を Secrets に登録 | secrets.APP_PEM |
| AWS | GitHub Actions OIDC IdP を登録 | IAM → ID プロバイダ |
| AWS | IAM ロール(Bedrock + S3 権限)を作成 | IAM → ロール |
| AWS | ロール ARN を Variables に登録 | vars.AWS_ROLE_ARN |
| AWS | Amazon Bedrock でモデルアクセスを有効化 | Bedrock → モデルアクセス |
| S3 | スナップショット保存用バケットを作成 | バケット名を vars.REPORT_BUCKET_NAME に登録 |
| Slack | Incoming Webhook URL を Secrets に登録 | secrets.SLACK_WEBHOOK_URL |
| GitHub | Project 番号を Variables に登録 | vars.PROJECT_NUMBER(URL の /projects/N) |
| GitHub | Organization 名を Variables に登録 | vars.ORG_NAME |
よくある質問
GITHUB_TOKEN はワークフローを実行しているリポジトリの権限のみを持ち、Organization Projects への読み取り権限がありません。GitHub App は Organization レベルの権限を付与できるため、Organization Projects にアクセスできます。Personal リポジトリ直下のプロジェクトであれば GITHUB_TOKEN でアクセスできる場合もありますが、チームで使う Organization Projects では GitHub App が必要です。startDate + duration(週数)で計算しています。duration フィールドは GitHub Projects GraphQL API の公式ドキュメントに明記はありませんが、ProjectV2ItemFieldIterationValue のレスポンスに含まれます。スプリントの設定方法によっては duration が 0 や null になるケースがあります。その場合は fetch_progress.sh のデフォルト期間(2週間=14日)を変数として外出しして調整してください。--max-turns 5 に絞るか、差分テキストのみ(スナップショット全体は渡さない)で分析させると削減できます。items(first: 100) を増やすか、after カーソルを使ったページネーション処理を追加してください。また、Organizations のレート制限(5,000 リクエスト/時間)に引っかかることもあります。複数プロジェクトを同時に取得する場合は matrix の concurrency を制限してください。model フィールドか、claude_args の --model オプションで行います。Bedrock のモデルアクセスは事前に AWS コンソールで有効化が必要です。if: steps.diff.outputs.has_changes == 'true' のように条件分岐を追加してください。あるいは「変更なし」の日は短い通知だけ送るなど、チームの運用スタイルに合わせて調整できます。まとめ
この記事で紹介した構成をまとめます。
| 要素 | 技術スタック | 役割 |
|---|---|---|
| データ取得 | gh CLI + GraphQL | Organization Projects の PBI・ステータス・スプリント情報を取得 |
| スナップショット管理 | AWS S3 | 日次 JSON を蓄積。前日比較の基準データとして参照 |
| 差分比較 | Python + DeepDiff + tabulate(PEP 723) | 変更点をテキスト形式で可視化。uv run だけで実行可能 |
| AI 分析 | Claude Code Skill(context: fork) | 差分テキストから進捗サマリーとリスクを JSON で返す |
| CI/CD | GitHub Actions + claude-code-action | 平日自動実行。Bedrock 経由で Claude を呼び出す |
| 通知 | Slack Incoming Webhook | 整形済みメッセージを毎朝チャンネルに投稿 |
この仕組みを使うことで、「昨日から何が進んだか」を手動で確認する作業がなくなり、スプリント達成リスクを早期に把握できるようになります。Claude Code Skills の使い方はSkills入門・Skills設計ガイドと合わせて参照してください。GitHub Actions との連携の基本についてはGitHub Actions ガイドも参照してください。

