Gitリポジトリをシェルスクリプトから操作する設計|自動pull・差分検知・タグ付与を安全に実装する方法

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)Linuxtips, シェルスクリプト > Gitリポジトリをシェルスクリプトから操作する設計|自動pull・差分検知・タグ付与を安全に実装する方法
「シェルスクリプトからgit pullを実行したら認証エラーが出て止まってしまう」
「cronで定期的にGitリポジトリを自動更新したいが、SSH鍵が読めなくてgitコマンドが失敗する」

こういった悩みは、自動化の仕組みを作り始めた頃に必ずぶつかる壁です。原因は、シェルスクリプトが動く環境(cronやsystemdのサービス)はログインシェルと全く別物であり、SSH_AUTH_SOCKPATHなどの環境変数が一切引き継がれないことにあります。

この記事では、シェルスクリプトからGitリポジトリを安全に操作するための設計パターンを解説します。SSH認証の環境変数設定、git status --porcelainによる機械可読な差分検知、git pull時の競合回避設計、デプロイ完了をバージョンタグで記録する方法まで、RHEL 9.4 / Ubuntu 24.04 LTSの実機で動作確認したコードで順を追って説明します。

この記事のポイント

・cronやsystemdではSSH_AUTH_SOCKがないためGIT_SSH_COMMANDで鍵ファイルを明示する
・git status --porcelainはスクリプトで変更の有無を判定するのに最適な出力形式
・git pull --rebaseとgit stashを組み合わせてローカル変更がある状態でも安全に同期する
・git tag -aで注釈付きタグを自動付与し、デプロイ記録をリポジトリ履歴に残す


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

シェルスクリプトでGitを扱う際の基本設計

Gitコマンドをシェルスクリプトから呼び出すとき、まず押さえておくべき設計の前提があります。

exit codeを必ず確認する
gitコマンドはすべて、成功時に0、失敗時に非0のexit codeを返します。set -e(またはset -euo pipefail)を先頭に書いておくと、git操作が失敗した瞬間にスクリプトを自動停止できます。停止と同時にログに記録するにはtrapと組み合わせます。

-Cオプションでリポジトリを指定する
git -C /path/to/repo statusのように-Cオプションでディレクトリを指定すると、cdを使わずにリポジトリを操作できます。cdを使うとサブシェルの外に戻れないバグが起きやすいため、-Cを使う設計が安全です。

基本的なスクリプトの骨格は次のようになります。

#!/bin/bash set -euo pipefail # ログファイルへ全出力をリダイレクト(cronからの実行時に記録が残る) LOG_FILE="/var/log/git-deploy.log" exec >> "${LOG_FILE}" 2>&1 # タイムスタンプ付きログ関数 timestamp() { date '+%Y-%m-%d %H:%M:%S'; } log() { echo "[$(timestamp)] $*"; } REPO_DIR="/srv/myapp" # 終了時に常に実行されるクリーンアップ cleanup() { local exit_code=$? if [ "${exit_code}" -ne 0 ]; then log "[ERROR] スクリプト異常終了: exit code=${exit_code}" fi } trap cleanup EXIT log "[INFO] デプロイスクリプト開始"

SSH認証設計|GIT_SSH_COMMANDでcronでもgitを通す

1. なぜcronからgitが失敗するのか

ログインシェルではssh-agentが起動しており、SSH_AUTH_SOCKという環境変数が鍵の場所を指しています。しかしcronのジョブはログインシェルを経由せずに起動するため、SSH_AUTH_SOCKが存在しません。gitがSSH鍵を探せず認証に失敗するのはこれが原因です。

また、GIT_AUTHOR_EMAILなどのGit設定も~/.gitconfigが存在しない場合は読み込まれません。cronではホームディレクトリの扱いも通常と異なるため、必要な情報はすべてスクリプト内で明示して渡す設計が必要です。

2. GIT_SSH_COMMANDで鍵ファイルを指定する

GIT_SSH_COMMAND環境変数にSSHコマンドを書くと、gitがSSH通信するたびにこのコマンドを使います。鍵ファイルを-iで明示し、-o BatchMode=yesでパスワード入力を求めないように設定します。

# デプロイ専用の鍵を使う(root鍵や個人鍵は使わない) export GIT_SSH_COMMAND="ssh -i /home/deploy/.ssh/id_ed25519 \ -o StrictHostKeyChecking=no \ -o BatchMode=yes" # git操作の前にテストしておくと安心 git -C "${REPO_DIR}" ls-remote --heads origin

StrictHostKeyChecking=noは初回接続時の「known_hostsに追加しますか?」プロンプトを省略します。プロンプトが出るとcronのジョブが止まるため、自動化では必須の設定です。なお、セキュリティを重視する環境では事前にknown_hostsを登録しておき、StrictHostKeyChecking=accept-new(初回のみ追加)に変更するとより安全です。

3. cronからの実行で動作確認する

GIT_SSH_COMMANDを設定してからgit ls-remoteでリモートに接続できるか確認します。

# cron環境を模したコマンド(環境変数を最小限にして実行) $ env -i HOME=/home/deploy \ GIT_SSH_COMMAND="ssh -i /home/deploy/.ssh/id_ed25519 -o BatchMode=yes -o StrictHostKeyChecking=no" \ git -C /srv/myapp ls-remote --heads origin # 成功例(ブランチ一覧が返る) a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 refs/heads/main 7f8e9d0c1b2a7f8e9d0c1b2a7f8e9d0c1b2a7f8e refs/heads/develop

差分検知設計|git status --porcelainで変更の有無を判定する

1. git statusの出力形式を比較する

git statusは人間向けの出力形式で、スクリプトで解析するには不便です。--porcelainオプションを使うと機械可読な形式になります。

# 通常のgit status(人間向け・スクリプト解析には向かない) $ git status On branch main Changes not staged for commit: (use "git add ..." to update what will be committed) modified: src/app.py # --porcelain(機械可読・XY形式で変更種別を2文字で表す) $ git status --porcelain M src/app.py ?? tmp/debug.log # XY形式の意味(先頭2文字) # M = インデックス変更済み # M = 作業ツリー変更済み # ?? = 追跡外ファイル # A = 新規追加(ステージ済み)

2. 変更ファイル数をカウントして条件分岐する

--porcelainの出力は変更ファイルが1行1件なので、wc -lでカウントできます。

# 変更の有無を確認する関数 has_local_changes() { local repo_dir="$1" git -C "${repo_dir}" status --porcelain | grep -q . } # 使い方 if has_local_changes "${REPO_DIR}"; then CHANGED_COUNT=$(git -C "${REPO_DIR}" status --porcelain | wc -l) log "[WARN] ローカルに未コミットの変更が ${CHANGED_COUNT} 件あります" fi

3. git diff --statで変更内容をログに記録する

pullの前後にgit diff --statを使うと、どのファイルが何行変わったかをログに残せます。デプロイ後の変更範囲の把握に役立ちます。

# pull前のコミットIDを記録 BEFORE_HASH=$(git -C "${REPO_DIR}" rev-parse HEAD) # pull実行(詳細は次節) git -C "${REPO_DIR}" pull --rebase origin main # pull後のコミットIDと変更統計を記録 AFTER_HASH=$(git -C "${REPO_DIR}" rev-parse HEAD) if [ "${BEFORE_HASH}" != "${AFTER_HASH}" ]; then log "[INFO] 変更内容:" git -C "${REPO_DIR}" diff --stat "${BEFORE_HASH}" "${AFTER_HASH}" | while read -r line; do log " ${line}" done else log "[INFO] 変更なし(リモートと同期済み)" fi # 実際のlog出力例: # [2026-08-28 14:30:12] [INFO] 変更内容: # [2026-08-28 14:30:12] src/app.py | 8 ++++++-- # [2026-08-28 14:30:12] conf/nginx.conf | 2 +- # [2026-08-28 14:30:12] 2 files changed, 7 insertions(+), 3 deletions(-)

自動pull設計|競合を防いで安全にリモートと同期する

1. --rebaseでマージコミットを作らない

git pullはデフォルトではマージを行い、「Merge branch 'main' of ...」というコミットが自動で作られます。自動化の文脈では履歴が汚れ、後でgit logを追いにくくなります。--rebaseを付けると、ローカルの変更をリモートの変更の上に積み直すため、余分なマージコミットが作られません。

# マージコミットを作らずpull git -C "${REPO_DIR}" pull --rebase origin main

2. git stashでローカル変更を退避してからpullする

デプロイ先に設定ファイルなど手動で変更したファイルがある場合、そのままpullすると競合が起きます。git stashで一時退避してからpullし、完了後に復元する関数を用意しておくと安全です。

pull_safe() { local repo_dir="$1" local remote="${2:-origin}" local branch="${3:-main}" local stashed=0 # ローカル変更があればstashする if has_local_changes "${repo_dir}"; then log "[INFO] ローカル変更をstashします" git -C "${repo_dir}" stash push -m "auto-stash $(date '+%Y%m%d-%H%M%S')" stashed=1 fi # pullを実行し、失敗時はstashを復元してexit if ! git -C "${repo_dir}" pull --rebase "${remote}" "${branch}"; then log "[ERROR] pull失敗。stashを復元します" [ "${stashed}" -eq 1 ] && git -C "${repo_dir}" stash pop || true return 1 fi # pullが成功したらstashを復元 if [ "${stashed}" -eq 1 ]; then log "[INFO] stashを復元します" git -C "${repo_dir}" stash pop fi log "[INFO] pullが完了しました" }

3. pull失敗時のエラー処理

set -eを使っている場合、pull失敗でスクリプト全体が即停止します。これは安全側に倒れた動作ですが、cronジョブで停止のたびに通知が飛ぶのが煩わしい場合はif ! command; then ... fiパターンで自前のエラー処理を書きます。

# pull失敗を握りつぶさずに原因をログに残す例 if ! pull_safe "${REPO_DIR}" origin main; then log "[ERROR] git pullに失敗しました。次の実行まで待機します" # 通知が必要な場合はここでcurl/mailコマンドを呼ぶ exit 1 fi

シェルスクリプトでこうした設計パターンを体系的に学びたい方には、シェルスクリプト実践講座も参考にしてください。

バージョンタグ付与設計|デプロイ完了をgit tagで記録する

デプロイが成功したタイミングでgitタグを打っておくと、「いつどのバージョンをデプロイしたか」をリポジトリの履歴から追えるようになります。障害発生時に「どのデプロイ以降に問題が起きたか」をすぐ特定できるため、本番運用では特に役立ちます。

1. 軽量タグと注釈付きタグの使い分け

軽量タグ(git tag v1.0.0):特定コミットへの単純なポインタ。コミットIDと同義で、メッセージや作成者の情報を持たない
注釈付きタグ(git tag -a v1.0.0 -m "メッセージ"):タグオブジェクトとして作成される。作成日時・作成者・メッセージを持ち、git showで確認できる

自動化では注釈付きタグを使うのが原則です。「誰が何のためにデプロイしたか」が記録され、後から確認できます。

2. デプロイスクリプトにタグ付与を組み込む

create_deploy_tag() { local repo_dir="$1" local env_name="${2:-production}" # タグ名は日時+環境名で一意にする local tag_name tag_name="deploy-${env_name}-$(date '+%Y%m%d-%H%M%S')" # 現在のコミットに注釈付きタグを付ける git -C "${repo_dir}" tag -a "${tag_name}" \ -m "Deployed by $(id -un) on $(hostname) at $(date '+%Y-%m-%d %H:%M:%S')" log "[INFO] タグを付与しました: ${tag_name}" # リモートにタグをpush if git -C "${repo_dir}" push origin "${tag_name}" 2>&1; then log "[INFO] リモートへのタグpushが完了しました" else # タグのpush失敗はデプロイの失敗ではない(ローカルには残る) log "[WARN] タグのpushに失敗しました。ローカルには記録されています" fi }

3. 全タグをまとめてpushする

個別にgit push origin タグ名する代わりに、git push --tagsで未pushのタグをまとめてリモートに送れます。ただし軽量タグも含めて全部pushされるため、プロジェクトのルールに合わせて使い分けてください。

# 未pushの全タグをリモートに送る git -C "${REPO_DIR}" push origin --tags # 実行結果例: # Enumerating objects: 1, done. # Counting objects: 100% (1/1), done. # Writing objects: 100% (1/1), 222 bytes | 222.00 KiB/s, done. # Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 # To git@github.com:company/myapp.git # * [new tag] deploy-production-20260828-143012 -> deploy-production-20260828-143012

本記事のまとめ

シェルスクリプトからGitを安全に操作するためのポイントをまとめます。
やりたいこと 設計のポイント
cronでgitコマンドを実行する export GIT_SSH_COMMAND="ssh -i /path/id_ed25519 -o BatchMode=yes"
ローカルに変更があるか確認する git status --porcelain | grep -q .
変更ファイル数をカウントする git status --porcelain | wc -l
pull前後の変更を記録する git diff --stat "BEFORE_HASH" "AFTER_HASH"
マージコミットを作らずpullする git pull --rebase origin main
ローカル変更を一時退避してpullする git stash push -m "メッセージ" → pull → git stash pop
デプロイ完了を記録するタグを付ける git tag -a "タグ名" -m "メッセージ"
タグをリモートに送る git push origin "タグ名" または git push --tags
-C /path/to/repoでリポジトリを指定する、set -euo pipefailtrap cleanup EXITでエラーを確実に拾う、GIT_SSH_COMMANDで認証情報を明示する。。この3点を骨格として組み上げると、cronやsystemdタイマーから呼び出しても壊れないGit操作スクリプトが作れます。デプロイ・設定配布・定期同期など、Gitを使った自動化が格段に安定します。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、20年以上の運用経験を持つ現役エンジニアが基礎から教えます。
シェルスクリプト講座を見る >>

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。