release: PLAN03-1 devbase env export / import#22
Conversation
* chore(PLAN03-1-export-local): Draft PR 作成 * feat(env): devbase env export を追加 (PLAN03-1 PR1) - lib/devbase/env/bundle.py: tar.gz + manifest.yml バンドル構築/展開、sha256 検証、未知 version 拒否、パストラバーサル拒否 - lib/devbase/env/cipher.py: pyrage 経由の age 暗号化/復号 (X25519 / OpenSSH ed25519,rsa / passphrase / @path 参照) - lib/devbase/env/storage.py: Local + Stdio backend、s3/gs は本 PR では未実装で明示エラー - lib/devbase/env/io_export.py: 機密キー検知警告、既定鍵 (~/.ssh/id_rsa.pub) 自動利用、--passphrase-stdin と DEST='-' 併用拒否 - cli.py / commands/env.py: env export サブコマンド登録 + SUBCMD_MAP 更新 - pyproject.toml: pyrage>=1.2 を deps、pytest>=8.0 を dev group、tool.pytest.ini_options 追加 - tests/env, tests/cli: ラウンドトリップ + 異常系 28 件 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): レビュー指摘の修正 (storage/bundle/cipher) - storage.py: LocalBackend で file:// URI を url2pathname で実パスへ変換 - bundle.py: manifest.files の要素型 (dict, path: str, sha256: str) を検証 - cipher.py: age 秘密鍵判定をバイト列で行い、UTF-8 デコード失敗を明示エラー化 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(env): round 2 レビュー指摘の修正 (堅牢性 + test 追加) - storage: file:// URI の netloc が空/localhost 以外なら StorageError で拒否 (codex major) - bundle: tar 内の重複エントリを BundleError で検出 (codex major) - cipher: _resolve_recipient の @path 再帰に深さ制限 (上限 5) を追加 (gemini minor) - tests/storage: file:// URI roundtrip と remote host 拒否の test を追加 (gemini minor) - tests/bundle: _validate_manifest 不正系 (files が list でない / entry が dict でない / path 不正 / sha256 不正) + 重複エントリの test を追加 (gemini minor) - tests/cipher: @path 循環参照で CipherError を返す test を追加 (gemini minor) * fix(env): sha256 必須化と ed25519 デフォルト鍵対応 (round 3) - bundle.py: manifest.files[*].sha256 を必須の 64 文字 16 進文字列として検証 None / 欠落 / 長さ違い / 非 16 進は BundleError。完全性チェック迂回を防止 - cipher.py: default_recipient_paths / default_identity_paths に ed25519 (id_ed25519.pub / id_ed25519) を追加し、rsa より優先 - tests: sha256 欠落 / None / 長さ違い / 非 16 進の異常系テストを追加 - tests: ed25519 がデフォルトパス候補に含まれ rsa より優先されることを検証 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(env): round 4 レビュー指摘の修正 (異常系の堅牢化) - bundle: yaml.safe_load の結果が dict でない場合に BundleError を送出 (top-level が list/str/数値の場合に AttributeError が漏れるのを防止) - cipher: @path 参照ファイルが UTF-8 でない場合 CipherError に包んで再送出 (UnicodeDecodeError が呼び出し側に漏れていた) - storage.resolve: Windows ドライブレター (C:\path 等) を urlparse が scheme と誤認する問題に対応し LocalBackend にフォールバック 各修正に対応する異常系 test を追加 (合計 +5 test)。 * fix(env): _resolve_identity の OSError を CipherError に包む (round 5) - lib/devbase/env/cipher.py: path.read_bytes() を try/except OSError で ラップし、I/O エラー時も CipherError で統一されたエラー型を返す - tests/env/test_cipher.py: monkeypatch で read_bytes に OSError を 発生させて CipherError に包まれることを検証する test を追加 gemini round 5 指摘 (minor / 堅牢性) に対応。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(env): round 6 レビュー指摘の修正 (決定性 + 完全性 + 堅牢性) - bundle.pack: gzip.GzipFile(mtime=0) でラップし出力を完全に決定的にする - bundle._validate_manifest: tar 内ファイルセットと manifest の完全一致を 検証し、manifest に記載のない未知ファイルを BundleError で拒否する - cipher._resolve_recipient: @path の read_text で発生する OSError を CipherError に包んで一貫したエラーハンドリングにする - cipher._resolve_identity: OpenSSH ヘッダで先に SSH 鍵を判別する分岐を 追加し、鍵形式判別を明示化 (将来の形式追加もしやすくする) - tests: pack 決定性 / unknown file 拒否 / @path OSError ラップ / OpenSSH ヘッダ優先判別の test を追加 * fix(env): @path 参照ファイルのコメント・空行をスキップする (round 6 追加) recipient ファイルにコメント (# 始まり) や空行が混在していても扱えるよう、 有効な最初の行のみを採用する。テストも追加。 * fix(env): round 1 レビュー指摘の修正 (TOCTOU + BundleError 統一 + prefix 互換 + completion) - storage.py: LocalBackend.write_bytes を os.open(mode=0o600, O_CREAT|O_TRUNC|O_WRONLY) で 作成時点から 0600 を強制し、umask に依らない TOCTOU 安全な書き込みに変更 (codex major / gemini minor — 同一指摘)。既存ファイル上書き時も先に chmod で権限を絞る。 read_bytes / write_bytes の OSError を StorageError にラップ (gemini minor)。 - bundle.py: unpack() の tarfile.open / getmembers / extractfile で発生する tarfile.TarError / OSError を BundleError にラップ (gemini major)。 make_entries_from_disk の exists() を is_file() に変更し、対象パスが ディレクトリだった場合の IsADirectoryError を防止 (gemini minor)。 _validate_manifest に manifest.files の path 重複検出を追加 (codex minor)。 - cli.py: SUBCMD_PREFIX_PREFERENCES を追加し、`devbase env e` が引き続き edit に 解決されるように prefix 解決の後方互換を維持 (codex minor)。 - etc/devbase-completion.bash, etc/_devbase: env export サブコマンドと 各オプションを補完に追加 (codex minor)。 - tests: storage の TOCTOU / OSError ラップ / 既存ファイル 0600 上書き、 bundle の path 重複 / 壊れた tar / is_file 切替、CLI prefix の後方互換テストを追加。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): round 2 advisory レビュー指摘の修正 (docstring / help / stdin prompt) - io_export.py: `_resolve_recipients` の docstring を更新し、既定鍵が `id_ed25519.pub` → `id_rsa.pub` の優先順で探索される実態に合わせる - cli.py: `--recipient` の help を `Default: ~/.ssh/id_ed25519.pub, then ~/.ssh/id_rsa.pub (first existing one)` に修正 - io_export.py: `--passphrase-stdin` で `sys.stdin.isatty()` の場合に `passphrase: ` プロンプトを stderr に表示し、対話実行時のハング感を解消 - 暗号化キー未指定エラーメッセージも ed25519 優先を反映 - tests/cli/test_env_export.py: tty / 非 tty 双方の挙動を検証する 2 ケース追加 Refs: PR #14 review comments 3280597873 / 3280597877 / 3280597881 --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: PLAN03-1 PR2 Draft PR 作成 (import 実装) * feat(env): PLAN03-1 PR2 devbase env import (Local + Stdio) `devbase env import` を追加し、export で生成したバンドル (age 暗号化済み or 平文 tar.gz) から `.env` 群を復元できるようにする。 主な機能: - merge セマンティクス: --merge keep-existing (既定) / prefer-incoming - --replace-keys: 指定キーのみ上書き - --replace: 対象 .env を丸ごと差し替え (backup 取得) - --dry-run: 差分プレビュー (書き込みなし) - 2 フェーズ書き出し (prepare → commit) で部分適用を最小化、失敗時は backup から best-effort で rollback - --backup-dir / --keep-last N (既定 10) で backup を GC - .env.sources.yml は既定で上書きせず参照用コピーのみ、--merge-metadata で 新規 source エントリのみ追加、--no-metadata で完全無視 - 暗号化判定: gzip magic で平文 / age 暗号化を識別 - 引数バリデーション: SOURCE='-' と --passphrase-stdin の併用拒否、 --passphrase-env と --passphrase-stdin の併用拒否、--replace と --replace-keys の併用拒否 - 既定 identity: ~/.ssh/id_ed25519 → ~/.ssh/id_rsa の順で探索 テスト (tests/cli/test_env_import.py, 17 ケース): - export → import の round-trip、0600 permission 保持、LF 保持 - merge モード別の挙動 (keep-existing / prefer-incoming / replace-keys / replace)、dry-run が変更しないこと、replace 時の backup 作成 - --include-project / --no-metadata / --merge-metadata の挙動 - 未知 manifest version のバンドル拒否、--keep-last による古い backup GC - 平文バンドル import、passphrase round-trip Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): import の merge/rollback/GC 安全性を改善 cross-review round 1 で指摘されたデータ損失・部分適用・merge 不整合を修正: - --replace-keys 指定外でも既存に無い incoming 新規キーは追加するように修正 (CLI help の "other keys behave like keep-existing" に整合) - _rollback で op='create' なターゲットは backup が無くても unlink し、 commit 途中失敗時の部分適用残骸を消す - _gc_backups は devbase が生成する timestamp 形式 (YYYYMMDD-HHMMSS) の ディレクトリのみを削除対象にし、--backup-dir 親に置かれた無関係なファイル /ディレクトリを誤って消さないようにする - EnvFile.parse_bytes(data) を新設し、io_import の bytes パースを 一時ファイル経由から直接パースに置き換え (I/O 削減 + 例外安全) - 上記 3 件分の回帰テストを追加 Refs: codex review #4336666744 / gemini review #4336672519 * fix(env): import の二重エスケープ / rollback / tmp 残骸 / completion を修正 PR #15 round 2 のクロスレビュー指摘 (codex 3 件 + gemini 2 件) に対応。 * EnvFile.parse_bytes に double-quote 内 escape の逆変換 (state machine) を 追加し、save / _format_env_bytes との round-trip を成立させる。これにより backslash / quote / 改行を含む値が import → export を経て二重エスケープ される問題を解消する。 * _plan_env_merge の create パスでは _format_env_bytes による再シリアライズを 避け、incoming_bytes をそのまま採用する。バンドル側のバイト列を完全に 保持し、parse/format に潜む副作用を確実に排除する。 * _rollback で「backup が無い = 元ファイル不在」のケースを op に関係なく unlink するように変更。op='sources-merge' で sources.yml を新規作成した ロールバックでも残骸が残らなくなる。 * _commit 失敗時に、まだ rename されていない .import.tmp ファイルを try-finally で確実に削除する。 * _make_backup_dir に microsecond + 連番フォールバックを付与し、同一秒内に import が複数回走っても backup ディレクトリが衝突しない。 * etc/devbase-completion.bash と etc/_devbase の env サブコマンド一覧に import を追加し、各オプションも補完できるようにする。 テストでは parse_bytes round-trip / create 経路の byte preservation / sources.yml rollback unlink / tmp cleanup / backup 衝突回避を検証する。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): import の merge でコメントを保持し $ をエスケープする - ``EnvFile.parse_entries`` を追加し、コメント / 空行 / kv を行単位で トークン化できるようにする (PR #15 gemini 指摘)。 - ``_merge_into_existing_bytes`` で既存 ``.env`` のコメント・空行・キー順を 保持したまま値を差し替えるよう merge 経路を変更。``EnvFile.dump_bytes`` への単純差し替えではコメントが失われていた。 - ダブルクオート値に含まれる ``$`` を ``\$`` にエスケープし、``parse_bytes`` 側でも ``\$`` を ``$`` に戻すように round-trip 対応 (シェル ``source`` 時の 意図しない変数展開を防止 / PR #15 gemini 指摘)。 - ``EnvFile.dump_bytes`` / ``EnvFile.dump_entries_bytes`` にフォーマット ロジックを集約し、``io_import._format_env_bytes`` を廃止。``EnvFile.save`` も ``dump_bytes`` 経由に統一して二重実装を解消 (PR #15 gemini 指摘)。 - 新規テスト: コメント保持マージ (prefer-incoming / keep-existing)、 ``$`` の round-trip、``EnvFile.dump_bytes`` のエスケープ仕様を追加。 * refactor(env): _plan_env_merge の重複を _build_merge_plan に共通化 各マージ戦略 (replace_keys / keep-existing / prefer-incoming) で個別に書かれていた new_bytes 生成と _Plan 構築を `_build_merge_plan` にまとめた。各分岐は merged / added / overwritten / skipped の計算だけを担当するように整理。 動作変更なし (PR #15 gemini round4 指摘 / 既存テスト 102 件 PASS)。 * fix(env): コメントのみ既存 .env を merge 経路に通す + env i 短縮維持 - _build_merge_plan / _plan_env_merge / replace ブランチの op 判定を existing (key=value dict) の有無から target.exists() に変更。 コメント / 空行のみで構成された既存 .env が「create」と誤判定されて incoming_bytes で上書きされ既存コメントが失われる問題を修正 (PR #15 round5 codex/gemini 指摘)。 - SUBCMD_PREFIX_PREFERENCES に i: init を追加。import 追加で devbase env i が init / import の両方にマッチして ambiguous に なっていたため、既存ショートカット (devbase env i → init) を維持する (PR #15 round5 codex 指摘)。 - 回帰テスト追加: - tests/cli/test_env_import.py: コメント/空行のみの既存 .env が prefer-incoming / keep-existing / replace-keys でコメント保持される こと。--replace ブランチでも op='replace' として報告され backup が 取られること - tests/cli/test_prefix_resolution.py: devbase env i → init, devbase env im → import, devbase env in → init Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): import の sys を先頭 import に / TTY 時 getpass.getpass でエコー抑止 gemini round 6 で挙がった minor 指摘 2 件に対応: 1. lib/devbase/env/io_import.py:97 `import sys` がローカル import になっていたのを ファイル先頭の標準 import セクションに移動 (`import getpass` も同時追加)。 メンテナンス性向上、PEP 8 への準拠。 2. lib/devbase/env/io_import.py:100 TTY 入力時に `sys.stdin.readline()` を使って いたため、パスフレーズがそのまま画面にエコーバックされていた。 `getpass.getpass(prompt, stream=sys.stderr)` を使うように変更。 パイプ入力時 (非 TTY) は従来どおり `sys.stdin.readline()` で読み ( stdin リダイレクト経由のテストフックとして必要)、getpass の EOFError は `ImportError("stdin からパスフレーズを読み取れませんでした")` に変換する。 回帰テスト 3 件を追加 (tests/cli/test_env_import.py): - test_read_passphrase_uses_getpass_on_tty - test_read_passphrase_falls_back_to_stdin_on_pipe - test_read_passphrase_tty_eof_raises_import_error ローカル品質チェック: - uv run pytest tests/ -> 112 passed (107 + 3 + 既存 review round で +2) - uv run python -m compileall lib bin -> OK - uvx ruff check --select=E9,F63,F7,F82 lib -> All checks passed Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): export 側にも TTY エコー抑止と先頭 import を適用 (import と対称化) PR #15 round 7 で io_import.py に入れた修正 (commit 454425e) を、対称な io_export.py 側にも適用する。両者は _read_passphrase の構造が同じで、 TTY エコー抑止と import 位置の問題も同じパターンで残っていた。 - lib/devbase/env/io_export.py: - ローカル `import sys` をファイル先頭の標準 import セクションに移動 - `import getpass` を追加 - TTY 入力時に `getpass.getpass("passphrase: ", stream=sys.stderr)` を 使うように変更。EOFError は ExportError("stdin からパスフレーズを 読み取れませんでした") に変換 - パイプ入力時 (非 TTY) は従来どおり `sys.stdin.readline()` を使用 - tests/cli/test_env_export.py: - 旧テスト test_read_passphrase_shows_prompt_on_tty / test_read_passphrase_no_prompt_on_pipe は `print` 経由の prompt 表示を 検証する構造だったため、getpass.getpass をモックする方式に書き換え - 追加: test_read_passphrase_uses_getpass_on_tty (TTY 時に getpass.getpass が呼ばれ stdin は消費されないこと) - 追加: test_read_passphrase_falls_back_to_stdin_on_pipe (非 TTY では getpass は呼ばれず stdin.readline 経路に入ること) - 追加: test_read_passphrase_tty_eof_raises_export_error (EOFError → ExportError 変換) ローカル品質チェック: - uv run pytest tests/ -> 113 passed - uvx ruff check --select=E9,F63,F7,F82 lib -> All checks passed Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(env): _unescape_double_quoted を re.sub + 逆引き辞書に書き換え PR #15 ユーザーレビュー (lib/devbase/env/store.py:160) で挙がった 「pythonic に書き直してください」への対応。 before: バックスラッシュ + 次文字を 1 文字ずつ走査する hand-rolled state machine (25 行)。 after: re.compile(r'\\.') で \<char> を一括捕捉し、逆引き辞書 {'\\\\': '\\', '\\"': '"', '\\n': '\n', '\\$': '$'} で 1 行置換する re.sub 呼び出し 1 つに集約 (5 行)。 挙動は state machine と完全に同等: - 未知エスケープ (例: \x) は dict.get の default にマッチ文字列 自身を返すことでバックスラッシュごと保持 - 末尾の単独 \ は \\. のドットが 2 文字目を要求するため自然に マッチせず、そのまま保持 - \\\\n (リテラル \\ + n) と \\n (改行) も re.sub は左から非重複で マッチするため state machine と同じ結果 差分: 43 → 15 行 (net -28 行 削除 / +15 行 追加 = -13 行)。 既存テスト (test_envfile_parse_bytes_round_trip_with_escapes 等) で double-quote escape の round-trip は完全に検証済みのため新規テストは追加せず。 ローカル品質チェック: - uv run pytest tests/ -> 113 passed - uvx ruff check --select=E9,F63,F7,F82 lib -> All checks passed - uv run python -m compileall lib bin -> OK Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: PLAN03-1 PR3 Draft PR 作成 (S3 backend) * feat(env): PLAN03-1 PR3 devbase env export/import S3 backend - `s3://bucket/key` を `devbase env export` / `devbase env import` の 入出力先として指定できるようにする - export 時は ServerSideEncryption (`aws:kms` 既定, `AES256` 切替可) を 常に PutObject に付与し、加えて GetBucketEncryption で **バケット側の 既定暗号化** も事前確認する - 暗号化未設定 / 確認不可 (AccessDenied) のバケットへは `--unsafe-allow-unencrypted-bucket` を明示しない限り export を拒否する (オブジェクト単位の SSE はこのフラグに関係なく常に付与される) - SSE 種別 / KMS 鍵 / エンドポイント / リージョンは環境変数 (`DEVBASE_S3_SSE`, `DEVBASE_S3_SSE_KMS_KEY_ID`, `DEVBASE_S3_ENDPOINT_URL`, `DEVBASE_S3_REGION`) で上書きできる - `boto3` は optional dep として `[project.optional-dependencies] s3` に追加 (`pip install 'devbase[s3]'` でインストール) - `gs://` (GCS) は PLAN03-1 PR4 廃案のため明示エラーで拒否する Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(env): PLAN03-1 PR3 storage.py minor 修正 (cross-review round 1) PR #19 のクロスレビュー (codex / gemini) で指摘された minor 3 件に対応。 - `_parse_s3_uri`: `urlparse` は S3 キーに含まれる `?` / `#` を query / fragment として落としてしまうため、AWS CLI と同じ挙動になるよう スキームを除去した上で `partition('/')` で分割する。 - boto3 未インストール時のエラーメッセージを `pip install boto3` から 本プロジェクトの optional dependency 経由 (`pip install 'devbase[s3]'` / `uv add 'devbase[s3]'`) に変更。 - `_verify_bucket_encryption`: MinIO / LocalStack 等の S3 互換ストレージで GetBucketEncryption が NotImplemented を返すケースに備え、 `--unsafe-allow-unencrypted-bucket` 指定時は未知エラーも警告のみで続行する 逃げ道として機能させる (CHANGELOG の S3 互換ストレージ対応との整合)。 新規テスト: query/fragment 保持、未知エラーの拒否、unsafe フラグでの続行を追加。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(env): PLAN03-1 PR3 boto3 を main dependency に昇格 boto3 を `[project.optional-dependencies].s3` から `[project].dependencies` に移し、ImportError ハンドラとフォローアップ案内文を撤去する。 意図: - S3 URI を初めて指定したユーザに `pip install 'devbase[s3]'` を 打たせる UX を廃する。25MB 程度のコスト増 (botocore 24MB) は 実装複雑度ゼロと引き換え。 - 引数検出 (`s3://` 走査) や lazy 自動 install を採らないのは、 CI / オフライン / read-only コンテナで挙動が安定するため。 storage.py / test_storage.py の boto3-missing 関連コードを削除。 CHANGELOG.md の optional 記述も同期更新。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(env): PLAN03-1 PR5 env export/import モジュールを整理
- 711 行に肥大化していた `io_import.py` を以下の 3 モジュールに分割する。
公開 API (`ImportOptions`, `import_bundle`, `ImportError`) は維持し、
テストの `_read_passphrase` 直接 import や `getpass` パッチも継続して動く:
- `io_import.py` (209 行): 引数検証 / 復号判定 / 全体オーケストレーション
- `_import_merge.py`: `Plan` データクラス、`plan_env_merge` / `plan_sources`、
既存コメント・空行を保持した merge ロジック、ログ整形
- `_import_atomic.py`: 2 フェーズ書き込み (`backup_existing` → `write_atomic`
→ `commit`)、`gc_backups`、ロールバック
- export / import で重複していた共通 helper を `io_common.py` に集約する:
- `read_passphrase()` — env / stdin 入力、tty 時の getpass エコー抑止
- `resolve_recipient_specs()` / `resolve_identity_specs()` — 省略時の
`~/.ssh/id_ed25519(.pub)` → `id_rsa(.pub)` fallback
- `write_secure_bytes()` — `os.open(O_WRONLY|O_CREAT|O_TRUNC, mode=0o600)` で
TOCTOU を避けてセキュアにバイト列を書き出す共通実装。`storage.LocalBackend`
と `_import_atomic` から呼び出す
- `_plan_env_merge()` の 4 段ネスト if/elif を 4 つの小さな関数
(`_plan_replace` / `_plan_replace_keys` / `_plan_keep_existing`
/ `_plan_prefer_incoming`) に分割し、`plan_env_merge` 本体はモード選択のみに
簡素化する
- `storage.LocalBackend.write_bytes` を `io_common.write_secure_bytes`
への薄いラッパに置き換え、重複していた `os.open` + chmod パターンを削除
- `io_export.py` (185 → 168 行) の `_read_passphrase` / `_resolve_recipients`
は `io_common` への delegation に置き換え、`encrypt_payload` / `validate_options`
の helper 関数に export 本体のロジックを分解
- `_commit()` 移動に伴い、テスト 3 件の `monkeypatch.setattr(_io_import.os, 'replace', ...)`
パッチ先を `_import_atomic.os` に更新。`log_plans` 移動に伴う caplog の
logger 名も `devbase.env._import_merge` に追従
リファクタの動機:
- io_import.py が PR1〜PR3 を通じて 711 行まで肥大化し、merge 計画 / atomic 書き込み
/ orchestration が同居して読みづらかった
- io_export と io_import で `_read_passphrase` / 既定鍵 fallback / セキュア書き込みが
ほぼ重複していた
挙動の変更は無く、全 136 テストが引き続き green を維持する。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* docs(env): PLAN03-1 PR5 env export/import 利用者向けガイドを追加
`docs/user/env-export-import.md` (456 行) を新設し、以下を網羅する:
- 対象ファイル一覧 (global / projects/*/.env / .env.sources.yml) と
公開可能な雛形 (`projects/*/env`) を含めない設計理由
- クイックスタート (既定鍵での export → 別マシンでの import)
- バンドル構造 (manifest.yml の sha256 検証、version 互換ポリシー)
- age 暗号化: recipient / identity / passphrase の 3 方式、対応鍵種別表、
ssh-ecdsa 非対応への対処 (`age-keygen` / `ssh-keygen -t ed25519`)、
既定鍵 (`~/.ssh/id_ed25519` → `id_rsa`)
- 入出力先: ローカル / stdio / S3。S3 の SSE 強制と
`--unsafe-allow-unencrypted-bucket`、`DEVBASE_S3_*` 環境変数
- export / import の全オプション表、merge モード (keep-existing /
prefer-incoming / --replace-keys / --replace) の動作比較
- `.env.sources.yml` の取り扱い (既定スキップ + 参照用コピー、
`--merge-metadata` での新規エントリ追加)
- 2 フェーズ書き込み + backup + ロールバックの仕組み、
`--keep-last N` での GC、ACID 非保証の注意
- 典型ワークフロー 4 件 (別マシン移行 / 定期バックアップ / S3 チーム共有 / CI 配布)
- トラブルシューティング 8 件
加えて以下のリンクを追加:
- `README.md`: 「利用者向け」ドキュメント表に env-export-import.md への
リンクを追加し、`env` グループの説明に export / import を併記
- `docs/user/environment-variables.md`: 「別マシンへの移行 / バックアップ」
節を新設して env-export-import.md へ誘導、ベストプラクティスに追記
- `CHANGELOG.md` (Unreleased): docs 追加とリファクタリングを記載
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(env): warning ログの文面矛盾を解消 (unsafe フラグ続行時)
`_verify_bucket_encryption` で `--unsafe-allow-unencrypted-bucket` 指定時に
「export を中止します。(unsafe フラグにより続行)」という矛盾した警告が出ていた。
問題説明 (problem) と対処案内 (guidance) を分離し、warning は problem のみ、
StorageError raise 時は問題+対処案内を出すよう統一。
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- _import_merge: 未対応 arcname を黙って捨てず MergeError で停止 (filter_members の logger.debug+continue → raise MergeError) - _import_merge: merge 経路で値が変更されていないキーは raw 行を温存し、 PATH=$HOME/bin のような未クオート値が PATH="\$HOME/bin" に勝手に エスケープされて source 時の意味が変わるのを防ぐ - cipher: age-keygen 出力の先頭コメント (# created / # public key) を 考慮し、行単位でコメント / 空行を除いて AGE-SECRET-KEY-1 行を検出 - 上記 3 件に対する回帰テストを追加 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- io_export._validate_options: DEST='-' (stdout) と --passphrase-stdin の
排他チェックを削除。stdin (passphrase) と stdout (bundle) は別ストリーム
のため衝突しない (例: `echo pass | devbase env export - --passphrase-stdin > out`)
- io_common.read_passphrase: stdin から読んだ行末を rstrip('\n') から
rstrip('\r\n') に変更。Windows/WSL 由来 CRLF パイプで末尾 \r が残ると
age 復号が無音で失敗するため。
- tests: 排他テストを併用許可テストに置換 + CRLF rstrip 回帰テスト追加
Refs: #13 (review)
- _import_merge.py の project 名正規表現を厳格化し、`./..`/隠しディレクトリを 弾く (path traversal: `env/projects/./.env` が `$DEVBASE_ROOT/projects/.env` に解決される問題への対策)。 - 上記の回帰テストを `tests/cli/test_env_import.py` に追加。 - zsh/bash 補完に `--unsafe-allow-unencrypted-bucket` (env export) を追加し CLI 定義と同期。
- cipher.py: `@PATH` ファイルに複数行の鍵が含まれる場合は明示的に CipherError を投げる。team_keys.txt のような複数公開鍵列挙を 暗黙に「最初の 1 人」だけ扱う挙動は、チーム運用で暗号化バンドル が壊れる原因になるため誤運用を防ぐ - io_common.py: `resolve_identity_specs` で `~/.ssh/id_ed25519` と `~/.ssh/id_rsa` の両方が存在する場合、両方を返すように変更。 `pyrage.decrypt` は複数 identity を受け付け、バンドルに合致する 鍵だけが使われる。これにより RSA で暗号化されたバンドルを ed25519 鍵だけで開けず失敗する問題を解消 - resolve_recipient_specs は意図的に最初の 1 つを返す挙動を維持 (どの鍵で暗号化するか一意に決める必要があるため) - 複数鍵ファイル拒否と複数 identity 復号の回帰テストを追加
- bundle.is_valid_project_name() を導入し、export 側 (make_entries_from_disk) でも import 側 (_PROJECT_ENV_RE) と同じ project 名 validator を適用する。空白 / 先頭 `.` / `-` 等を含む ディレクトリは警告のみで skip し、round-trip 不能な bundle が 作られるのを防ぐ (codex round 5 指摘)。 - _import_merge._PROJECT_ENV_RE を bundle._VALID_PROJECT_NAME_RE から組み立てるよう変更し、import / export 両側の validator を 契約レベルで同期させる。 - S3Backend._verify_bucket_encryption で NoSuchBucket / 認証・接続 系エラー (code が取れないケース) は --unsafe-allow-unencrypted-bucket の有無に関わらず即 StorageError を投げる。続行しても put_object が 同じエラーで再失敗するだけのため、早期にエラーを返してユーザの トラブルシューティングを助ける (gemini round 5 指摘)。 - 回帰テスト追加: - test_is_valid_project_name / test_make_entries_from_disk_skips_invalid_project_names - test_make_entries_from_disk_invalid_name_explicitly_included_is_still_skipped - test_make_entries_from_disk_validator_matches_import_side (契約同期) - test_s3_backend_rejects_no_such_bucket_even_with_unsafe_flag - test_s3_backend_rejects_auth_or_network_error_without_aws_code Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- io_export._encrypt_payload で passphrase ありかつ opts.recipients 指定時に ExportError を送出。従来は recipients=[] に上書きされて cipher.encrypt 側の同時指定チェックを silently バイパスし、ユーザ が明示した --recipient が無視されていた (gemini round 6 指摘)。 - docs/user/env-export-import.md L201 の制約説明を修正。round 2 で export 側の DEST='-' x --passphrase-stdin 排他チェックを撤廃した ため、SOURCE='-' (import) のみ併用不可、export は併用可能であること を明記 (codex round 6 指摘)。 - recipient + passphrase 併用拒否の回帰テストを追加。 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | codex | COMMENT
manifest version の受け入れ条件と import identity help の実挙動不一致を修正してください。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | gemini | APPROVE
S3 対応および export/import リファクタリングの実装とテストを確認しました。特に修正すべき指摘事項はありません。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 1 | gemini | REQUEST_CHANGES
devbase env import において、--passphrase-* と --identity を同時指定した際のエラーチェックが漏れており、ユーザーの意図に反して identity が暗黙的に無視される状態になっています。
devbase env export 側で行われた修正と非対称になっているため、io_import.py 側にも同様の排他チェックを追加してください。
- bundle.py: manifest version チェックを厳密一致 (!=) に変更し、 0 や負数の未知スキーマを拒否 (codex major 指摘) - cli.py: import --identity の help を "(all existing ones)" に修正 (codex minor 指摘、resolve_identity_specs は全鍵を返す) - io_import.py: --identity と --passphrase-* の同時指定を _validate_options で拒否 (gemini major 指摘) - テスト追加: version=0/-1 の拒否、identity+passphrase 排他チェック Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🔧 /ndf:fix サマリ (round 1)対応件数: critical=0 / major=2 / minor=1 (合計 3 件) 修正内容
CIRuff lint FAILURE は認証エラー (GitHub Actions checkout の |
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 2 | codex | APPROVE
修正必須の指摘はありません。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 2 | gemini | REQUEST_CHANGES
io_export.py のオプション排他チェックが遅延しているため、io_import.py と同じく fail-fast に倒す修正を提案します。
[major / 堅牢性] lib/devbase/env/io_export.py のオプション排他チェックを _validate_options で早期に行うべき
PR #22 の round1 対応において io_import.py では --identity と --passphrase-* の併用チェックが _validate_options に移動し fail-fast になりましたが、io_export.py の --recipient と --passphrase-* の併用チェックは依然として _encrypt_payload 内に残っています。
_encrypt_payload は全 .env ファイルのディスク読み取りと tar.gz 圧縮 (_bundle.pack) が終わった後に呼ばれるため、オプションの矛盾によるエラーとしては遅すぎます。無駄なファイル I/O を防ぐため、io_import.py と同様に io_export.py でも _validate_options の中で排他チェックを行ってください (--force-unencrypted と鍵指定の併用チェックも併せて _validate_options に移動できます)。
- --recipient と --passphrase-* の排他チェックを _encrypt_payload から _validate_options へ移動 (ディスク I/O 前に弾く) - --force-unencrypted と鍵指定の排他チェックを export() から _validate_options へ移動 - io_import._validate_options と対称的な構造に統一 - _validate_options 直接テスト 5 件追加 PR #22 round2 gemini [major / 堅牢性] 指摘対応 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🔧 fix | round 2 | gemini 指摘対応 | b81aae4対応内容
詳細[major / 堅牢性] gemini の指摘通り、 修正:
deferred (--defer-nit)なし (nit 指摘なし) rejectedなし (指摘は妥当) |
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 3 | gemini | APPROVE
PRの変更内容全体を確認しました。
_import_merge.pyでの.envフォーマット保持(コメント・空行の温存、正規表現を用いたエスケープの完全な可逆変換)_import_atomic.pyにおけるos.replaceとロールバック時の厳密なクリーンアップ処理storage.pyにおけるbotocore例外とローカル例外の分別判定
エッジケースが全て適切にカバーされており、非常に堅牢な実装となっています。
修正すべき点はありません。このままマージして問題ありません。
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 3 | codex | REQUEST_CHANGES
バックアップ GC と既定 export ファイル名に、ユーザーデータを無言で削除・上書きし得る経路があります。
PR #22 round3 codex 指摘対応: 1. [major] _import_atomic.py: backup ディレクトリ名に `dbenv-` prefix を付与し、 --backup-dir 親に無関係なタイムスタンプ形式ディレクトリがあっても GC で rmtree しないようにした。旧フォーマット (prefix なし) は後方互換で GC 対象。 2. [minor] io_export.py: 既定出力名を秒精度から microsecond 精度に変更し、 同一秒の複数 export による無言上書きを防止。加えて既定名使用時に同名 ファイルが既に存在する場合は ExportError で失敗させる。 テスト追加: - test_gc_backups_ignores_bare_timestamp_dirs_from_other_tools - test_default_dest_includes_microsecond - test_export_default_dest_rejects_existing_file Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Round 3/5 修正サマリー (codex review)commit: a751323 対応した指摘 (2件)
deferred / rejected
テスト
CI
|
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 4 | gemini | REQUEST_CHANGES
io_export.py における opts.dest の空文字ハンドリングについて 1 件の修正をリクエストします。
`opts.dest` が `""` (空文字) の場合、`opts.dest or _default_dest(...)` で 既定名に置換されるが、`if opts.dest is None` チェックが `False` となり 同名ファイル存在時の ExportError ガードがバイパスされていた。 `if not opts.dest` に修正し、空文字も None と同等に扱うようにした。 テスト `test_export_empty_dest_rejects_existing_file` を追加。 PR #22 round4 gemini 指摘対応 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR #22 Round 5 修正サマリ対応した指摘 (1件)
修正内容
CI
deferred / rejected
commit: |
takemi-ohama
left a comment
There was a problem hiding this comment.
🤖 cross-review | round 5 | gemini | APPROVE
修正を要する指摘事項はありません。仕様通り実装されています。
概要
devbase env export/devbase env importコマンドを新設し、devbase 管理下の全.envファイルを 1 つのバンドルファイルにまとめて退避・復元できるようにする。設計・仕様は
issues/PLAN03-1.mdに記載。関連 Issue
変更点
PR1: devbase env export (Local + Stdio) (#14)
lib/devbase/env/bundle.py— バンドル構築・展開・manifest 生成・sha256 検証lib/devbase/env/cipher.py— age 暗号化/復号 (pyrage)lib/devbase/env/storage.py— Local / Stdio バックエンドlib/devbase/env/io_export.py— export 高レベル実装lib/devbase/commands/env.py—devbase env exportサブコマンド登録lib/devbase/cli.py— env export/import パーサ追加PR2: devbase env import (#15)
lib/devbase/env/io_import.py— import オーケストレーションlib/devbase/env/_import_merge.py— merge 計画 (keep-existing / prefer-incoming / replace-keys)lib/devbase/env/_import_atomic.py— 2 フェーズ atomic 書き込み + backup GC--dry-run,--replace,--keep-last N対応PR3: S3 backend (#19)
lib/devbase/env/storage.pyに S3Backend 追加GetBucketEncryption事前確認--unsafe-allow-unencrypted-bucketフラグboto3を main dependency として追加PR5: ドキュメント + リファクタ (#20)
docs/user/env-export-import.md新設 (456 行)io_common.pyに集約io_import.pyを 3 モジュールに分割 (711行 → orchestration + merge + atomic)その他
動作確認
./bin/devbase --helpが正常に動作するdevbase env export/devbase env importのラウンドトリップを確認--dry-run,--merge,--replace,--force-unencrypted各オプション確認docs/,README.md) を更新したtests/env/,tests/cli/test_env_export.py,tests/cli/test_env_import.py)補足