From 61d3eb38816cad69de95b99db359c45420e787f4 Mon Sep 17 00:00:00 2001 From: gotomts Date: Sun, 30 Aug 2026 00:38:58 +0900 Subject: [PATCH] =?UTF-8?q?fix(setup):=20cutover=20=E3=81=AE=20skip=20?= =?UTF-8?q?=E5=88=A4=E5=AE=9A=E3=81=AB=E5=AE=A3=E8=A8=80=E5=85=A5=E5=8A=9B?= =?UTF-8?q?=E3=81=AE=20fingerprint=20=E3=82=92=E5=8A=A0=E3=81=88=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit migrate は cutover の manifest success を「必須バイナリが実在するか」だけで 再検証していたため、別端末で main を pull して Homebrew 宣言が変わっても skip され、`sudo zsh setup/migrate.zsh --apply` が新しいアプリを適用しなかった。 必須バイナリは前回の switch のまま残っているので postcondition は満たされ続ける。 直近 success 時の desired-input fingerprint を manifest の detail に記録し、 現在値と一致するときだけ skip するようにした。fingerprint は nix/ 配下の構成 (ファイル内容と symlink の readlink 値) と flake.lock、/etc/dotfiles-role、 ~/.config/dotfiles/homebrew.local.nix の内容/不在を材料にする。記録する値は switch を起動する直前に確定させ、switch 実行中に宣言が変わった場合はその変更を 適用済みとして取りこぼさない。fingerprint の記録が無い旧 manifest は安全側に 1 度だけ再実行し、その実行で記録して以降は通常の skip に戻る。 宣言変更で cutover が日常的に走るようになるため、PAM の扱いも 2 点締める。 cutover 直前に退避した Touch ID ファイル (.before-restore.) は switch 後の pam 再適用が success し、かつ内容が再適用後の pam.d/sudo_local と byte-for-byte 一致するときだけ削除する。cutover / pam の失敗時と、手書き追加ルール等で 内容が一致しない場合はその内容の唯一の控えなので残す。削除はこの実行が作った 1 パスだけを 追跡して行い、別の実行が残した退避ファイルには触れない。 また が pristine でないのに .before-setup が無い場合は、pristine が何だったかの 記録が無いまま上書きすることになるため switch 前に停止する (fail-closed)。 自体が無い真の初回はこれまでどおり通す。 既存の必須バイナリ postcondition と PAM の fail-closed 保証はそのまま維持する。 Co-Authored-By: Claude Opus 5 --- setup/README.md | 12 +- setup/migrate.zsh | 248 +++++++++++++++++-- setup/tests/migrate.bats | 505 ++++++++++++++++++++++++++++++++++++--- 3 files changed, 710 insertions(+), 55 deletions(-) diff --git a/setup/README.md b/setup/README.md index e25aa56..e0fbc60 100644 --- a/setup/README.md +++ b/setup/README.md @@ -39,6 +39,15 @@ success したステップは再実行しない(idempotent)。全ステッ 非ゼロ終了コードを返し続ける(部分適用を健全な状態として扱わない)。`SUDO_USER` が特定できない 環境(sudo を介さない直接 root ログイン等)では非 root ステップは blocked のまま止まる。 +例外は `cutover`(`darwin-rebuild switch`)で、manifest の success だけでは skip しない。 +必須 Homebrew バイナリ(mise/starship)の実在に加え、直近 success 時に記録した +desired-input fingerprint — `nix/` 配下の構成と `flake.lock`、`/etc/dotfiles-role`、 +`~/.config/dotfiles/homebrew.local.nix` — が現在値と一致するかを毎回検証し、変わっていれば +同じ `--apply` の中で再実行する。`git pull` で Homebrew 宣言が変わった端末で `--apply` が +「全部 success 済み」と判断して何も適用しない、という取りこぼしを防ぐため。fingerprint の記録が +無い古い manifest は安全側に 1 度だけ再実行する(その実行で記録され、以降は通常どおり skip +に戻る)。何が再実行されるかは `--dry-run` で事前に確認できる。 + 個別スクリプトの直接実行はメンテナンス目的(単体テスト・特定ステップだけをデバッグしたい場合等) でのみ行う: @@ -101,7 +110,8 @@ symlink 越しに即座に反映される。再実行が必要なのは「`setup `claude-sync`/`codex-sync`/`herdr-sync`)。`languages.zsh` 自身が「mise は darwin-switch で事前導入 済みが前提」と明記しているため、cutover を languages より先に置く。各ステップの結果は `~/.dotfiles-migrate/manifest.log` に永続化し、success 済みステップは再実行しない - (idempotent な部分適用検出・再開)。権限不足なステップは blocked として記録し、その + (idempotent な部分適用検出・再開)。ただし `cutover` だけは、必須バイナリの実在と + desired-input fingerprint の一致(宣言側が変わっていないこと)も満たすときにのみ skip する。権限不足なステップは blocked として記録し、その Phase 内の残りは試行を続けるが次の Phase へは進まない(Phase 境界は厳格)。実失敗は 即座に全体を停止する(fail-closed)。`rollback.zsh` は一切呼ばない(no-automatic-rollback、 常に人間の明示判断)。全ステップ success 後も、manifest の自己申告を信用せず各ステップの diff --git a/setup/migrate.zsh b/setup/migrate.zsh index 0ba03c5..b9aeb5b 100755 --- a/setup/migrate.zsh +++ b/setup/migrate.zsh @@ -47,12 +47,19 @@ # 残りステップだけ試行を続け、Phase 境界は厳格に守る(次の Phase には進まない) # - 冪等検知: manifest に success の記録があるステップは再実行せず skip する。 # これにより「部分適用済みの実機」を安全に検出・再開できる。ただし cutover は -# manifest の success だけでは skip 可としない: postcondition(mise/starship 等、 -# desired Homebrew set の必須バイナリが実在すること)も満たしているか都度再検証する -# (migrate::skippable)。desired set が switch 後に変わった場合(例: starship を -# 後から追加)、古い success はもう postcondition を保証しないため、その場合は -# manifest に `postcondition-unmet` を記録したうえで同じ --apply 内で cutover を -# 再実行する(実機インシデント、2026-08-22) +# manifest の success だけでは skip 可としない。次の 2 つを都度再検証し、どちらかが +# 崩れていれば manifest に `postcondition-unmet` を記録したうえで同じ --apply 内で +# cutover を再実行する(migrate::skippable / migrate::cutover_rerun_reason): +# 1. postcondition(mise/starship 等、desired Homebrew set の必須バイナリが実在 +# すること)。desired set が switch 後に変わった場合(例: starship を後から +# 追加)、古い success はもう postcondition を保証しない(実機インシデント、 +# 2026-08-22) +# 2. 直近 success 時に記録した desired-input fingerprint(nix/ 配下の構成と +# flake.lock、/etc/dotfiles-role、~/.config/dotfiles/homebrew.local.nix)が +# 現在値と一致すること。1 だけでは「main を pull して宣言にアプリが増えた」 +# ケースを検出できない(必須バイナリは前回の switch のまま残っているので +# skip され、増えたアプリが適用されない)。fingerprint の記録が無い旧 manifest +# は安全側に 1 度だけ再実行する # - PAM/cutover の所有権結合: nix-darwin の activation は pam.d/sudo_local を # 「macOS 純正デフォルトへの symlink であること」以外一切許容しない(許容ハッシュが # 常に空)。setup/pam.zsh が Touch ID 内容へ書き換え済みの状態で switch すると @@ -64,7 +71,11 @@ # 再実行されるたびに同じ手順を踏む(実機インシデント、2026-08-22)。復元自体は # 既存の宛先へ直接 mv せず、nix-darwin 自身の /etc 置換手順と同じ「新規パスへ退避 # してから空いたパスへ置く」形を取る(既存宛先への直接 mv は macOS 側が -# "Operation not permitted" を返した実機インシデントあり、2026-08-23) +# "Operation not permitted" を返した実機インシデントあり、2026-08-23)。退避した +# ファイルは、switch 後の pam 再適用が success し、かつ内容が再適用後の pam.d/sudo_local +# と byte-for-byte 一致するときだけ削除する(複製にすぎないと確認できた場合のみ)。 +# cutover か pam が失敗した場合、および手書き追加ルール等で内容が一致しない場合は +# その内容の唯一の控えなので残す(migrate::pam_discard_vacated) # - 部分適用は健全な状態として扱わない: 全ステップが success になるまで # --apply は終了コード 1 を返し続ける。「動いているように見える」ことは # 完了の根拠にしない(health check で実ファイル/実状態を確認する) @@ -91,6 +102,10 @@ PHASE1_STEPS=(link) PHASE2_STEPS=(cutover pam) PHASE3_STEPS=(languages defaults claude-sync codex-sync herdr-sync) +# この --apply の中で cutover 直前に退避した Touch ID ファイルのパス( +# migrate::pam_restore_pristine_if_safe が設定し、migrate::pam_discard_vacated が使う)。 +MIGRATE_PAM_VACATED_PATH="" + migrate::script_for() { echo "${SETUP_DIR}/${1}.zsh" } @@ -255,6 +270,86 @@ migrate::command_available() { PATH="${homebrew_paths}:${PATH}" command -v "${name}" &>/dev/null } +# migrate::nix_dir fingerprint の対象にする nix 宣言ディレクトリ。テストでは +# MIGRATE_NIX_DIR_OVERRIDE で差し替える(このリポジトリ自身の nix/ を書き換えずに +# 「宣言が変わった」状況を再現するため)。 +migrate::nix_dir() { + echo "${MIGRATE_NIX_DIR_OVERRIDE:-${SETUP_DIR:h}/nix}" +} + +# migrate::fingerprint_entry