Commit e043df2
release: PLAN03-1 devbase env export / import (#22)
* chore(PLAN03-1): release ブランチ作成
* feat(env): PLAN03-1 PR1 devbase env export (Local + Stdio) (#14)
* 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>
* feat(env): PLAN03-1 PR2 devbase env import (#15)
* 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>
* feat(env): PLAN03-1 PR3 devbase env export/import S3 backend (#19)
* 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>
* feat(env): PLAN03-1 PR5 ドキュメント追加 + import/export リファクタ (#20)
* 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>
* fix(env): PR #13 round1 codex/gemini 指摘対応
- _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>
* fix(env): PR #13 round2 gemini 指摘対応
- 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)
* fix(env): PR #13 round3 codex 指摘対応
- _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 定義と同期。
* fix(env): PR #13 round4 gemini 指摘対応
- 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 復号の回帰テストを追加
* fix(env): PR #13 round5 codex/gemini 指摘対応
- 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>
* chore: 不要な migrate_ai_to_home.sh を削除
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(env): recipient と passphrase 同時指定を拒否 + docs 表現修正
- 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>
* fix(env): PR #22 round1 codex/gemini 指摘対応
- 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>
* fix(env): export オプション排他チェックを _validate_options に集約 (fail-fast)
- --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(env): backup GC に dbenv- prefix で安全弁 + export 既定名を microsecond 精度に
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>
* fix(env): opts.dest 空文字で既定名ガードがバイパスされる問題を修正
`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>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent 25c30e4 commit e043df2
31 files changed
Lines changed: 5876 additions & 121 deletions
File tree
- bin
- docs/user
- etc
- lib/devbase
- commands
- env
- tests
- cli
- env
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2 | 2 | | |
3 | 3 | | |
4 | 4 | | |
| 5 | + | |
5 | 6 | | |
6 | 7 | | |
7 | 8 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
7 | 23 | | |
8 | 24 | | |
9 | 25 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
76 | 76 | | |
77 | 77 | | |
78 | 78 | | |
79 | | - | |
| 79 | + | |
80 | 80 | | |
81 | 81 | | |
82 | 82 | | |
| |||
106 | 106 | | |
107 | 107 | | |
108 | 108 | | |
| 109 | + | |
109 | 110 | | |
110 | 111 | | |
111 | 112 | | |
| |||
This file was deleted.
0 commit comments