✦urchin_hat
URCHIN_HAT / TECHNICAL FIELD NOTES

TECH MEMOTECH MEMO

SRE、クラウド、AI、DevOps。
観測したこと、試したこと、考えたことを記録する技術アーカイブ。

ウニの帽子をかぶり、ノートに技術メモを書くキャラクター
OBSERVATION LOG 50 ENTRIES
# FIELD NOTES

TECH MEMO TOPICS

01 / RELIABILITY SRE &
RELIABILITY
02 / PLATFORM CLOUD &
PLATFORM
03 / AUTOMATION AI &
AUTOMATION
「gitでいいじゃん?」から始めるGitHub CLI入門 TECH MEMO / FIELD NOTE

「gitでいいじゃん?」から始めるGitHub CLI入門

はじめに:「gitでいいじゃん?」と思っていた頃

GitHubの「Code」ボタンには、「HTTPS」「SSH」と並んで「GitHub CLI」タブがあります。そこに表示されるのは gh repo clone <owner>/<repo> というコマンドです。

初めて見たときは、「クローンするだけなら git clone でいいじゃん?」と思いました。専用のCLIを入れる必要性が見えず、既存のGitコマンドと操作が重なって、かえって混乱するのではとも感じていました。

実際に調べて使ってみると、Gitと gh は競合せず、担当する操作が異なります。Gitがローカルのバージョン管理を担うのに対し、gh ではIssueの作成やPull Requestの確認など、それまでブラウザで行っていたGitHub上の操作をターミナルから実行できました。

AIアシスタントやAIエージェントと実装を進めたり、このブログを運用したりする中では、IssueやPull Requestを確認するたびにブラウザへ移るのが地味な手間です。その手間を減らせると分かって、ようやく gh を入れる理由が腹落ちしました。

この記事では、git switch と git restore の使い分けから、Gitと gh の役割、RHELの自宅サーバーで週報Issueを自動起票するまでを、私が試した順にまとめます。

まずはローカルを整理:git switch と git restore

GitHub CLIとの役割を分けるため、まずは手元で行うGit操作を整理します。

長くGitを使ってきたため、私はブランチの切り替えにも、作業ツリーの変更取り消しにも git checkout を使いがちでした。一つのコマンドがブランチ操作とファイルの復元の両方を担うため、実行前に何を対象にしているのかを確認する必要があります。

Git 2.23では、用途ごとに git switch と git restore が導入されました。ブランチを扱うのか、ファイルを復元するのかを、コマンド名から判別しやすくなります。

ブランチ操作に専念する git switch

ブランチの切り替えや新規作成には git switch を使います。

# 既存の main ブランチに切り替え
git switch main

# 新しいブランチを作成して即座に切り替え
git switch -c feature/weekly-issue-automation

私は「ブランチを移動する」という意図をコマンド名から確認できる点が分かりやすいと感じました。

ファイルの巻き戻しを行う git restore

作業ツリーやステージングエリア(インデックス)の変更を元に戻すには git restore を使います。

# ステージング(git add)した変更を取り消す(アンステージ)
git restore --staged path/to/file

# 作業ツリーの変更を破棄して最新のコミット状態に戻す
git restore path/to/file

git reset HEAD <file> や git checkout -- <file> と比べて、「状態を復元する」という用途を把握しやすいコマンドです。

GitとGitHub CLI(gh)の役割分担:なぜ競合しないのか?

当初の「新しいCLIを入れると、既存のGitコマンドと競合するのでは」という懸念は、2つのツールが何を操作するのかを整理すると解消しました。

gh はGitを置き換えるツールではなく、GitHubプラットフォームを操作するためのCLIです。gh repo clone も内部で標準の git clone を呼び出すため、既存のGitワークフローと併用できます。

ツール役割主な操作対象
Git分散バージョン管理コミット、ブランチ、タグ、リモート差分の追跡
GitHub CLI(gh)GitHubプラットフォームの操作Issue、Pull Request、Actions、Projects、Releases

私の用途では、これまでブラウザで行っていたIssueやPull Requestの操作をターミナルに戻せることが利点でした。

詳細な機能一覧や公式ガイドは、一次情報である GitHub CLI ドキュメント を参照してください。

「ぶっちゃけMCPでよくない?」という疑問に対する答え

ここで私が気になったのは、「GitHubの操作なら、AIエージェントにGitHub MCP(Model Context Protocol)サーバーをつなげば十分では?」という点です。

対話型セッションでAIに「このIssueを処理して」「PRを作成して」と依頼するなら、MCPサーバーが便利です。一方、今回のようにスケジューラから定期実行したい処理には、CLIである gh が合っていました。

  1. cronやスケジューラから実行できる: AIモデルを呼び出さず、シェルスクリプトとして無人実行できる。
  2. CI/CDでも利用できる: ローカルで試したシェルスクリプトやMakefileのコマンドを、GitHub Actionsなどからも実行できる。
  3. 人間も手元から操作できる: IssueやPull Requestの確認に、同じコマンドをそのまま使える。

今回は、対話しながらAIに作業を任せるときはMCP、定時に同じ処理を動かすときは gh と整理しました。

GitHub CLI の導入と初期設定

インストール

GitHub CLIは主要なOSやパッケージマネージャー向けに提供されています。各環境の手順は、公式リポジトリの Installation ガイド で確認できます。

私はmacOSのパッケージ管理にMacPortsを使っているため、port コマンドから導入しました。公式ドキュメントには、Homebrewや事前ビルドバイナリのほか、コミュニティサポートの方法としてMacPortsやCondaも案内されています。

# MacPorts(筆者の環境)
sudo port install gh

# Homebrew
brew install gh

自宅サーバーで稼働しているRHEL(Red Hat Enterprise Linux)環境では、公式のRPMリポジトリを追加して dnf でインストールします。DNF4とDNF5でリポジトリの追加方法が異なるため、dnf --version で自分の環境を確認してから実行します。

# DNF4
sudo dnf install 'dnf-command(config-manager)'
sudo dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo
sudo dnf install gh

# DNF5
sudo dnf install dnf5-plugins
sudo dnf config-manager addrepo --from-repofile=https://cli.github.com/packages/rpm/gh-cli.repo
sudo dnf install gh

GitHubアカウントとの認証

手元のmacOSでは、対話型の gh auth login を使いました。

gh auth login

画面の指示に従い、GitHub.com、プロトコル(HTTPS または SSH)、Login with a web browser を選択すると、ブラウザが立ち上がり認証が完了します。

認証状態は以下のコマンドで確認できます。

gh auth status

自宅サーバー(ヘッドレス環境)での認証とハマりどころ

今回の目的は、自宅サーバーからcronでIssueを自動起票することです。このサーバーにはSSHで接続しており、ブラウザを直接操作しないため、手元のmacOSとは認証の進め方が異なります。

認証方式の選択

自宅サーバーで gh auth login を実行した場合、主に2つの認証方法があります。

  1. Webブラウザ認証(Device Flow):
    ターミナルにワンタイムコード(例: ABCD-1234)が表示されるため、手元のPCやスマートフォンのブラウザで https://github.com/login/device を開いてコードを入力するだけで認証できます。
  2. トークン認証(Personal Access Token):
    GitHubのWeb画面でトークンを発行し、ターミナルに貼り付けます。

ハマりどころ:Fine-grained PAT と Classic PAT の違い

自宅サーバーでPersonal Access Token(PAT)を使って認証したとき、GitHubが推奨する Fine-grained personal access token では次のエラーに遭遇しました。

GraphQL: Could not resolve to a Repository with the name 'urchin-hat/urchin-hat'. (repository)

私の設定では、GitHub CLIが発行するGraphQL APIクエリに対して、Fine-grained PATのリポジトリスコープまたは権限が足りていませんでした。GitHub CLIの gh auth login マニュアルでも、Fine-grained PATはリソースごとのスコープによって注意が必要とされています。GitHub Projects(Projects v2)の操作にも必要な権限があるため、リポジトリの参照だけで認証を確認するのでは不十分です。

今回は、スクリプトからProjectsまで操作できることを優先し、Tokens (classic) を使うことで回避しました。gh auth login --with-token でClassic PATを渡す場合、GitHub CLIが求める最低限のスコープに、Projects操作用のスコープを加えます。

  • Note: homelab-weekly-issue
  • Scopes:
    • repo: プライベートリポジトリを含むリポジトリへのアクセス。GitHub CLIでIssueを参照・作成するために使う
    • read:org: ユーザーが所属するOrganizationとTeamの読み取り。gh auth login --with-token がClassic PATに求める
    • gist: Gistの作成・更新。今回のスクリプトでは使わないが、gh auth login --with-token がClassic PATに求める
    • project: GitHub Projectsの読み書き。gh issue create の --project で「ブログ記事進捗」へIssueを追加するために使う

Classic PATの repo と project は影響範囲の広い権限です。トークンには有効期限を設け、スクリプト本体やcron設定には書き込みません。認証情報の保管場所は gh auth status で確認します。セキュアな認証情報ストアを利用できない環境では、GitHub CLIがトークンをプレーンテキストのファイルへ保存するため、実行ユーザー以外から読まれないことも確認します。

日常でよく使う基本コマンド

私が日常的に使っているのは、Issue、Pull Request、GitHub Actionsを確認する次のコマンドです。これらを覚えてから、確認のたびにブラウザを開く回数が減りました。

Issueの確認

# 未解決のIssue一覧を表示
gh issue list

# 特定のIssueをターミナル上で閲覧
gh issue view 211

Pull Request の作成とチェックアウト

# 対話形式でPull Requestを作成
gh pr create

# タイトルと本文を指定して一発で作成
gh pr create --title "GitHub CLI導入メモ" --body "Issue #181 の対応です。"

# 他のメンバーのPRを手元にチェックアウトして検証
gh pr checkout 42

GitHub Actions の監視

プッシュした変更に対してCIが正常に完了したかを、ターミナル上でリアルタイムに監視できます。

# 直近のワークフロー実行状況を一覧表示
gh run list

# 一覧で確認したRun IDを指定してリアルタイムに監視
gh run watch <run-id>

実践:自宅サーバーから週報Issueを自動起票するcronスクリプト

背景:GitHub Projectsによる記事の進捗管理

このブログでは、記事の執筆から公開までの進捗をGitHub Projectsで管理しています。

記事ごとにIssueを起票し、本文には以下の定型チェックリストを設けています。

- [ ] バナー作成
- [ ] 検証
- [ ] 記事執筆
- [ ] 校正

起票したIssueは「ブログ記事進捗」Projectに追加し、「Todo」→「Writing」→「Done」とステータスを動かしています。

毎週土曜日に公開している週報は、毎回新しいIssueが必要です。タイトルの日付を入力し、チェックリストを貼り、ラベルとProjectを設定する。一回の操作は短くても、毎週繰り返す定型作業になっていました。

そこで、すでに定期稼働しているRHELの自宅サーバーから、cronで起票することにしました。

スクリプトの要件

  1. 自宅サーバーでリポジトリをcloneしたり、最新化したりしなくて済むよう、--repo で対象を指定する
  2. 実行日から週報を公開する土曜日を求め、YYYY/MM/DD 形式でタイトルに入れる
  3. 同じタイトルの未解決Issueがあれば、二重起票せずに終了する
  4. 定型チェックリスト、documentation ラベル、担当者、「ブログ記事進捗」Projectを起票時に設定する

実装したスクリプト

以下が実際に作成したスクリプトです(scripts/create-weekly-issue.sh)。

#!/usr/bin/env bash
set -euo pipefail

# 実行ユーザーの環境設定(cron用)
export HOME="${HOME:-/home/<username>}"
export PATH="/usr/local/bin:/usr/bin:/bin:${PATH:-}"
export TZ="Asia/Tokyo"

# 対象リポジトリ(リポジトリのcloneが不要)
REPO="<owner>/<repo>"

# 実行日以降で最初の土曜日を計算(Python3優先、なければdateコマンド)
if command -v python3 >/dev/null 2>&1; then
  TARGET_DATE=$(python3 -c 'import datetime; today=datetime.date.today(); sat=today + datetime.timedelta((5 - today.weekday()) % 7); print(sat.strftime("%Y/%m/%d"))')
elif date -v+sat >/dev/null 2>&1; then
  # macOS (BSD date)
  TARGET_DATE=$(date -v+sat +%Y/%m/%d)
else
  # Linux (GNU date)
  TARGET_DATE=$(date -d "saturday" +%Y/%m/%d)
fi

TITLE="週報(${TARGET_DATE})"

echo "[$(date '+%Y-%m-%d %H:%M:%S')] Starting weekly issue check for: ${TITLE}"

# 既存Issueの重複バリデーション(同名タイトルのOPENなIssueが存在すればスキップ)
EXISTING_ISSUE=$(gh issue list \
  --repo "${REPO}" \
  --state open \
  --limit 1000 \
  --json number,title \
  --jq ".[] | select(.title == \"${TITLE}\") | .number")

if [[ -n "${EXISTING_ISSUE}" ]]; then
  echo "[$(date '+%Y-%m-%d %H:%M:%S')] Issue already exists: #${EXISTING_ISSUE} (${TITLE}). Skipping."
  exit 0
fi

# 定型チェックリスト本文
BODY=$(cat <<'EOF'
- [ ] バナー作成
- [ ] 検証
- [ ] 記事執筆
- [ ] 校正
EOF
)

# Issue作成
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Creating issue: ${TITLE}..."
ISSUE_URL=$(gh issue create \
  --repo "${REPO}" \
  --title "${TITLE}" \
  --body "${BODY}" \
  --assignee "@me" \
  --label "documentation" \
  --project "ブログ記事進捗")

echo "[$(date '+%Y-%m-%d %H:%M:%S')] Issue created successfully: ${ISSUE_URL}"

スクリプトのポイント

1. 内蔵 --jq による重複チェック

GitHubの --search は検索インデックスの更新に時間がかかることがあります。そこで gh issue list から未解決Issueを取得し、gh に組み込まれた --jq でタイトルが完全に一致するかを判定しました。gh issue list はデフォルトでは30件しか取得しないため、ここでは --limit 1000 を指定しています。未解決Issueが1000件を超えるリポジトリで使う場合は、この上限も見直す必要があります。また、自宅サーバーへ別途 jq を入れる必要はありません。

2. cron実行時の環境変数

cronから実行すると、対話シェルと同じ PATH や HOME を前提にできません。そこでスクリプト内に HOME と PATH を記載し、GitHub CLIが実行ユーザーの認証情報を参照できるようにしました。set -u で PATH が未定義でもスクリプトが停止しないよう、展開には ${PATH:-} を使っています。さらに TZ=Asia/Tokyo を指定し、Issueの日付とログの時刻をJSTで計算します。

実行結果の検証

同じタイトルのIssueがすでにある状態で、自宅サーバーからテストしました。結果は次のとおりで、Issueは新しく作られず、意図どおりスキップされました。

(redhat)[user@homelab ~]$ /path/to/scripts/create-weekly-issue.sh
[2026-09-18 13:10:07] Starting weekly issue check for: 週報(2026/09/19)
[2026-09-18 13:10:08] Issue already exists: #211 (週報(2026/09/19)). Skipping.

cronの設定:なぜ /etc/cron.weekly/ ではなく crontab -e なのか

定期実行には /etc/cron.weekly/ ではなく、実行ユーザー自身の crontab -e を選びました。時刻と認証情報の扱いを自分で揃えられるからです。

  1. 実行時刻: この環境の /etc/cron.weekly/ は anacron で管理され、ランダムな遅延も入ります。「毎週日曜日の12:00(JST)」という時刻は、ユーザーのcrontabのほうが指定しやすいです。
  2. 実行ユーザー: /etc/cron.weekly/ は root で実行されるため、一般ユーザーとして認証したGitHub CLIの認証情報はそのまま利用できません。

自宅サーバーで実行対象ユーザーの crontab を開きます。

crontab -e

以下を登録します。

# 毎週日曜日 12:00 (JST) に週報Issueを自動作成
CRON_TZ=Asia/Tokyo
0 12 * * 0 /path/to/scripts/create-weekly-issue.sh >> /path/to/logs/weekly-issue.log 2>&1

CRON_TZ=Asia/Tokyo はcronの起動時刻をJSTとし、スクリプト内の TZ=Asia/Tokyo は日付計算とログの時刻をJSTに揃えます。日曜日に実行すると、スクリプトが6日後の土曜日を求め、次回の週報Issueを事前に起票します。

おわりに

最初は「git clone で十分」と思っていましたが、実際に導入してみると、gh が担うのはGitとは別の層でした。ローカルのバージョン管理はGit、IssueやPull RequestなどGitHub上の操作は gh と分けると、両方を無理なく併用できます。

とくに私の環境で役立ったのは、毎週のIssue起票をRHELの自宅サーバーへ任せられたことです。今回確認できたのは、既存Issueがある場合に重複起票せず終了するところまでです。

まずは自動起票から始めましたが、gh の使いどころは定期実行だけではありません。これからは、Issueの確認やPull Requestの作成、GitHub Actionsの監視など、自分が日常的に行うターミナル操作でも活用していきます。

@urchin_hat
WRITTEN BY@urchin_hat

とある領域のSaaSサービスのSREとして、プロダクトの信頼性と事業成長を支えています。

← BACK TO MEMO