GitHub Actions × Claude Code Skillsでスプリント進捗を自動分析|GitHub Projectsの日次スナップショット差分をSlackに通知する実装ガイド

GitHub Actions × Claude Code Skillsでスプリント進捗を自動分析|GitHub Projectsの日次スナップショット差分をSlackに通知する実装ガイド AI開発

「先週何が終わったか、朝会で確認してから気づく」「スプリント後半になるまでリスクが見えない」——GitHub Projects を使っているチームでよく聞く悩みです。

この記事では、GitHub Projects の進捗を毎日自動取得し、前営業日との差分を Claude Code Skills で分析して Slack に自動報告する仕組みの作り方を解説します。

ポイントは「スナップショットの蓄積」です。GitHub Projects API は現在の状態しか返さないため、履歴を持てません。日々の JSON スナップショットを S3 に保存して比較することで、「昨日から何が進んだか」「due date がいつ変わったか」「スプリントのリスクはどこか」を定量的に把握できるようになります。Claude Code Skills の使い方はSkills入門Skills設計ガイドも合わせて参照してください。

スポンサーリンク

この仕組みで解決できること

毎日の朝会・スプリントレビュー前に自動で 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 ワークフロー
S3 なしで動かすことも可能です:スナップショットを S3 ではなく Git リポジトリのブランチに保存するアプローチもあります。S3 を使う利点は、アクセスログや保存コスト管理が容易な点です。本記事では S3 を使う構成で説明しますが、Git 保存版の応用例は後半で紹介します。

前提設定① 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 必須(基本情報)
  1. GitHub の「Settings → Developer settings → GitHub Apps → New GitHub App」から作成
  2. 上記の権限を設定して保存
  3. 「Install App」でリポジトリ(または Organization)にインストール
  4. 「Generate a private key」で秘密鍵(.pem ファイル)をダウンロード
  5. App ID と秘密鍵の内容を GitHub Actions の Secrets/Variables に登録
GitHub Actions で登録する設定値
# 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 ロールの信頼ポリシー

trust-policy.json(OIDC 信頼ポリシー)
{
  "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 ロールに付与するポリシー

iam-policy.json(Bedrock + S3 アクセス)
{
  "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/*"
      ]
    }
  ]
}
OIDC IdP の登録:まだ設定していない場合は、AWS コンソールの「IAM → ID プロバイダ → プロバイダを追加」から token.actions.githubusercontent.com を OpenID Connect 形式で追加してください。サムプリントは自動取得されます。

Step 1: GitHub Projects データ取得スクリプト(fetch_progress.sh)

gh CLI の GraphQL 機能を使って、現在のスプリントに属する PBI のタイトル・ステータス・due date・アサイン情報を取得し、JSON ファイルに保存します。

スプリント判定のしくみ

GitHub Projects API には「現在のスプリントを返す」エンドポイントが存在しません。Iteration フィールドから全スプリントの startDateduration(週数)を取得し、今日の日付がどのスプリントの範囲内かを bash 側で計算します。

scripts/fetch_progress.sh
#!/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 不要)。

scripts/diff.py(PEP 723 形式)
# /// 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)
PEP 723 の利点:スクリプト先頭の # /// 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 として参照しやすくなります。

.claude/skills/check-pbi-progress/schema.json
{
  "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 の定義

.claude/skills/check-pbi-progress/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 について: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)に自動実行するワークフローを定義します。

.github/workflows/daily-progress.yml
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 でそのまま送信します。

シンプルな Slack 通知ステップ
- 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 メッセージを組み立てられます。

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 リポジトリのブランチにスナップショットを保存することもできます。

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 だけを分析対象に絞れます。

diff.py にラベルフィルタを追加する例
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 を並列で分析できます。

matrix で複数プロジェクトを並列処理
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

よくある質問

QGITHUB_TOKEN ではなく GitHub App Token が必要な理由は?
AGitHub Projects(Organization Projects)は Organization レベルのリソースです。GITHUB_TOKEN はワークフローを実行しているリポジトリの権限のみを持ち、Organization Projects への読み取り権限がありません。GitHub App は Organization レベルの権限を付与できるため、Organization Projects にアクセスできます。Personal リポジトリ直下のプロジェクトであれば GITHUB_TOKEN でアクセスできる場合もありますが、チームで使う Organization Projects では GitHub App が必要です。
Q現在のスプリントの判定が正しくできない場合は?
Afetch_progress.sh のスプリント判定は startDate + duration(週数)で計算しています。duration フィールドは GitHub Projects GraphQL API の公式ドキュメントに明記はありませんが、ProjectV2ItemFieldIterationValue のレスポンスに含まれます。スプリントの設定方法によっては duration が 0 や null になるケースがあります。その場合は fetch_progress.sh のデフォルト期間(2週間=14日)を変数として外出しして調整してください。
QClaude の分析コストはどのくらいかかりますか?
A1 回の実行で Claude に渡すコンテキストは「差分テキスト(1〜3KB)+ スナップショット JSON(〜50KB)」が目安です。Bedrock 経由で Claude Sonnet を使う場合、1 回の実行コストは通常 $0.01〜$0.05 程度です。平日 20 回/月実行しても $0.20〜$1.00 の範囲に収まります。コストが気になる場合は --max-turns 5 に絞るか、差分テキストのみ(スナップショット全体は渡さない)で分析させると削減できます。
Qfetch_progress.sh がタイムアウトするケースがあります。
AGitHub Projects に 100 件超の PBI がある場合、GraphQL のページネーションが必要です。スクリプトの items(first: 100) を増やすか、after カーソルを使ったページネーション処理を追加してください。また、Organizations のレート制限(5,000 リクエスト/時間)に引っかかることもあります。複数プロジェクトを同時に取得する場合は matrix の concurrency を制限してください。
QS3 の代わりに GitHub Artifacts に保存できますか?
Aできますが、Artifacts の保存期間はデフォルト 90 日(設定変更で最大 400 日)のため、長期の履歴管理には向きません。また Artifacts は前回のワークフロー実行からのみ取得しやすく、特定日のスナップショットを検索するには一工夫必要です。S3 は日付付きファイル名で蓄積でき、検索も容易なため、継続運用には S3 を推奨します。
Qclaude-code-action の use_bedrock オプションで使えるモデルは?
Aus-east-1 リージョンの Amazon Bedrock で Claude Claude Sonnet 4.6(claude-sonnet-4-6)や Claude Haiku などが利用可能です。モデルの指定は SKILL.md の model フィールドか、claude_args--model オプションで行います。Bedrock のモデルアクセスは事前に AWS コンソールで有効化が必要です。
Q差分がなかった日も Slack 通知が来ますか?
Aデフォルトの実装では毎日通知が送られます。「変更なしの日は通知しない」にしたい場合は、diff.py の出力に変更なしフラグを追加し、GitHub Actions のステップで 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 ガイドも参照してください。