UTF-8 Git ユーザー名のデプロイロックでのエンコード不整合を解消

basecamp/kamal

Kamal はデプロイロック生成時に Git の user.name が UTF-8 を含む場合、Ruby が出力を US‑ASCII とみなすことで Encoding::CompatibilityError が発生していたが、本 PR でエンコーディングを明示的に UTF‑8 に強制し、シェルコマンドは ASCII のみを保ちつつロック詳細を正しく保持できるようになった。

背景

git config user.name の結果が UTF‑8 バイト列であっても、Ruby が外部エンコーディングを US‑ASCII と判定すると文字列のエンコードが不整合となり、ロック情報をシェルへ渡す段階で Encoding::CompatibilityError がスローされた。(このエラーは Cyrillic 文字列 "Сергей Федоров" で再現された。)

デプロイロックは base64 でエンコードされた詳細を SSHKit 経由でリモートに送信し、リモート側で echo "..." > .kamal/lock‑app‑production/details と展開するため、シェルコマンド自体は ASCII‑only である必要がある。(ロック詳細だけが非 ASCII になることは許容される。)

したがって、Git ユーザー名が非 ASCII を含む環境でも、ロック生成ロジックがエンコードエラーで中断しないように、Git 情報取得時点で文字列のエンコーディングを正しく設定する必要があった。

技術的な変更

lib/kamal/git.rbuser_name メソッドに force_encoding(Encoding::UTF_8) を挿入し、取得した文字列を UTF‑8 に強制した上で strip するように変更した。

@@
-    `git config user.name`.strip
+    `git config user.name`.force_encoding(Encoding::UTF_8).strip

この修正に伴い、テストスイートが 3 つ追加された。test/commands/lock_test.rb では、スタブした Kamal::Git.user_name が "Сергей Федоров" を返すケースでロック取得コマンドを生成し、コマンド全体が ASCII のみであることと、エンコードされた詳細をデコードすれば元の UTF‑8 名が復元できることを検証した。

@@
   test "acquire encodes non-ASCII lock details for shell" do
     Kamal::Git.stubs(:user_name).returns("Сергей Федоров")
@@
     assert_predicate command, :ascii_only?
     assert_includes Base64.decode64(encoded_details).force_encoding(Encoding::UTF_8), "Locked by: Сергей Федоров"
   end

test/git_test.rb では、外部エンコーディングが US‑ASCII に設定された状態で git config user.name が UTF‑8 バイト列を返すことをスタブし、Kamal::Git.user_name が正しく UTF‑8 文字列を返すことと valid_encoding? が真であることを確認した。

@@
   test "user name reads UTF-8 bytes under US-ASCII external encoding" do
     user_name = +"Сергей Федоров\n"
     user_name.force_encoding(Encoding::US_ASCII)
@@
     result = Kamal::Git.user_name

     assert_equal "Сергей Федоров", result
     assert_predicate result, :valid_encoding?
   end

これらのテストは、エンコード強制が期待通りに機能し、ロックコマンドが ASCII のみで安全に実行できることを証明している。

設計判断

エンコーディングの強制は Git 情報取得層で行う という方針が採られた。これにより、ロック生成ロジックや SSHKit の呼び出し側は変更不要で、既存のフローをそのまま利用できる。

代替案としては、SSHKit 側で文字列を自動的にエンコード変換する実装や、ロック詳細を UTF‑8 のままシェルに埋め込む方法が考えられたが、シェルの文字セットは環境依存が強く、実行時エラーのリスクが増大するため採用されなかった。

今回の実装は 後方互換性 を保ちつつ、UTF‑8 名を安全に扱える最小限の変更に留めた点が評価できる。既存コードは user_name が文字列を返すだけなので、他の呼び出し箇所への影響はなく、テストカバレッジも追加されたことで信頼性が向上した。

まとめ

Kamal::Git.user_name が UTF‑8 エンコーディングを明示的に適用することで、US‑ASCII 環境下でも非 ASCII Git ユーザー名が正しく扱われ、デプロイロック作成時の Encoding::CompatibilityError を防止した。ロックコマンドは依然として ASCII のみで安全に実行でき、ロック詳細は Base64 エンコードにより元の UTF‑8 名を保持できるようになった。

記事メタデータ

Generated by:
gpt-oss-120b for DiffDaily
LLM Trace:
1833ff76

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
gpt-oss-120b for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文、背景、技術的変更、設計判断(任意)、まとめの全てが揃っており、まとめはリード文の単なる繰り返しではない。構成が明確です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

コードブロックはファイル名付きシンタックスハイライト形式(```ruby:パス)が正しく使用され、PRリンクも #番号 形式で正しくリンク化されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

専門的なエンジニア向けの内容で、余計な初心者向け説明はありません。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクションは総論→各論→結論の段落構成になっており、段落はトピックセンテンスで始まり、1段落1トピック、長さも適切です。空行で区切られています。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

記事中のコードブロックは提供されたDiffと完全に一致し、ファイル名・変更内容とも合致しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

Encoding::CompatibilityError、UTF-8、US-ASCII、Base64 などの用語が正しく使われ、PRで使用されている語彙と一致しています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

技術的な説明はPRの意図・変更点と合致しており、因果関係も論理的です。誤りはありません。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

全ての主張がPRタイトル、説明、Diff、テストコードで裏付けられており、推測や外部知識の付加はありません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号 #1860 などの数値・固有名詞は正確です。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

記事タイトルはPRの内容「Handle UTF-8 git user names in deploy locks」を日本語で的確に表現しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

バージョンサポートやリリース日程などPRに無い外部情報は含まれていません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

時間表現の歪曲はなく、PRの記述と一致しています。