Quartet / Quintet

2026-09-04  ·  githubactions · github · ci · claudecode

PRの動きに合わせてIssueのラベルを自動で張り替える

Issue に status:in-progressstatus:review のようなラベルを付けて進捗を追う運用、 やってみると必ず付け忘れます。

人間も忘れますし、AI にやらせても忘れます。実装が終わって PR を出した後、 Issue のラベルが in-progress のまま残っている。翌日見て「これ終わってたっけ」となる。

状態管理は機械に寄せるのが正解でした。実際に使っているワークフローを全文載せます。

やること

PR で起きたこと Issue のラベル
PR が作られた status:review
レビューで changes requested status:changes-requested
PR がマージされた status:done

PR 本文の Closes #<番号> から Issue を特定します。

全文

name: issue-status

on:
  pull_request:
    types: [opened, reopened, ready_for_review, closed]
  pull_request_review:
    types: [submitted]

permissions:
  contents: read
  issues: write
  pull-requests: read

concurrency:
  group: issue-status-${{ github.event.pull_request.number }}
  cancel-in-progress: false

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - name: Sync label on the linked issue
        env:
          GH_TOKEN: ${{ github.token }}
          REPO: ${{ github.repository }}
          PR_BODY: ${{ github.event.pull_request.body }}
          PR_MERGED: ${{ github.event.pull_request.merged }}
          ACTION: ${{ github.event.action }}
          REVIEW_STATE: ${{ github.event.review.state }}
        run: |
          set -euo pipefail

          issues="$(printf '%s' "${PR_BODY:-}" \
            | grep -oiE '(close[sd]?|fixe?[sd]?|resolve[sd]?)[[:space:]]*#[0-9]+' \
            | grep -oE '[0-9]+' \
            | sort -u || true)"

          if [ -z "$issues" ]; then
            echo "No linked issue in the PR body. Nothing to do."
            exit 0
          fi

          target=""
          if [ "$ACTION" = "closed" ]; then
            if [ "$PR_MERGED" = "true" ]; then target="status:done"; fi
          elif [ "$ACTION" = "submitted" ]; then
            case "${REVIEW_STATE:-}" in
              changes_requested) target="status:changes-requested" ;;
            esac
          else
            target="status:review"
          fi

          [ -z "$target" ] && { echo "No state change for this event"; exit 0; }

          all="status:planned status:in-progress status:review \
               status:changes-requested status:conflict status:done"

          for n in $issues; do
            echo "Issue #${n} -> ${target}"
            gh issue edit "$n" --repo "$REPO" --add-label "$target" >/dev/null
            for l in $all; do
              [ "$l" = "$target" ] && continue
              gh issue edit "$n" --repo "$REPO" --remove-label "$l" >/dev/null 2>&1 || true
            done
          done

ハマったところ

付いていないラベルを消そうとすると落ちる

gh issue edit --remove-label は、そのラベルが付いていないと失敗します。 status: を常に1つだけに保ちたいので全部消しにいくのですが、そのままだと 最初の1つで set -e に引っかかって死にます。

gh issue edit "$n" --remove-label "$l" >/dev/null 2>&1 || true

握りつぶします。ただし --add-label のほうは握りつぶしません。 そこが失敗したら本当に問題なので、落として気づけるようにします。

イベントが連続すると競合する

PR を作ってすぐレビューが付くと、2つのイベントがほぼ同時に走ります。 どちらも同じ Issue のラベルを触るので、順番によっては古い状態が後から上書きされます。

concurrency:
  group: issue-status-${{ github.event.pull_request.number }}
  cancel-in-progress: false

PR 番号ごとに直列化します。cancel-in-progress: false が重要で、 true にすると先行のラベル更新が途中でキャンセルされます。 待たせるのが正解です。

#45 のような素の参照を拾わない

「関連: #45」のような言及で状態を変えてしまうと事故ります。 Closes / Fixes / Resolves が前に付いているものだけを拾います。

実際に検証した結果です。

Closes #12                    -> [12]
Fixes #7 and Closes #9        -> [7 9]
Resolved #3                   -> [3]
この PR は #45 に関連します      -> []        ← 拾わない
issue #8 を見てください          -> []        ← 拾わない

権限は最小にする

既定の GITHUB_TOKEN は広い権限を持つので明示的に絞ります。 このワークフローに書き込みが要るのは Issue だけです。

fork からの PR では動かない

fork 元の GITHUB_TOKEN は読み取り専用になるため、外部コントリビューターの PR ではラベルが更新されません。個人プロジェクトや社内リポジトリなら問題に なりませんが、OSS で使うなら pull_request_target を検討することになります (ただしセキュリティ上の注意が別途必要です)。

ラベルを先に作る

ラベルが無いと --add-label が失敗するので、作成もスクリプトにしておきます。

labels=(
  "status:planned|ededed|Issue created, not started"
  "status:in-progress|1d76db|Implementation in progress"
  "status:review|fbca04|PR opened, waiting for review"
  "status:changes-requested|d93f0b|Changes requested"
  "status:conflict|b60205|Waiting for conflict resolution"
  "status:done|0e8a16|Merged and closed"
)

for entry in "${labels[@]}"; do
  IFS='|' read -r name color desc <<< "${entry}"
  gh label create "${name}" --color "${color}" --description "${desc}" 2>/dev/null \
    || gh label edit "${name}" --color "${color}" --description "${desc}"
done

作成に失敗したら編集にフォールバックするので、何度実行しても同じ結果になります。

なぜ機械に寄せるのか

これは、Claude Code に複数の人格を分担させて開発を回す構成の一部として使っています。 設計者・実装者・レビュアーと役割を分けると、それぞれにラベル操作までやらせたく なります。 しかし人格に任せると、必ず付け忘れます。

状態は機械が持ち、人格は「何をしたか」だけに集中させる。この分け方にしてから、 Issue の一覧が実態とずれなくなりました。

このワークフローを含む4人格ぶんの設定を MIT で公開しています。 コピーして ./setup.sh を叩けば動きます。


Claude Code に設計・実装・レビューを別々の人格として分担させ、GitHub Issue と ブランチを軸に並列開発を回すための設定一式を MIT で公開しています。 コピーして ./setup.sh を叩けば動きます。技術スタックには依存しません。

https://github.com/quintetkit/quartet

このワークフローだけで実際にツールを 1 つ作りました。Issue の分割から PR、 レビュー、マージまで記録が全部残っています。うまくいかなかった箇所も消していません。

https://github.com/quintetkit/mdlinkcheck

UI 設計人格・レビュー基準・Issue 単位の並列実行スクリプト・実践ガイド 10 章を 足したものは製品ページにあります。

この運用そのものを配っています

4 人格版 Quartet は MIT で無料公開しています。UI 設計人格・レビュー基準・ Issue 単位の並列実行スクリプト・実践ガイド 10 章を足した Quintet は有料版です。

無料版を見る 製品ページ