「cronで定期的にGitリポジトリを自動更新したいが、SSH鍵が読めなくてgitコマンドが失敗する」
こういった悩みは、自動化の仕組みを作り始めた頃に必ずぶつかる壁です。原因は、シェルスクリプトが動く環境(cronやsystemdのサービス)はログインシェルと全く別物であり、
SSH_AUTH_SOCKやPATHなどの環境変数が一切引き継がれないことにあります。この記事では、シェルスクリプトから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で注釈付きタグを自動付与し、デプロイ記録をリポジトリ履歴に残す
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
シェルスクリプトで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 pipefailとtrap cleanup EXITでエラーを確実に拾う、GIT_SSH_COMMANDで認証情報を明示する。。この3点を骨格として組み上げると、cronやsystemdタイマーから呼び出しても壊れないGit操作スクリプトが作れます。デプロイ・設定配布・定期同期など、Gitを使った自動化が格段に安定します。
シェルスクリプト講座を見る >>
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
姓・名・メールの3つだけ/30秒/解除は3秒 / 詳細はこちら
- 前のページへ:Linuxサーバーのネットワーク設定をsystemd-networkdで管理する方法|networkctlと設定ファイルの実践手順
- この記事の属するカテゴリ:Linuxtips・シェルスクリプトへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は30秒、解除もいつでも可。
登録無料・いつでも解除できます