CursorとGitHub Issueの相性の良さを実感した開発効率化の取り組み

エンジニアの関口です。去年からAI推進の活動を日々行いつつ、開発の現場でもAIを活用する場面が増えてきました。

私のチームでは、GitHub IssuesGitHub Projectsを使った形で開発のタスクを管理しています。AIとの親和性が高いので、今回紹介します。

チームではCursorを使った開発がメインであり、その中でCursorのcommands機能を活用して、Issueの作成からブランチ作成、PR作成などを自動処理しています。これにより、デイリーでの起票のスピードが圧倒的に速まりました。たわいもない会話から、すぐに起票するスピード感や、中身のあるIssueが作れるのはありがたいところです。

今のチームに限らずプロジェクトを進めるときにタイトルだけのチケットを作ることが多かったです。 AIに起票をお願いすることで、テンプレートに沿って情報を整理する必要があるため、タスクの内容を明確に言語化でき、タスクに対する理解も高まる効果がありました。

※チケット:イシュー管理システムでのタスクのこと

どんなことをしたのか

GitHub CLIghコマンド)とCursorのcommands機能を組み合わせることで、開発フローを自動化しました。具体的には、以下の3つの作業を自動化しています。

今回紹介するのは、チームでも利用頻度が高いcommandsを紹介します。

Cursorで操作させるためには、まずghをインストールして利用できる状態にしておくことが必須です。インストール方法については、GitHub CLIの公式ドキュメントを参照してください。

  • ブランチ作成は、GitHub Issuesのリンクから自動的にブランチ名を生成し、ブランチを作成します
  • PR作成は、ブランチ名からIssue番号を推測し、テンプレートを使ってPRを作成してIssueと自動的に紐づけます
  • Issue作成は、テンプレートからIssueを作成し、GitHub Projectsに自動的に追加して、現在のスプリントも自動適用します

それぞれの自動化について、詳しく紹介していきます。

ブランチ作成を自動化してみた

新しい機能を実装するとき、チケット番号からブランチ名を決めるのが面倒でした。毎回「あれ、ブランチ名って何だったっけ」と確認するのが手間でした。

そこで、チケット番号から自動的にブランチ名を生成するようにしました。例えば、チケット番号が222なら、features/222というブランチを作る感じです。

具体的には、以下の手順でブランチを作成します。

git コマンドでブランチを作成して

## 手順
1. `git fetch origin main:main`からメインを更新
2. チケット番号からブランチ名を決める
    - `features/{{チケット番号}}`
    - 例: `features/222`
3. `git switch -c {{branch_name}} main`でブランチを作成 & 移動

ブランチ名作成は、実際のGitHubで立てたIssueのリンクを貼り付けて、/make-branchという形で投げ入れます。

/make-branch 
https://github.com/{owner}/{repo}/issues/{issue_number} 

これをスクリプト化したことで、ブランチ名のミスが減り、作業時間も短縮できました。

PR作成も自動化してみた

PRを作るたびに、テンプレートをコピペして、Issue番号を探して、タイトルを考えて...という作業が続いていました。特に、IssueとPRを紐づけるのを忘れることが多々ありました。

gh pr createコマンドを使うと、これが一気に楽になりました。ブランチ名からIssue番号を推測して、テンプレートを読み込んで、自動的にIssueと紐づけてくれます。

例えば、features/546というブランチからPRを作るときは、Issue番号546を自動的に推測して、PR本文にCloses #546を入れてくれます。PRがマージされると、Issueも自動的にクローズされるので、手動でクローズする手間がなくなりました。

PR本文は一時ファイル(.pr_body_temp.md)に保存して、--body-fileオプションで渡しています。長い--bodyを直接渡すとエディタが開いてしまうことがあるので、ファイル経由の方が確実です。

`gh pr create --base main`コマンドを使用して、プルリクエストを作成する

## 前提
- 未commitがある場合は、stashするか停止するかを聞いてください
- 作業ブランチが push 済みか確認
    - 未対応ならpush
- `.github/pull_request_template.md` をテンプレートとする
- 差分やコミットログを確認し、プルリクエストの本文、タイトルを作成する
    - ブランチ名に含まれる数字からIssue番号を推測する
        - 例: `features/546` → `546`
        - Issue番号はブランチ名の `features/` や `hotfix/` の後の数字部分
    - PRタイトルはブランチ名の接頭辞(`features` → `feat:`, `hotfix` → `hotfix:`)を使用し、日本語で記述する
    - PR本文は一時ファイル(`.pr_body_temp.md`)を作成し、`--body-file` オプションを使用する
        - 長い `--body` を直接渡すとエディタが開く可能性があるため、`--body-file` を使用する
        - `.github/pull_request_template.md` を読み込み、`{Issue番号}` を実際のIssue番号に置換する
        - `Closes #{Issue番号}` の形式でIssueと紐づける(PR本文に含めることで自動的に紐づけられる)
        - 例: `Closes #546`
- PR作成時は以下のオプションを指定する
    - `--assignee @me`: 自分自身をアサイン
    - 例: `gh pr create --base main --assignee @me --title "feat: ..." --body-file .pr_body_temp.md`
- PR作成後、一時ファイル(`.pr_body_temp.md`)を削除する
- PR作成の確認は `gh pr list --head {ブランチ名}` でおこなう
    - コマンドの実行は時間がかかる場合があるので、確認前に2-3秒待機する

## Issueとの紐づけについて
- GitHub CLIには `--issue` オプションは存在しません
- PR本文に `Closes #{Issue番号}` または `Fixes #{Issue番号}` を含めることで、Issueと自動的に紐づけられます
- PRがマージされると、紐づけられたIssueが自動的にクローズされます

Issue作成も自動化してみた

Issueを作るときも、毎回同じような内容を書くのが面倒でした。特に、GitHub Projectsに手動で追加するのを忘れがちでした。そのため、プロジェクト管理がうまくいかないこともありました。

gh issue createコマンドを使うと、テンプレートからIssueを作成して、自動的にGitHub Projectsに追加してくれます。バグ報告ならbug_report.md、機能追加ならfeature_request.mdという感じで、テンプレートを選ぶだけで、必要な情報が揃います。

実際にissueを作成したチャットのキャプチャ

テンプレートのフロントマター(---で囲まれた部分)は除外して、本文だけを使うようにしています。GitHub CLIではフロントマターが使えないので、本文だけを一時ファイルに保存してから、Issueを作成します。

Issueを作成した後は、GraphQL APIを使ってGitHub Projectsに自動的に追加します。Organizationプロジェクトが存在しない場合は、Userプロジェクトとして試行するようにしているので、どちらの場合でも動作します。

`gh issue create`コマンドを使用して、GitHub Issuesを作成する

## 前提
- `.github/ISSUE_TEMPLATE/` 配下のテンプレートファイルを使用する
- 利用可能なテンプレート:
  - `issue_template.md`: 汎用Issueテンプレート
  - `feature_request.md`: 機能追加提案
  - `bug_report.md`: バグ報告
- GitHub Projects(プロジェクト番号: {project_number})と自動的に紐づける
- 現在のカレントスプリントを自動的に適用する

## 手順
1. ユーザーにIssueの種類を確認する
   - 汎用Issue: `issue_template.md`
   - 機能追加: `feature_request.md`
   - バグ報告: `bug_report.md`
2. 選択されたテンプレートファイルを読み込む
3. テンプレートの内容を一時ファイル(`.issue_body_temp.md`)に保存
   - フロントマター(`---`で囲まれた部分)は除外して本文のみを保存
4. ユーザーにタイトルを確認する
   - テンプレートの`title`フィールドを参考にする
   - 例: `[Feature] タイトル`、`[Bug] タイトル`、`[カテゴリ] タイトル`
5. ラベルを確認する
   - テンプレートの`labels`フィールドを確認
   - 必要に応じて追加のラベルを提案
6. Issueを作成する
   - `gh issue create --title "{タイトル}" --body-file .issue_body_temp.md --label "{ラベル}"`
   - ラベルが複数の場合は `--label` を複数回指定
   - 例: `gh issue create --title "[カテゴリ] タイトル" --body-file .issue_body_temp.md --label "label1"`
7. Issue作成後、作成されたIssue番号を取得する
   - `gh issue list --limit 1 --json number --jq '.[0].number'` で最新のIssue番号を取得
8. GitHub Projects(プロジェクト番号: {project_number})にIssueを追加する
   - Issueのnode IDを取得: `gh issue view {ISSUE_NUMBER} --json id --jq '.id'`
     - GraphQL APIではなく、`gh issue view`コマンドを使用する(シンプルで確実)
   - プロジェクトのnode IDを取得: `gh project view {project_number} --owner {owner} --format json --jq '.id'`
     - GraphQL APIではなく、`gh project view`コマンドを使用する(シンプルで確実)
   - Issueをプロジェクトに追加: `gh api graphql -f query='mutation($projectId: ID!, $contentId: ID!) { addProjectV2ItemById(input: { projectId: $projectId contentId: $contentId }) { item { id } } }' -f projectId={PROJECT_NODE_ID} -f contentId={ISSUE_NODE_ID}`
   - プロジェクトアイテムIDを取得: 上記のmutationのレスポンスから `item.id` を取得する
   - プロジェクトがOrganizationに存在しない場合は、Userプロジェクトとして試行する
9. 現在のカレントスプリントを適用する
   - `./scripts/apply-current-sprint.sh {PROJECT_ITEM_ID}` を実行して、現在のカレントスプリントをプロジェクトアイテムに適用する
   - スクリプトの実行に失敗した場合でも、Issue作成とプロジェクトへの追加は成功しているため、エラーを無視して続行する
10. Issue作成後、一時ファイル(`.issue_body_temp.md`)を削除する
11. Issue作成の確認は `gh issue list --limit 1` でおこなう
    - コマンドの実行は時間がかかる場合があるので、確認前に2-3秒待機する

## 注意事項
- テンプレートのフロントマター(`---`で囲まれた部分)はGitHub CLIでは使用されないため、本文のみを保存する
- タイトルはテンプレートの`title`フィールドを参考にするが、ユーザーに確認してから使用する
- ラベルが空の場合は `--label` オプションを指定しない
- プロジェクトへの追加に失敗した場合でも、Issue作成は成功しているため、エラーを無視して続行する
- カレントスプリントの適用に失敗した場合でも、Issue作成とプロジェクトへの追加は成功しているため、エラーを無視して続行する

apply-current-sprint.shを使うことで、プロジェクトの紐づけから設定しているスプリントの紐づけも自動で行うようにシェルスクリプトを用意しています。

#!/bin/bash
set -euo pipefail

# 引数の検証
if [ $# -lt 1 ] || [ $# -gt 2 ]; then
  echo "Usage: $0 <project_item_id> [--dry-run]"
  echo "Example: $0 PVTI_lAHOBNYczs4BJxudzg52VlY"
  echo "  This will apply the current sprint to the specified project item"
  echo ""
  echo "Options:"
  echo "  --dry-run    Validate arguments and fetch data, but do not actually apply sprint"
  exit 1
fi

project_item_id=$1
dry_run=false

# 第2引数が--dry-runの場合は検証モード
if [ $# -eq 2 ] && [ "$2" = "--dry-run" ]; then
  dry_run=true
  echo "=== DRY RUN MODE: Validation only, no actual changes will be made ==="
  echo ""
fi

# スクリプトのディレクトリに移動
cd "$(dirname "$0")"

# プロジェクト設定
owner="{owner}"
project_number={project_number}
sprint_field_name="Iteration"

# プロジェクトIDを取得
project_id=$(
  gh project view "${project_number}" \
  --owner "${owner}" \
  --format json \
  --jq '.id'
)

echo "Project ID: ${project_id}"
echo "Project Item ID: ${project_item_id}"
echo ""

# すべてのスプリント(Iteration)を取得
all_iterations=$(
  gh api graphql \
  --field project_id="${project_id}" \
  -f query='
    query($project_id: ID!){
    node(id: $project_id) {
      ... on ProjectV2 {
        field(name: "Iteration") {
          ... on ProjectV2IterationField {
            id
            name
            configuration {
              iterations {
                startDate
                id
                title
                duration
              }
            }
          }
        }
      }
    }
  }' \
  --jq '.data.node.field.configuration.iterations'
)

if [ -z "${all_iterations}" ] || [ "${all_iterations}" = "null" ] || [ "${all_iterations}" = "[]" ]; then
  echo "Warning: No iterations found. Skipping sprint assignment."
  exit 0
fi

# すべてのスプリントを表示(デバッグ用)
echo "Available iterations:"
echo "${all_iterations}" | jq -r '.[] | "  - \(.title) (Start: \(.startDate | split("T")[0]), Duration: \(.duration) days)"'
echo ""

# 現在の日付を取得(YYYY-MM-DD形式)
current_date=$(date +%Y-%m-%d)
echo "Current date: ${current_date}"
echo ""

# 現在の日付が含まれるIterationを判定
# startDateからduration日後までの範囲に現在の日付が含まれるかを確認
# jqで日付比較を行う(YYYY-MM-DD形式の文字列比較で十分)
current_iteration=$(
  echo "${all_iterations}" | jq --arg current_date "${current_date}" '
    .[] | 
    select(
      (.startDate | split("T")[0]) <= $current_date
    )
  ' | jq -s 'sort_by(.startDate) | reverse | .[0]'
)

if [ -z "${current_iteration}" ] || [ "${current_iteration}" = "null" ]; then
  echo "Warning: No current sprint found for date ${current_date}. Skipping sprint assignment."
  exit 0
fi

# 選択されたIterationの終了日を計算して、現在の日付が範囲内か確認
start_date_str=$(echo "${current_iteration}" | jq -r '.startDate | split("T")[0]')
duration=$(echo "${current_iteration}" | jq -r '.duration')

# 終了日を計算(startDate + duration日)
# LinuxとmacOSの両方に対応
if date -d "${start_date_str} +${duration} days" +%Y-%m-%d >/dev/null 2>&1; then
  # Linux (GNU date)
  end_date=$(date -d "${start_date_str} +${duration} days" +%Y-%m-%d)
elif date -v+${duration}d -j -f "%Y-%m-%d" "${start_date_str}" +%Y-%m-%d >/dev/null 2>&1; then
  # macOS (BSD date)
  end_date=$(date -v+${duration}d -j -f "%Y-%m-%d" "${start_date_str}" +%Y-%m-%d)
else
  # フォールバック: Pythonを使用
  end_date=$(python3 -c "
from datetime import datetime, timedelta
start = datetime.strptime('${start_date_str}', '%Y-%m-%d')
end = start + timedelta(days=${duration})
print(end.strftime('%Y-%m-%d'))
" 2>/dev/null || echo "")
fi

if [ -z "${end_date}" ]; then
  echo "Warning: Could not calculate end date for sprint. Skipping sprint assignment."
  exit 0
fi

# 現在の日付が範囲内か確認
if [ "${current_date}" \< "${start_date_str}" ] || [ "${current_date}" \> "${end_date}" ]; then
  echo "Warning: Current date ${current_date} is not within sprint range (${start_date_str} to ${end_date}). Skipping sprint assignment."
  exit 0
fi

current_iteration_id=$(echo "${current_iteration}" | jq -r '.id')
current_iteration_title=$(echo "${current_iteration}" | jq -r '.title')

echo "=== Current Sprint Found ==="
echo "Sprint: ${current_iteration_title}"
echo "ID: ${current_iteration_id}"
echo "Start Date: ${start_date_str}"
echo "End Date: ${end_date}"
echo "Duration: ${duration} days"
echo ""

# スプリントフィールドIDを取得
sprint_field_id=$(
  gh project field-list "${project_number}" \
  --owner "${owner}" \
  --format json \
  --jq "
      .fields[]
      | select(
        .name==\"${sprint_field_name}\"
         and .type==\"ProjectV2IterationField\"
         )
      | .id
    "
)

if [ -z "${sprint_field_id}" ]; then
  echo "Error: Sprint field not found"
  exit 1
fi

echo "Sprint Field ID: ${sprint_field_id}"
echo ""

# dry-runモードの場合はここで終了
if [ "${dry_run}" = "true" ]; then
  echo "=== DRY RUN MODE: Validation completed successfully ==="
  echo "Would apply sprint '${current_iteration_title}' (ID: ${current_iteration_id}) to project item ${project_item_id}"
  echo ""
  echo "To actually apply the sprint, run without --dry-run option:"
  echo "  $0 ${project_item_id}"
  exit 0
fi

# カレントスプリントをプロジェクトアイテムに適用
echo "Applying current sprint to project item..."
gh project item-edit \
  --id "${project_item_id}" \
  --field-id "${sprint_field_id}" \
  --project-id "${project_id}" \
  --iteration-id "${current_iteration_id}"

echo ""
echo "Successfully applied current sprint (${current_iteration_title}) to project item ${project_item_id}"

実際に使ってみて

これらの自動化を導入してから、開発フローがかなりスムーズになりました。特に、IssueとPRの紐づけを忘れることがなくなったのは大きいです。以前は、PRがマージされたのにIssueが開いたままになっていることがよくありましたが、今は自動的にクローズされるので、プロジェクト管理が楽になりました。

GitHub Projectsへの自動追加も便利です。以前は、Issueを作った後に手動でプロジェクトに追加するのを忘れることがありましたが、今は自動的に追加されるので、プロジェクトの可視性が向上しました。

まとめ

GitHub CLIを使った自動化により、ブランチ作成、PR作成、Issue作成の作業が楽になりました。特に、IssueとPRの自動紐づけや、GitHub Projectsへの自動追加により、手動作業によるミスが減りました。プロジェクト管理の品質も向上しました。

まだ改善の余地はありますが、開発フローがかなり効率化できたので、今後も継続的に改善していきたいです。

弥生では一緒に働く仲間を募集しています。
www.yayoi-kk.co.jp
弥生のエンジニアに関するnote記事もご覧ください。
note.yayoi-kk.co.jp