
GitHub Actions ✕ Claude CodeのスキルでGitHub Projectsからプロジェクトの進捗状況を取得・分析してSlackに通知してみた
リテールアプリ共創部@大阪の岩田です。
現在私が参画しているプロジェクトはプロジェクト管理にGitHub Projectsを利用しており、以下画像のようなボードで進捗状況を可視化しています。

ざっくりしたルールは以下の通りです。
- SprintというフィールドにIterationを設定
- start Date/due Dateというフィールドでissueの開始/完了予定日を管理
- 進捗状況に応じてステータスを変更する
デイリーの進捗報告では、上記のようなボードやフィールドを参照しながら「差し込みタスクの影響でxxxの完了予定が少し後ろにズレました」とか、「予定通りに進捗しています」といった報告が飛び交うことになるのですが、この進捗報告について以下のような課題を感じていました。
-
前日と比較して何がどこまで進捗したのか分かりづらい
-
差し込みタスクや見積もりミスに起因してスプリント内でstart Dateやdue Dateを変更された際、何がどう変わったのか把握し辛い
-
結局のところスプリントのゴールは達成できそうなのか、達成できないリスクがあるのか分かりづらい
ということで、これらの課題を解消するためにClaude Codeのスキルを使ってプロジェクトの進捗状況を自動で分析/報告する仕組みを作ってみました。
構成
以下のような構成を作ってみます。

GitHub Actionsのワークフローを定期実行し、GitHub Projectsから進捗状況を取得します。GitHubのAPIだけではProjectsから取得した各フィールド値の履歴が追えないため、進捗状況は日々JSON形式でS3に保存しておきます。
この進捗状況のファイルを前営業日のファイルと比較し、Claude Codeに分析してもらうことで、プロジェクト進行上のリスク有無などを客観的な視点からアドバイスしてもらいます。
やってみる
それでは実際にやっていきましょう。
スキルの準備
まずはClaude Codeのスキルを作成します。ディレクトリの構造は以下の通りです。
.claude/
└── skills
└── check-pbi-progress
├── scripts
│ ├── diff.py
│ └── fetch_progress.sh
└── SKILL.md
まずSKILL.mdです。
---
name: check-pbi-progress
description: 各PBIの進捗状況をチェックするスキル
disable-model-invocation: true
user-invocable: true
allowed-tools:
- Bash(date +%Y_%m_%d)
- Read
- Grep
---
## Context
- 本日の日付: !`date +%Y_%m_%d`
## Your Task
### 1.シェルスクリプトを実行し、プロジェクトの進捗状況ファイルを取得する
- `bash ${CLAUDE_SKILL_DIR}/scripts/fetch_progress.sh $ARGUMENTS[0] $ARGUMENTS[1]` を実行する
- 作成された進捗状況ファイルは`progress report created: "/foo/bar/github-user/project/iteration/yyyy_mm_dd_pbi_progress.json"`のような形式で出力されるので、このファイルパスを取得する。
### 2.前回取得した進捗状況ファイルと今回取得した進捗状況ファイルを比較する
- 先程取得した進捗状況ファイルと同じディレクトリから前回保存した進捗状況ファイルを探す
- スプリントの初日などは前回保存した進捗状況ファイルが存在しないので、その場合は処理を終了する
- 以下のコマンドを実行して進捗状況の差分を取得
`uv run ${CLAUDE_SKILL_DIR}/scripts/diff.py <本日の進捗状況ファイル> <前回保存した進捗状況ファイル>`
- 取得した比較結果をターミナルの表示に最適化したフォーマットで見やすく表示する
### 3.進捗状況について報告
- 今日の進捗状況と前回の進捗状況を比較して、どのPBIが完了したか、どのPBIが遅れているかを要約
- "due Date"が変更されている場合は変更内容についても報告する
- 土日は稼働しないのでその点を考慮する
- 日本の祝日は日本のメンバーが稼働しないため、レビューや仕様確認が停滞することも考慮する
## Output
以下の形式で結果を出力する
```json
{"result":"<報告内容>"}
```
ポイントをいくつか解説します。
まずLLMに本日の日付を正しく認識させるためにコンテキスト情報としてdateコマンドの実行結果を渡しています。
## Context
- 本日の日付: !`date +%Y_%m_%d`
この!`<コマンド>`という記述はスキル実行時に評価され、Claude Codeが実際に仕事を始める前にコンテキスト情報としてClaude Codeに注入されます。
続いて## Your Taskの部分で実際の手順を記述しています。スクリプトの実行をする箇所の${CLAUDE_SKILL_DIR}はSKILL.mdが存在するディレクトリに動的に解決されます。
Available string substitutions
## Outputの部分は出力形式をJSONにするよう指示しています。これは後述するGitHub Actionsの後続ステップからsteps.<ステップのID>.outputs.structured_outputでスキルの実行結果を参照できるようにするためです。通常のテキスト形式だとanthropics/claude-code-action@v1のoutputからは参照できないようです。
GitHub Projectsから進捗状況を取得するシェルスクリプトです。
#!/bin/bash
set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
OWNER="${1:?Usage: $0 <owner> <project_number>}"
PROJECT_NUMBER="${2:?Usage: $0 <owner> <project_number>}"
# === Iteration一覧を取得 ===
ITERATIONS=$(gh api graphql -f query="
{
user(login: \"${OWNER}\") {
projectV2(number: ${PROJECT_NUMBER}) {
field(name: \"Sprint\") {
... on ProjectV2IterationField {
configuration {
iterations {
id
title
startDate
duration
}
}
}
}
}
}
}")
# 現在のスプリントの取得
CURRENT_SPRINT=$(echo "$ITERATIONS" | jq -r "
.data.user.projectV2.field.configuration.iterations[] |
.startDate as \$start |
.duration as \$dur |
(now | strftime(\"%Y-%m-%d\")) as \$today |
(\$start | strptime(\"%Y-%m-%d\") | mktime + (\$dur * 86400) | strftime(\"%Y-%m-%d\")) as \$end |
select(\$today >= \$start and \$today <= \$end) |
.title
")
CURRENT_SPRINT_DIR_NAME=$(echo "${CURRENT_SPRINT}" | tr '[:upper:]' '[:lower:]' | sed 's/ //g')
TODAY=$(date +%Y_%m_%d)
OUTPUT_DIR=$(dirname "${SCRIPT_DIR}")/output/${OWNER}/${PROJECT_NUMBER}
if [ ! -d "${OUTPUT_DIR}" ]; then
mkdir -p "${OUTPUT_DIR}"
fi
if [ ! -d "${OUTPUT_DIR}/${CURRENT_SPRINT_DIR_NAME}" ]; then
mkdir -p "${OUTPUT_DIR}/${CURRENT_SPRINT_DIR_NAME}"
fi
OUTPUT_FILE_NAME="${OUTPUT_DIR}/${CURRENT_SPRINT_DIR_NAME}/${TODAY}_pbi_progress.json"
gh project item-list --owner "${OWNER}" "${PROJECT_NUMBER}" --query "sprint:\"${CURRENT_SPRINT}\"" --format json --limit 100 \
| jq '[.items[] | {number: .content.number, title: .title, status: .status, "due Date": ."due Date", "start Date": ."start Date"}]' \
> "${OUTPUT_FILE_NAME}"
echo "progress report created: \"${OUTPUT_FILE_NAME}\""
スクリプトの第1引数でリポジトリのオーナーを、第2引数でGitHub Projectsのプロジェクト番号を取得して後続処理に利用します。今回はリポジトリのオーナーがユーザーの前提でスクリプトを記述していますが、Organization所有のリポジトリの場合はGraphQLクエリのuserを適宜organizationに変更すれば同じように動作します。
処理の流れは以下の通りです。主にghコマンドを使って必要な情報を取得していきます。
Sprintというフィールドから本日日付がどのスプリントにあたるかを取得- 取得したスプリントに設定されているissueの一覧を取得してJSONファイルに保存。取得するフィールドは以下
- タイトル
- ステータス
- start Date
- due Date
- 保存したJSONファイルのファイル名を出力
このスクリプトで本日日付の進捗状況をJSONファイルとして取得し、前営業日のJSONファイルと比較することで何がどう変わったのか?を定量的に評価します。
続いてJSONファイルの差分を分析するためのPythonスクリプトです。
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "deepdiff",
# "tabulate",
# ]
# ///
import argparse
import json
import sys
from deepdiff import DeepDiff
from tabulate import tabulate
def flatten_diff(diff):
rows = []
type_map = {
"values_changed": ("changed", True),
"dictionary_item_added": ("added", False),
"dictionary_item_removed":("removed", False),
"iterable_item_added": ("added", False),
"iterable_item_removed": ("removed", False),
"type_changes": ("type_changed", True),
}
for key, (label, has_old_new) in type_map.items():
items = diff.get(key, {})
for path, value in items.items():
if has_old_new:
rows.append([label, path, value.get("old_value", ""), value.get("new_value", "")])
elif "added" in label:
rows.append([label, path, "-", value])
else:
rows.append([label, path, value, "-"])
return rows
def main():
parser = argparse.ArgumentParser(description="JSON diff in table format")
parser.add_argument("file1", help="Old JSON file")
parser.add_argument("file2", help="New JSON file")
parser.add_argument("--format", default="grid",
choices=["grid", "github", "pipe", "tsv", "csv", "plain"],
help="Table format (default: grid)")
args = parser.parse_args()
with open(args.file1) as f:
old = json.load(f)
with open(args.file2) as f:
new = json.load(f)
diff = DeepDiff(old, new, verbose_level=2)
if not diff:
print("No differences found.")
sys.exit(0)
rows = flatten_diff(diff)
print(tabulate(rows, headers=["Type", "Path", "Old", "New"], tablefmt=args.format))
if __name__ == "__main__":
main()
差分を計算するためにDeepDiffというライブラリを利用しているのですが、冒頭のコメント部分でPEP 723に準拠した形で依存関係を定義しています。これにより、pip install等実行することなくuv runするだけで必要な依存関係が満たされた環境でスクリプトが実行できます。
GitHub Actionsのワークフロー作成
スキルが用意できたので、GitHub Actionsでスキルを定期実行するためのワークフローを作成します。
name: Project Daily Report
permissions:
id-token: write
contents: read
on:
workflow_dispatch:
schedule:
- cron: '0 1 * * 1-5' #JST 10:00に実行 ※15時以降にするとUTCとJSTで日付が変わってしまうので注意
jobs:
report:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
- uses: actions/setup-python@v6
with:
python-version: "3.11"
- name: Setup uv
uses: astral-sh/setup-uv@v7
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
aws-region: us-east-1
- name: Download previous report
run: |
aws s3 sync s3://${{ vars.REPORT_BUCKET_NAME }}/${{ github.repository_owner }} .claude/skills/check-pbi-progress/output/${{ github.repository_owner }}
- name: Run Claude Skill
id: claude
uses: anthropics/claude-code-action@v1
with:
use_bedrock: true
github_token: ${{ secrets.GITHUB_TOKEN }}
# show_full_output: true
prompt: |
/check-pbi-progress ${{ github.repository_owner }} ${{ vars.PROJECT_NUMBER }}
claude_args: |
--max-turns 20
--allowed-tools Read,Bash
--json-schema '{"type":"object","properties":{"result":{"type":"string"}},"required":["result"]}'
- name: Process results
run: echo "${RESULT}"
env:
RESULT: ${{ fromJSON(steps.claude.outputs.structured_output).result }}
- name: Prepare Slack message
id: slack_msg
run: |
{
echo 'text<<DELIM'
echo '```'
echo "$RESULT"
echo '```'
echo DELIM
} >> "$GITHUB_OUTPUT"
env:
RESULT: ${{ fromJSON(steps.claude.outputs.structured_output).result }}
- name: Send Report
uses: slackapi/slack-github-action@v3.0.1
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{"text": ${{ toJSON(steps.slack_msg.outputs.text) }}}
- name: Upload report
run: |
aws s3 sync .claude/skills/check-pbi-progress/output/${{ github.repository_owner }} s3://${{ vars.REPORT_BUCKET_NAME }}/${{ github.repository_owner }}
要約すると以下のステップを順に実行しています。
- ソースコードのチェックアウト、スクリプトやClaude Code環境のセットアップ
- S3から過去に生成した進捗状況のファイルを取得
- Claude Codeのスキルを実行
- レスポンスを整形してSlackで通知
- 生成した進捗状況のファイルをS3に保存
メインとなる処理はanthropics/claude-code-action@v1の呼び出しです。該当箇所だけ抜粋すると以下の通りです。
- name: Run Claude Skill
id: claude
uses: anthropics/claude-code-action@v1
with:
use_bedrock: true
github_token: ${{ secrets.GITHUB_TOKEN }}
# show_full_output: true
prompt: |
/check-pbi-progress ${{ github.repository_owner }} ${{ vars.PROJECT_NUMBER }}
claude_args: |
--max-turns 20
--allowed-tools Read,Bash
--json-schema '{"type":"object","properties":{"result":{"type":"string"}},"required":["result"]}'
今回は進捗状況を記録したJSONファイルをS3に保存することもあり、APIキー利用ではなくuse_bedrock: trueにしてBedrock経由でClaude Codeを利用するようにしています。claude_argsには--json-schemaを指定しており、これによって実行結果がJSON形式で取得できます。前述した通り後続のステップからsteps.claude.outputs.structured_outputで実行結果が参照できるようになります。
後続処理ではスキルの実行結果を整形してSlackで通知します。
- name: Prepare Slack message
id: slack_msg
run: |
{
echo 'text<<DELIM'
echo '```'
echo "$RESULT"
echo '```'
echo DELIM
} >> "$GITHUB_OUTPUT"
env:
RESULT: ${{ fromJSON(steps.claude.outputs.structured_output).result }}
- name: Send Report
uses: slackapi/slack-github-action@v3.0.1
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
{"text": ${{ toJSON(steps.slack_msg.outputs.text) }}}
スキルの実行結果には改行コードが含まれるため、処理を単純化するため一旦RESULTという環境変数に代入して利用します。
最終的にSlackに通知するメッセージのフォーマットについては色々悩んだんですが、最終的にはスキルの実行結果を単にバッククォートで括っただけの形式としました。細かく指示すればSlackのマークダウンに最適化した出力も得られそうですが、表形式で表示できないことなどを考慮するとこの形式が一番無難と判断しました。
AWSリソース、Slackアプリの準備
続いてワークフロー実行に必要になるAWSリソースとSlackアプリを準備します。
すでに多数のブログが存在するため、詳細な手順は割愛しますが。以下のリソースを準備してください。
- 進捗状況のファイルを保存するためのS3バケット
- GitHub Actionsから利用するIAMロール
- 以下の権限を付与
- Bedrock:InvokeModel
- Bedrock:InvokeModelWithResponseStream
- s3:PutObject
- s3:GetObject
- s3:ListBucket
- 以下の権限を付与
- 実行結果を通知するためのSlackアプリ&WebHook URL
GitHub Actions用のVariablesとSecretsの準備
先ほど作成したリソースの情報をGitHub ActionsのVariablesとSecretsに登録します。プライベートリポジトリの運用を前提として構築しているので、情報が漏洩してもそれ単体では事故に直結しないものはVariablesに登録しています。
- Secrets
SLACK_WEBHOOK_URL: SlackのWeb Hook URL
- Variables
AWS_ROLE_ARN: GitHub Actions用IAMロールのARNPROJECT_NUMBER: GitHub Projectsのプロジェクト番号REPORT_BUCKET_NAME: 進捗状況ファイル保存用のS3バケット名
テスト
準備が整ったので試しにワークフローを手動実行してみましょう。

無事に実行完了しました。
Slackの通知も確認してみます。

問題なく出力されています!!
まとめ
コードを書くだけでなくプロジェクト管理にもAIを活用してうまく省力化していきたいですね!







