ビルドスクリプトが開発者の Mac では正常に動作していても、クラウド Mac にプッシュすると Permission denied や bad interpreter が発生したり、同じ行の末尾に原因の分かりにくい $'\r' が現れたりすることがあります。こうした障害は通常、アプリケーションのロジックではなく、Git のファイルモード、改行コード、実際に使用されるインタープリターが統一された契約になっていないことに起因します。まず失敗したコマンド、コミット番号、スクリプトのパスを記録し、以下の順序で確認すると、キャッシュの削除を繰り返すより早く解決できます。
まずどのレイヤーで失敗しているか確認する
「スクリプトが失敗した」というだけで、すぐに chmod +x を追加してはいけません。シェルスクリプトには少なくとも、作業ツリー上の権限、Git インデックスに記録されたモード、ファイルの実際のバイト列、runner がスクリプトを呼び出す方法という4つの状態が関係します。
script="ci/build.sh"
ls -l "$script"
git ls-files -s -- "$script"
file "$script"
head -n 1 "$script" | cat -vet
git ls-files -s の出力で先頭列が 100755 なら、Git に実行ビットが記録されています。100644 なら通常ファイルです。cat -vet で行末に ^M が表示される場合は、ファイルに CRLF が混入しています。先頭行が #!/bin/zsh や #!/usr/bin/env bash などの有効な shebang になっていることも確認してください。
呼び出し方法も必ず記録します。bash ci/build.sh はファイルに実行権限がなくても動作しますが、./ci/build.sh は権限を確認して shebang を読み取ります。Xcode のビルドフェーズ、ジョブスケジューラーのスクリプト、ローカルターミナルで異なる呼び出し方をしていると、「CI でだけ失敗する」ように見えることがあります。
修正の目的は、ある1回のジョブを偶然成功させることではありません。スクリプトをどのインタープリターで実行するのか、直接実行を許可するのか、どの改行コードを使用するのかをリポジトリに明確に記録することです。
Git インデックスで実行ビットを修正する
直接実行するエントリーポイントのスクリプトには実行ビットを記録します。一方、source で読み込むスクリプトやインタープリターへ明示的に渡すスクリプト片は、通常 100644 のままにします。すべての .sh を実行可能ファイルに変更しないよう、最初にエントリーポイントの一覧を作成してください。
git update-index --chmod=+x ci/build.sh
git update-index --chmod=+x ci/test.sh
git diff --summary
git ls-files -s -- ci/build.sh ci/test.sh
git diff --summary にモード変更が表示されない場合は、ローカル環境で権限の追跡が無効になっていないか確認します。
git config --get core.fileMode
git config core.fileMode true
core.fileMode が影響するのは、現在の作業ツリーで変更を検出する方法だけです。コミットを通じて実際に共有されるのは Git インデックスです。runner の起動時に chmod -R +x . を実行する方法を恒久対策にしてはいけません。リポジトリの誤りが隠れるだけでなく、設定ファイル、機密情報を含むファイル、通常のデータファイルに不要な権限を付与する可能性があります。
直接実行を許可するスクリプトを ci/entrypoints.txt に1行ずつ登録する方法もあります。検証時はこの一覧だけを確認し、新しいエントリーポイントを追加するときは登録ファイルも同時に更新するようにすれば、コードレビューでも権限の境界を変更した理由を把握できます。
LF を固定し、既存ファイルを慎重に正規化する
リポジトリのルートにある .gitattributes を、改行ルールの唯一の情報源にします。一般的なビルド関連ファイルには、次のような基準を適用できます。
*.sh text eol=lf
*.command text eol=lf
*.zsh text eol=lf
*.xcconfig text eol=lf
*.pbxproj text eol=lf
*.png binary
*.zip binary
ルールを追加しても、すでにコミットされているファイルが自動的に書き換えられるわけではありません。変更のないクリーンなブランチで次を実行します。
git add --renormalize .
git status --short
git diff --check
git diff --cached --stat
差分を確認してから正規化の結果だけを別途コミットし、機能変更と混在させないでください。大量のプロジェクトファイルが一度に変更された場合は、実際の変更が改行だけであることを抜き取り確認します。圧縮ファイル、コードジェネレーター、外部スクリプトから CRLF が再び持ち込まれることもあるため、.gitattributes は検査ゲートの代わりにはなりません。
| 検査結果 | よくある原因 | 正しい対処 |
|---|---|---|
100644 のまま直接実行している |
インデックスに権限が記録されていない | git update-index --chmod=+x で修正する |
行末に ^M が表示される |
ファイルが CRLF を使用している | LF に正規化してコミットする |
bad interpreter |
shebang に CR が含まれているか、パスが無効 | 先頭行を修正し、インタープリターの存在を確認する |
| ローカルでは成功し、CI では失敗する | 呼び出し方またはシェルが異なる | 呼び出しコマンドを出力し、シェルを明示的に選択する |
インタープリターの違いを明示的な契約にする
macOS の対話型デフォルトシェルと、CI が実際に使用するシェルは同じとは限りません。スクリプトで Bash の配列、[[ ]]、プロセス置換を使用する場合は、sh に暗黙的に解釈させてはいけません。zsh の構文に依存する場合も、使用するシェルを直接宣言します。
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo_root"
PATH、エイリアス、環境変数が対話型シェルの設定ファイルから提供されることを前提にしてはいけません。CI のエントリーポイント側で必要最小限の PATH を設定し、command -v xcodebuild や command -v ruby などでツールの存在を確認します。必要な環境変数はジョブ設定から注入し、スクリプトでは変数名だけを確認して機密値を出力しないようにします。
Bash の shebang があるファイルを sh script.sh で呼び出すことも避けてください。明示的に指定した sh は shebang を無視するためです。DebugMac ノード上のジョブも、あるセッションでたまたま利用できたシェルから実行環境を推測するのではなく、リポジトリで宣言された内容に従う必要があります。
コミット前と CI に二重の検査ゲートを設ける
ローカルフックはフィードバックを早めるのに役立ちますが、唯一の防御手段にはできません。通常の clone ではフックが自動的に有効にならないためです。CI では依存関係のインストールや Xcode ビルドより前に、軽量な検査を実行します。
#!/usr/bin/env bash
set -euo pipefail
while IFS= read -r script; do
[[ -z "$script" ]] && continue
mode="$(git ls-files -s -- "$script" | awk '{print $1}')"
[[ "$mode" == "100755" ]] || {
printf 'entrypoint is not executable: %s\n' "$script" >&2
exit 1
}
done < ci/entrypoints.txt
if git grep -Il $'\r' -- '*.sh' '*.zsh' '*.command' > /tmp/crlf-files; then
printf 'CRLF detected:\n' >&2
cat /tmp/crlf-files >&2
exit 1
fi
検査ゲートでは、登録ファイルに記載されたパスが実際に存在することも確認し、未コミットの正規化差分があれば失敗させます。最終確認は4項目で実施できます。新規チェックアウトを行い、エントリーポイントのスクリプトを直接実行し、git status --porcelain が空であることを確認してから、非対話型セッションで最小構成のビルドを1回完了させます。これにより、コストの高い依存関係のダウンロードやコンパイルが始まる前に権限と改行の問題を検出でき、runner の実行環境をその場で変更して維持する必要もなくなります。
よくある質問
bash で呼び出すスクリプトにも実行権限は必要ですか?
bash script.sh では不要ですが、直接実行やビルドフェーズ、一部の runner では必要です。実際の入口は Git で 100755 として管理します。
.gitattributes を追加すれば既存の CRLF も直りますか?
自動では直りません。git add --renormalize . を実行して差分を確認し、正規化結果をコミットします。生成物や外部ファイルの CI 検査も残します。
独占型Apple Silicon物理ノードで次のビルドを実行
契約期間とノードのリージョンを選択し、Mac Mini M4、16GB RAM、256GB SSD、支払額(USD)を確認して注文を送信します。