PJR-76AG テストがgitフック配下でGIT_DIRを引き継ぎ実リポジトリを破壊する事故が3度目の再発をした
1. 課題内容
PJR-TPY9 の実行結果をコミットした際、lefthook の pre-commit が npm test を起動し、その中で新規追加されたテストが一時ディレクトリのつもりで git init / add / commit を実行した。git フック配下では親の git が GIT_DIR / GIT_INDEX_FILE を環境変数へ設定するため、env を渡さずに git を起動すると実リポジトリへ適用される。共有 config へ core.bare=true と user.name=test が書き込まれ、worktree ブランチへ不正コミットが作られた。いずれも復旧済みで push 済み履歴には到達していない。PJR-X3E8、PJR-A99J に続く3度目の同型再発であり、規約の記述だけでは再発を止められないことが確認された。
2. 影響範囲
| 観点 | 影響 |
|---|---|
| スコープ | テスト実行環境(vitest.config.ts、tests/helpers/git-environment.ts)、git を起動する全テスト、lefthook の pre-commit 構成 |
| スケジュール | 直接の遅延はない。ただし復旧と調査で PJR-TPY9 の完了が中断した |
| コスト | 事故のたびに復旧作業が発生する。今回は config 復旧・不正コミット破棄・被害範囲確認を要した |
| 品質 | 共有リポジトリの config と履歴が壊れる。core.bare=true により全 worktree が使用不能になり、汚染された identity でのコミットは後から書き換えられない |
| 関係者 | 全開発者と全 agent。フックを通る操作すべてが経路になる |
今回の被害と復旧(確定情報)
| 被害 | 復旧 |
|---|---|
共有 config へ core.bare=true | 済 |
共有 config へ user.name=test / user.email=test@example.com | 済 |
worktree ブランチへ不正コミット1件(squashed register transitions) | 済 |
project/prj-0001/develop および push 済み履歴 | 到達していない |
3. 対応方針
| 項目 | 内容 |
|---|---|
| 原因 | テストプロセスが git フックから継承した GIT_DIR / GIT_INDEX_FILE / GIT_WORK_TREE を落としていない。個々のテストが env を渡し忘れると、一時ディレクトリでの git 操作が実リポジトリへ着弾する |
| 対応策 | テスト実行の入口で git の位置を決める環境変数を除去する。呼び出し箇所ごとの規約ではなく、環境そのものを無害化する |
| 依存事項 | なし。vitest.config.ts と src/exec-worktree.ts の GIT_LOCAL_ENV_VARS が既にある |
| 完了条件 | 下記のとおり |
完了条件
- テストプロセスで git の位置を決める環境変数が除去されている。
GIT_DIRを設定した状態でnpm testを全件実行し、そのGIT_DIRが指すリポジトリと実リポジトリのいずれも変化しないことを確認する。個々のテストの書き方に依存しないことが要件である。 - 除去する変数の一覧を二重に持たない。
src/exec-worktree.tsのGIT_LOCAL_ENV_VARSを参照するか、共通の定義へ寄せる。 - 既存の identity 注入を壊さない。
TEST_GIT_ENVIRONMENTは PJR-EX5E の対策であり、GIT_AUTHOR_*/GIT_COMMITTER_*/commit.gpgsignは引き続き適用される必要がある。 - 現在の検査(
tests/src/git-environment-isolation.test.ts)を、envが渡っているかではなくgitEnvironment()が使われているかまで見るよう強める。env: process.envを通さない。困難な場合は、なぜ強められないかを記録して代替を示す。 - TypeScript 以外からの git 起動の扱いが決まっている。シェルスクリプトや npm scripts は現在の検査の対象外である。フックから呼ばれる経路に限定するか、全体を対象にするかを判断して記録する。
- 3度再発した理由が個別に記録されている。PJR-X3E8、PJR-A99J とも原因を直したと判断した後に再発した。それぞれで何を見落としたか、今回の対処がその見落としをどう塞ぐかを書く。
- 事故時の復旧手順が文書化されている。
core.bareの復旧、汚染 config の除去、不正コミットの破棄、被害範囲(push 到達の有無)の確認方法を含める。 npm run typecheck、npm run lint:ts、npm run test:unit、npm run test:integrationが成功する。
調査済みの事実
- 発生経路は agent の実行中ではなく、人がコミットしたときの lefthook pre-commit の中である。PJR-A99J で対処した agent 起動時の隔離とは別の経路である。
- git を起動する既存の13ファイルはすべて
gitEnvironment()を経由していた。tests/src/exec-agent-protected-config.test.tsには今回とまったく同じ失敗を説明するコメントがある。規約は存在していたが、新しいテストがそれを知らずに書かれた。 - 暫定対処として
tests/src/git-environment-isolation.test.tsを追加した。src/tests/tools/scriptsの git 起動箇所でenvが渡っていなければ失敗する。envを渡さないファイルを置くと実際に検出することを確認済みである。ただしenv: process.envは通過する。 - 対処後、使い捨てリポジトリを
GIT_DIRに指定して全1,386件のテストを実行し、その使い捨てリポジトリと実リポジトリのいずれも変化しないことを確認した。 - 対策の仕組みは既に存在し、片手落ちであった。
vitest.config.tsはtests/helpers/git-environment.tsのTEST_GIT_ENVIRONMENTをtest.envへ渡している。これは PJR-EX5E(identity 混入)の対策で入ったもので、GIT_AUTHOR_*/GIT_COMMITTER_*/commit.gpgsignを注入する。しかしリポジトリの位置を決めるGIT_DIR/GIT_INDEX_FILE/GIT_WORK_TREEは落としていない。ここを除去していれば、個々のテストの書き方に関係なく今回の事故は起きなかった。 - vitest の
test.envは値の設定であり、既存の環境変数の削除に使えるかは未確認である。setupFilesでprocess.envから削除する方法とあわせて確認する。worker とその子プロセスに適用されればよい。
4. 対応結果
解決内容:
- repositoryの位置を決める環境変数一覧を
src/git-environment.tsのGIT_LOCAL_ENV_VARSへ移し、gitEnvironment()とVitest setupが同じ正本を参照する構造にした。src/exec-worktree.tsからの既存exportは維持しているため、既存呼び出し元の互換性は変えていない。 vitest.config.ts、vitest.unit.config.ts、vitest.integration.config.tsの全入口へ共通setupを設定した。各workerはtest moduleを読み込む前にGIT_DIR、GIT_INDEX_FILE、GIT_WORK_TREEなどをprocess.envから除去し、個々のテストがenvを渡し忘れても子プロセスはcwdからrepositoryを解決する。- PJR-EX5Eで導入した
TEST_GIT_ENVIRONMENTは変更せず、author / committer identityとcommit.gpgsign=falseの一時設定を引き続きworkerへ注入する。回帰テストでrepository固有変数が未設定であることとidentityが設定済みであることを同時に確認する。 tests/src/git-environment-isolation.test.tsの文字列window検査をTypeScript AST検査へ置き換えた。envプロパティの有無ではなく、直接呼び出し、短縮プロパティ、拡張objectのいずれもgitEnvironment()由来であることを追跡するため、env: process.envは不合格になる。
3度の再発で見落とした境界:
- PJR-X3E8は事故を起こした1テストへ
env: gitEnvironment()を追加し、記述規約を設けたが、テスト実行の入口は隔離しなかった。今回、Vitest workerのsetupを必須にして、個別テストの遵守に依存しない防御を追加した。 - PJR-A99Jはagent起動環境の隔離とagent前後のGit状態検知を実装したが、人がcommitした際にagent外で起動するlefthook pre-commitの
npm testはその境界を通らなかった。今回、全Vitest設定を直接防御対象に追加した。 - PJR-TPY9の暫定検査はGit起動optionに
envという語があれば通り、env: process.envも許可した。また既存のVitest共通環境はidentity注入だけでrepository固有変数を除去しなかった。今回、ASTでgitEnvironment()由来を検証し、共通setupでworker環境自体を無害化する二層にした。
TypeScript以外の扱い:
- 現在の静的検査は
src、tests、tools、scripts配下のTypeScriptからspawnSync、spawn、execFileSync、execFileでGitを起動する箇所を対象とする。shell scriptとnpm scriptはAST検査の対象外とした。 - 調査時点でlefthookから直接Git fixtureを起動するshell / npm scriptはなく、事故経路はpre-commitからVitestへ入るため共通setupで遮断できる。将来hookから直接Gitを起動するテスト用scriptを追加する場合は、その入口で同じ変数一覧を除去する専用検査を追加する。この境界をSpecDojo システム設計横断ルールとexec設定ガイドへ記録した。
復旧と確認:
- exec運用ガイドへ、共有Git directoryの特定、
core.bare=falseへの復旧、事故で混入したlocal identityだけの除去、reflogによる変更前HEAD特定、remote branch / tagへの到達有無確認、未pushの不正commitを含むworktree / branchの破棄、push済みの場合のrevert方針を記録した。 npm run typecheckとnpm run lint:tsは成功した。危険なGIT_DIR/GIT_INDEX_FILEを設定して共通setupだけを読み込むスモーク確認でも、両変数が削除された。npm run test:unit、npm run test:integration、npm run validate:schemaはexecutor内で二重実行せず、pipeline親runnerの検証結果を正本とする。
5. 関連ドキュメント
- 暫定対処を含む実装: PJR-TPY9 Register履歴をGitコミット粒度から分離する
- 1度目の同型事故: prj-0001:pjr-x3e8-test-git-env-bare-repository
- 2度目の同型事故: prj-0001:pjr-a99j-agent-git-isolation-breach
- git identity の混入: prj-0001:pjr-ex5e-git-identity-isolation
- 検査の実体:
tests/src/git-environment-isolation.test.ts、tests/setup/git-environment.ts、src/git-environment.ts