From a3aeafebc4d7abf0fb737dbcff23dcbe4fe3225d Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 11:38:10 +0900 Subject: [PATCH 1/3] =?UTF-8?q?docs(PLAN65):=20devbase=20scale=20=E3=81=AE?= =?UTF-8?q?=20Compose=20=E5=91=BC=E3=81=B3=E5=87=BA=E3=81=97=E3=82=92?= =?UTF-8?q?=E5=85=B1=E9=80=9A=E7=B5=8C=E8=B7=AF=E3=81=B8=E5=AF=84=E3=81=9B?= =?UTF-8?q?=E3=82=8B=E8=A6=81=E6=B1=82=E4=BB=95=E6=A7=98=E3=81=A8=E8=A8=AD?= =?UTF-8?q?=E8=A8=88=20(#192)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cmd_scale が docker compose を共通経路 (utils/docker.py の docker_compose) を 通さずに呼ぶため、compose_env() が適用されない。確定仕様 docs/specifications/compose-profiles.md は「COMPOSE_PROFILES を端末や .env に 置いても devbase 経由の操作には効かない」と約束する一方で、同じ文書の中で cmd_scale をその対象から外している。 この食い違いを、約束の側を正として解く設計を置く。実測で cmd_login にも同じ穴が あることが分かったため、そちらも同じ Pull Request で塞ぐ決定にした。 実装は 2 本の Pull Request に分ける (振る舞いと仕様 / 構造)。順序は 仕様 → 共通経路へ寄せる → 現状固定テストを足す → 関数を分ける。 Co-Authored-By: Claude Opus 5 (1M context) --- issues/PLAN65_scale-compose-path-design.md | 488 +++++++++++++++++++++ issues/PLAN65_scale-compose-path.md | 276 ++++++++++++ 2 files changed, 764 insertions(+) create mode 100644 issues/PLAN65_scale-compose-path-design.md create mode 100644 issues/PLAN65_scale-compose-path.md diff --git a/issues/PLAN65_scale-compose-path-design.md b/issues/PLAN65_scale-compose-path-design.md new file mode 100644 index 00000000..0c6bc96d --- /dev/null +++ b/issues/PLAN65_scale-compose-path-design.md @@ -0,0 +1,488 @@ +# PLAN65: `devbase scale` の Compose 呼び出しを共通経路へ寄せ、`cmd_scale` の段階を分ける の設計 + +要求と受け入れ条件は `issues/PLAN65_scale-compose-path.md` にある。この文書は「どう作るか」 +だけを扱う。 + +対象 issue: devbasex/devbase#192 / base は `release/v3.7.0`(release Pull Request は #212) + +## 機能一覧 + +| # | 機能 | 誰が使うか | +| --- | --- | --- | +| 1 | `devbase scale` の起動が共通経路(`docker_compose`)を通り、子プロセスの `COMPOSE_PROFILES` が打ち消される | devbase の利用者 | +| 2 | `devbase scale` の起動の対象が既定のサービスに限られる | devbase の利用者 | +| 3 | `devbase login` の `exec` も共通経路の規則に従う | devbase の利用者 | +| 4 | `cmd_scale` の段階に名前が付き、`cmd_up` と同じ形で読める | devbase を保守する側 | +| 5 | `docker compose config --format json` を起動する箇所が 1 つになる | devbase を保守する側 | +| 6 | `devbase scale` の正常系の手順がテストで固定される | devbase を保守する側 | +| 7 | 確定仕様の「devbase 経由の操作には効かない」が例外を持たない | 仕様を読む側 | + +## 構成要素 + +| 要素 | 責務 | 変更 | +| --- | --- | --- | +| `docker_compose`(`utils/docker.py`) | `docker compose` を `env=compose_env()` で起動する唯一の共通経路 | 変えない(呼び出し元が増えるだけ) | +| `compose_env`(`utils/docker.py`) | 子プロセスの `COMPOSE_PROFILES` を `__devbase_none__` にする | 変えない | +| `default_services`(`commands/container.py`) | 生成物から `profiles:` を持たないサービス名を求める | 変えない(`cmd_scale` から呼ぶようになる) | +| `_compose_lines`(`commands/container.py`) | `config --services` / `--profiles` を読む。非 0 は `DevbaseError` | 変えない(`default_services` の下位) | +| `_compose_run`(`commands/container.py`) | `devbase ps` / `devbase logs` の起動 | 変えない(確定仕様の経路の表に並ぶため図に載せる) | +| `_ensure_images`(`commands/container.py`) | 起動前のイメージの確認 | 呼ぶ関数の名前だけ変わる(`_read_compose_services` → `_compose_config_services`) | +| `cmd_scale`(`commands/container.py`) | 前提の検査・段階の呼び出し・後処理・終了コードの決定 | **本体を 86 行から 40 行以下へ縮める** | +| `_check_scale_request`(新設) | `new_scale` の妥当性(1 未満・現在以下)の判定と案内のログ | **新設** | +| `_run_scale_pipeline`(新設) | `[1/5]`〜`[5/5]`。`project.yml` の書き換え・ボリューム・network・構成生成・既定のサービスの解決・`--no-recreate` の起動・ready 待ち | **新設**(`cmd_up` の `_run_deploy_pipeline` と対称) | +| `_compose_config_services`(新設) | `config --format json` の(終了コード, `services`)を返す唯一の関数 | **新設。`_read_compose_services` を置き換える** | +| `_resolve_dev_service` | dev サービス定義を返す。失敗は `None` | **本文を `_compose_config_services` の上へ載せ替える。名前と契約は変えない** | +| `cmd_login` | `docker compose exec - bash` | **`env=compose_env()` を渡す(1 行)** | +| `docs/specifications/compose-profiles.md` | 経路・コマンド列・運用・テスト観点の確定仕様 | **`scale` と `login` を対象に含める形へ書き換える** | +| `tests/commands/test_container_scale_order.py`(新設) | `devbase scale` の正常系の手順と子プロセスの環境の固定 | **新設** | +| `tests/utils/test_docker_profiles.py` | 経路ごとに `COMPOSE_PROFILES` の打ち消しを固定する | **棚卸しの一覧とコメントを更新する** | + +## 経路の図 + +図は実行時の呼び出しだけを描く。**上の表の要素のうち 4 つは図に現れない。** + +| 図に現れない要素 | 理由 | +| --- | --- | +| `_compose_run` | この束では触らず、確定仕様の経路の表に並ぶだけである | +| `docs/specifications/compose-profiles.md` | 実行時の呼び出しを持たない | +| `tests/commands/test_container_scale_order.py` | 同じ | +| `tests/utils/test_docker_profiles.py` | 同じ | + +`devbase scale` の経路: + +```mermaid +graph TD + SCALE[cmd_scale] --> CSR[_check_scale_request] + SCALE --> RSP[_run_scale_pipeline] + RSP --> DS[default_services] + RSP --> DC[docker_compose] + DS --> CL[_compose_lines] + CL --> CE[compose_env] + DC --> CE + CE --> COMPOSE[(docker compose)] +``` + +`config --format json` の読み取りと `devbase login` の経路: + +```mermaid +graph TD + RDS[_resolve_dev_service] --> CCS[_compose_config_services] + EI[_ensure_images] --> CCS + CCS --> DC2[docker_compose] + LOGIN[cmd_login] --> CE2[compose_env] + DC2 --> CE2 + CE2 --> COMPOSE2[(docker compose)] +``` + +## 構造 + +処理の順序を変える(1 つの関数の通しの流れを 2 つの関数へ分ける)ため、変更後の呼び出しの +並びを示す。型(クラス)は追加しない。モジュール関数の並びで構成する。 + +### `cmd_up` と `cmd_scale` の段階の対応(変更後) + +| 段階 | `cmd_up` 側 | 段階 | `cmd_scale` 側 | +| --- | --- | --- | --- | +| 前提の検査 | `_run_pre_up_checks`(group / env / pre-up / images) | 前提の検査 | `_check_group_consistency` と `_check_scale_request`(**新設**) | +| — | (`cmd_up` 本体で `_auto_snapshot`) | — | 行わない(`scale` は退避を取らない) | +| — | — | `[1/5]` | `project_runtime.write_scale` | +| `[1/6]` | `ensure_volumes` | `[2/5]` | `ensure_volumes` | +| `[1.5/6]` | `ensure_network` | `[2.5/5]` | `ensure_network` | +| `[2/6]` | `_build_scaled_override`(`_previous_scale_compose` の中) | `[3/5]` | `_build_scaled_override`(退避は取らない) | +| (2 と 3 の間) | `default_services(override_file)` | (3 と 4 の間) | `default_services(override_file)`(**新設**) | +| `[3/6]` | `docker_compose_down` | — | 行わない(既存を止めないのが `scale` の趣旨) | +| `[4/6]` | `docker_compose_up(services=...)` | `[4/5]` | `docker_compose(['up', '-d', '--no-recreate', *services])`(**変更**) | +| `[5/6]` | `wait_for_containers_ready` | `[5/5]` | `wait_for_containers_ready` | +| 後処理 | `_report_missing_repos` → `./deploy` → `_push_bao_token` → `_apply_window_titles` → `_maybe_open_editor` | 後処理 | `_push_bao_token(start=current+1)` → `./deploy`(範囲は `current+1..new`)。変えない | + +`[1/5]`〜`[5/5]` と `[2.5/5]` の文字列は変えない(決定 7)。後処理の順序(bao → deploy)も +`cmd_up` と逆のまま変えない。`cmd_scale` が `_report_missing_repos` / `_apply_window_titles` / +`_maybe_open_editor` を呼ばないことも変えない(範囲外。#224 として起票済み)。 + +## 入出力の契約: 組み立てるコマンド列 + +### `devbase scale` が組み立てるコマンド列 + +変更前: + +``` +docker compose -f <生成物> up -d --no-recreate +env: 呼び出し元の os.environ そのまま(COMPOSE_PROFILES は利用者と .env の値) +``` + +変更後: + +``` +docker compose -f <生成物> up -d --no-recreate <既定のサービス...> +env: compose_env()(COMPOSE_PROFILES=__devbase_none__、ほかは os.environ の複製) +``` + +`<生成物>` は `_build_scaled_override` が返したパス。`<既定のサービス...>` は +`default_services(<生成物>)` が返した一覧の全件で、並びは変えない。`--profile` は付けない。 + +### `devbase login` が組み立てるコマンド列 + +コマンド列は変えない。`env` だけを `compose_env()` にする。 + +``` +docker compose [-f <生成物>] exec <開発サービス名>- bash # 生成物がある場合 +docker compose exec --index= <開発サービス名> bash # 生成物が無い場合 +``` + +## 入出力の契約: 新設・変更する関数のシグネチャ + +```python +def _check_scale_request(new_scale: int, current_scale: int) -> bool: + """``new_scale`` が受け付けられるかを判定し、受け付けないときは案内を出す。 + + ``new_scale < 1`` と ``new_scale <= current_scale`` の 2 つで False を返す。 + ログの文言と出し分け (error / warning + info 2 行) は変更前のまま。 + """ + + +def _run_scale_pipeline(project_name: str, new_scale: int, current_scale: int, + config, target: docker_context.DockerTarget, + dev_service_name: str) -> Optional[Path]: + """``[1/5]``〜``[5/5]`` の本体。生成した override compose のパスを返す。 + + 起動が 0 以外で終わったときだけ ``None`` を返す (error ログ + ``Failed to start new containers`` はこの関数が出す)。それ以外の失敗は + ``DevbaseError`` / ``DockerError`` のまま呼び出し元へ伝播する。 + """ + + +def _compose_config_services() -> tuple[int, dict]: + """``docker compose config --format json`` の (終了コード, services) を返す。 + + 非 0 なら ``services`` は空の辞書。JSON として読めなければ + ``json.JSONDecodeError`` を伝播する。``_read_compose_services`` の契約と同じで、 + 実行は共通経路 ``docker_compose`` を通る。 + """ + + +def _resolve_dev_service() -> Optional[dict]: + """compose config から dev サービス定義を取得する。失敗時は None。 + + 名前・引数・戻り値の契約は変えない (終了コードが非 0、JSON として読めない、 + のどちらでも ``None``)。 + """ +``` + +`_read_compose_services` は削除する。唯一の呼び出し元 `_ensure_images`(`container.py:2019`)は +`_compose_config_services` を呼ぶ。`_resolve_dev_service` の名前を残すのは、 +`tests/cli/test_base_image_staleness.py` の 3 か所がこの名前を差し替えているためである。 + +## 確定仕様 `docs/specifications/compose-profiles.md` の書き換え + +| 節(変更前の行) | 何をするか | +| --- | --- | +| 構成要素の表(38-40 付近の `devbase up` の起動の行) | `devbase scale` の起動も `docker_compose` を通ることを書く | +| 有効なプロファイルの決め方・経路の表(74-81) | 行を 5 つにする。`docker_compose` の用途へ `scale` の `up -d --no-recreate` を足し、`_resolve_dev_service` / `_read_compose_services` の 2 行を `docker_compose` の用途へ畳み、`cmd_login` の `exec` の行を足す | +| 同じ節(83-84) | 「`cmd_scale` が直接呼ぶ …… この対象に含めない」を削り、「devbase が Compose を起動する経路はこの表の 5 つだけである」に置き換える | +| 組み立てるコマンド列の表(107-120) | `devbase scale` の起動の行(`up -d --no-recreate <既定のサービス...>`)と `devbase login` の行(`exec <開発サービス名>- bash`)を足す | +| `devbase up` と `devbase down` の節(122 以降) | `devbase scale` の段落を足す。生成物を作った直後に `default_services` を求め、停止の段を持たないこと、既に動いているプロファイルのサービスを止めないことを書く | +| 運用(386-389) | 「`devbase scale` はプロファイルのサービスを複製しない」に「起動の対象にも入れない。既に動いているプロファイルのサービスは止めない」を足す。388-389 の約束はそのまま残す(成り立つようになる) | +| テスト観点(441-445 付近) | `devbase scale` の行を足す(コマンド列・子プロセスの環境・手順の順序) | + +`docs/plugin-dev/compose-profiles.md:82` は変えない。共通経路へ寄せれば約束が成り立つため +である。 + +## 処理の流れ + +```mermaid +graph TD + A[devbase scale N] --> B{_check_group_consistency} + B -->|不一致| Z1[1 を返す] + B -->|一致| C{_check_scale_request} + C -->|受け付けない| Z2[1 を返す] + C -->|受け付ける| D["[1/5] write_scale"] + D --> E["[2/5] ensure_volumes
[2.5/5] ensure_network"] + E --> F["[3/5] _build_scaled_override"] + F --> G["default_services(生成物)"] + G --> H["[4/5] docker_compose
up -d --no-recreate 既定のサービス..."] + H -->|非 0| Z3["Failed to start new containers
1 を返す"] + H -->|0| I["[5/5] wait_for_containers_ready"] + I --> J["_push_bao_token(start=current+1)"] + J --> K["./deploy (current+1..N)"] + K --> L[0 を返す] +``` + +`[1/5]` から `[5/5]` までが `_run_scale_pipeline` の中にある。`_push_bao_token` 以降は +`cmd_scale` の本体に残る(決定 8)。 + +## 失敗の経路 + +失敗の経路は 4 つで、いずれも終了コード 1 になる。 + +| 失敗 | どこで | 見え方 | +| --- | --- | --- | +| グループの不一致 | `_check_group_consistency` | 変更前のまま。`project.yml` は書き換えない | +| `new_scale` が不適 | `_check_scale_request` | 変更前のまま。`project.yml` は書き換えない | +| 構成生成・既定のサービスの解決・ready 待ちの失敗 | `_run_scale_pipeline` の中で `DevbaseError` / `DockerError` | `cmd_scale` の `except DevbaseError` が `Scale failed: ...` を出す。`project.yml` は**既に書き換わっている**(変更前も同じ) | +| 起動が 0 以外 | `_run_scale_pipeline` が `None` を返す | `Failed to start new containers` を出して 1。変更前のまま | + +**既定のサービスの解決を足すと、失敗の経路が 1 つ増える。** `default_services` は +`_compose_lines` を通り、非 0 の終了を `DevbaseError` にする。`cmd_up` は同じ解決を既に +行っており、`scale` は `up` の後にしか使えない(生成物を作り直すのも同じ関数である)ため、 +`up` が通るプロジェクトでこの解決だけが失敗する経路は無い。 + +## 決定の記録 + +### 決定 1: `devbase scale` の Compose 呼び出しを共通経路へ寄せる(issue #192 の案 A) + +`docs/specifications/compose-profiles.md` の 2 つの記述のうち、**388-389 行目の約束を正とする。** +その約束は「`COMPOSE_PROFILES` を端末や `.env` に置いても devbase 経由の操作には効かない」で +ある。**83-84 行目の `cmd_scale` の除外は削る。** + +理由: + +- **約束の側が、より外に向いている。** 388-389 は「運用」の節にあり、利用者が読んで自分の + 端末と `.env` の扱いを決めるための文である。同じ約束は利用者向けの + `docs/plugin-dev/compose-profiles.md:82` にもある。83-84 は「有効なプロファイルの決め方」の + 節にある実装の内訳で、読者は devbase を保守する側である。**外向きの約束に例外を足すほうが、 + 内向きの内訳を 1 行足すより高くつく** +- **除外の理由が成り立っていない。** 83-84 は「プロファイルの入口ではないためである」と書くが、 + 打ち消しが要るのは入口だからではなく、**子プロセスへ利用者の値がそのまま渡るから**である。 + `cmd_scale` はサービスの指定も持たないため、プロファイルのサービスが起動の対象に入りうる。 + 同じ理由で `_resolve_dev_service` / `_read_compose_services`(読み取りだけで、入口ではない)も + 対象に入っている。除外の基準は既に一貫していない +- **仕様へ例外を書く形は、3 か所へ例外を足すことになる。** 確定仕様の 2 か所(経路の表・運用)と利用者向けの文書 1 か所に + 「ただし `devbase scale` は除く」を足すことになる。`devbase up` と `devbase scale` で + `COMPOSE_PROFILES` の効き方が違う状態を、覚える対象として利用者へ渡す +- **仕様へ例外を書く形では、構造の側の目的も達しない。** #192 は「関数を分割しても共通経路を + 通さなければ、同じ食い違いが残る」と書いている。この形を採ると `cmd_scale` だけが自前で + `subprocess.run` を + 持つ形が残り、`up` の起動と `scale` の起動が別の規則で動く状態が確定仕様として固定される + +採らなかった案: + +| 採らなかった形 | 内容 | 退けた理由 | +| --- | --- | --- | +| 仕様へ例外を書く(issue #192 の案 B) | 「devbase 経由の操作には効かない」を `scale` 除外込みへ書き直す | 上の 4 点。とくに、利用者向けの約束に例外が増える | +| `scale` にプロファイルの口を足す | `devbase scale` へ `--profile` を受ける引数を足し、プロファイルも複製できるようにする | 確定仕様 141・386-387 が「プロファイルのサービスは scale の対象にせず、複製されるのは開発サービスだけ」と決めている。この決定を変える要求は #192 に無い | +| 環境だけを渡す | `compose_env()` だけを渡し、起動の対象は明示しない | 決定 4 で退けた | + +### 決定 2: `cmd_login` の穴も同じ Pull Request で塞ぐ + +実測で、`compose_env()` を渡していない箇所は `cmd_scale`(1648)だけでなく +**`cmd_login`(1413)も**だった(要求仕様の実測の表の 4 と 5)。`cmd_login` も塞ぐ。 + +理由: + +- **決定 1 は「経路の表が devbase の Compose の起動を網羅している」ことを仕様として言い直す + 決定である。** 網羅していない状態のまま表を書き直すと、同じ食い違いを別の行で作る +- **観測できる振る舞いは変わらない。** `docker compose exec <サービス> bash` は既に動いている + コンテナを名指しする。開発サービスは `profiles:` を持たない既定のサービスなので、 + `COMPOSE_PROFILES` の値で選ばれ方が変わらない。差分は 1 行(`env=compose_env()`)で、 + `exec` の中で動く `bash` の環境はコンテナ側から来るため影響を受けない +- **棚卸しのコメントが実装と合っていない。** `tests/utils/test_docker_profiles.py:102` は + 「決定 7 の棚卸しの 4 か所」と書き、`cmd_scale` と `cmd_login` を数えていない。片方だけ直すと + コメントを 5 か所へ直すことになり、次に読む人が残りの 1 つを穴と気づけない + +採らなかった案: `cmd_login` を別の課題として起票し、この束では触らない。退けたのは、 +経路の表を書き直す作業がこの束にあるためである(表に「`cmd_login` は未対応」と書くか、 +誤った表を残すかのどちらかになる)。ただし**この 1 行は独立して切り出せる**ので、レビューで +範囲外と判断されたら分ける(決定 10 の Pull Request 1 の中で独立したコミットにする)。 + +### 決定 3: `docker_compose_up()` は拡張せず、`docker_compose()` を直接呼ぶ + +`cmd_scale` の起動を次の形にする。 + +```python +docker_compose(['up', '-d', '--no-recreate', *services], + compose_file=override_file, check=False) +``` + +理由: + +- **`docker_compose_up()` は `--no-recreate` を持たず、`check=True` 固定である。** + `no_recreate: bool = False` と `check` を足すと、引数が 2 つ増える。`utils/docker.py` の + 関数が `scale` の事情を知ることにもなる +- **`check=False` を保つ必要がある。** 変更前の `cmd_scale` は終了コードを見て + `Failed to start new containers` を出す。`docker_compose_up()` を使うと + `subprocess.CalledProcessError` が飛ぶ。これは `cmd_scale` の `except DevbaseError` を + 素通りし、traceback で落ちる。`cmd_up` は `except subprocess.CalledProcessError` も持つが、 + `cmd_scale` は持たない。**共通経路へ寄せる実装を素直に書くと、ここを踏む** +- `docker_compose()` は `env=compose_env()` を渡す唯一の共通経路であり、目的(決定 1)は + これを通すことで達する。`profile up` / `profile down` / `profile list` も同じく + `docker_compose()` を直接呼んでいる(`container.py:1503` / `1531` / `1547`) + +採らなかった案: + +| 案 | 退けた理由 | +| --- | --- | +| `docker_compose_up()` に `no_recreate` と `check` を足す | 上の 1 つ目と 2 つ目。`tests/utils/test_docker_profiles.py:85-99` が固定している `docker_compose_up` の契約も広がる | +| `cmd_scale` に `except subprocess.CalledProcessError` を足して `docker_compose_up()` を使う | ログが `Failed to start new containers` から変わるか、2 か所で同じ文言を持つことになる。終了コードを見るほうが差分が小さい | + +### 決定 4: 起動の対象は `default_services(<生成物>)` で明示する + +`compose_env()` を渡すだけにせず、`up` と同じく既定のサービス名を全件並べる。 + +理由: + +- **確定仕様が `up` で明示する理由が、`scale` にそのまま当たる。** 確定仕様 136-138 行目は + 「起動の対象を明示するのは、打ち消し用のプロファイル名が効かない形でプロファイルが有効に + なっても、一覧に無いサービスを起動しないため」と書いている。`scale` の起動も同じ生成物に + 対する `up` である +- **`up` と `scale` で対象の決め方が違う状態を残さない。** #192 の「`scale` の手順を変えるときに + `cmd_up` と `cmd_scale` の片方だけを直す食い違いが起きやすい」は、まさにこの形である +- **プロファイルを持たないプロジェクトでは集合が変わらない。** `default_services` は + `profiles:` を持たないサービスの全件で、サービス名を付けない `up` の対象と同じである + (前提 6 / 受け入れ条件 B-5・E-1) + +採らなかった形(環境だけを渡す): `compose_env()` だけを渡し、サービス名は付けない。差分は最小で、 +失敗の経路も増えない。退けたのは上の 1 つ目と 2 つ目で、とくに「`up` と `scale` の規則が違う」 +状態が残ることを避けた。**増える失敗の経路は「処理の流れ」の節で見積もっており、`up` が通る +プロジェクトでは踏まない。** + +### 決定 5: 失敗の扱いとログの文言は変えない + +次の文言と、その出し分けを変えない。`cmd_scale` に `except subprocess.CalledProcessError` を +足さない。 + +``` +[1/5] ... [5/5] +Using --no-recreate to avoid restarting existing containers... +Failed to start new containers +Scale failed: %s +=== Scale completed successfully === +``` + +理由: 振る舞いの変更を「子プロセスの環境」と「起動の対象」の 2 点だけに絞る。ログを同時に +変えると、現状固定テストが何を守っているのかが読めなくなる。 + +### 決定 6: `_previous_scale_compose()` は使わない + +`cmd_up` は生成に失敗したとき旧構成を書き戻すために `_previous_scale_compose()` を使うが、 +`cmd_scale` には入れない。 + +理由: `scale` は既存のコンテナを止めない。止める段が無いので、旧構成で停止する必要が無い。 +入れると `scale` が失敗したときに生成物だけが巻き戻り、`project.yml` の `scale` の値 +(`[1/5]` で既に書き換わっている)と食い違う。**変更前の振る舞いを保つ**。 + +### 決定 7: 段階の番号の文字列は変えない + +`[1/5]`〜`[5/5]` と `[2.5/5]` をそのまま持つ。`default_services` の呼び出しには段階の番号を +付けない(`cmd_up` も `[2/6]` と `[3/6]` の間で番号を持たない)。 + +理由: 番号を振り直すと、`cmd_scale` の出力を読んでいる人にとっての差分が増える。`[2.5/5]` の +ような中途の番号は `cmd_up` の `[1.5/6]` と同じ流儀で、この束で整えるものではない。 + +### 決定 8: 抽出は 2 つの関数に分け、`cmd_up` と対称にする + +`_check_scale_request`(前提の検査)と `_run_scale_pipeline`(`[1/5]`〜`[5/5]`)の 2 つにする。 +後処理(bao token・`./deploy`・完了のログ)は `cmd_scale` に残す。 + +理由: + +- **`cmd_up` が同じ形をしている。** `_run_pre_up_checks` と `_run_deploy_pipeline` があり、 + 後処理は `cmd_up` 本体にある。2 つのコマンドを並べて読めるようにするのが #192 の狙いである +- **`_check_group_consistency` は既に関数である。** 残る前提の検査は `new_scale < 1` と + `new_scale <= current_scale` の 2 つだけである。これを 1 つにまとめれば、`cmd_scale` の冒頭が + 「2 つの検査 → 段階 → 後処理」の 3 段に読める +- **後処理を出さない理由**: bao token と `./deploy` は `current_scale + 1` から + `new_scale` までの範囲を使う。`cmd_up` も同じものを本体に持つ。移すと 2 つのコマンドの形が + かえって離れる + +採らなかった案: + +| 案 | 退けた理由 | +| --- | --- | +| 段階ごとに 5 つの関数へ分ける | 1 行か 2 行の関数が並ぶ。`cmd_up` の形と離れ、順序の読み取りが `_run_scale_pipeline` 1 つを読むより難しくなる | +| `_run_deploy_pipeline` と `_run_scale_pipeline` を 1 つの関数へ統合し、引数で分岐させる | 停止の有無・退避の有無・`--no-recreate` の有無・段階の番号の 4 つで分岐する。分岐で分ける対象が 4 つあるものは 1 つの関数にしない | + +### 決定 9: config の読み取りは「下位の 1 関数 + 既存の名前を残した包み」に統合する + +`_compose_config_services()` を新設し、`_read_compose_services` を削除、`_resolve_dev_service` は +名前と契約を保ったまま本文を載せ替える。 + +理由: + +- **2 つの契約は違うので、1 つの関数に畳めない。** `_resolve_dev_service` は不正 JSON を + `None` に、`_read_compose_services` は `json.JSONDecodeError` の伝播にしている。どちらの + 呼び出し元も、その違いに合わせた失敗処理を持つ(`_build_resolved` は `if not dev_service:`、 + `_ensure_images` は外側の `except Exception`)。**引数で切り替える形にすると、呼び出し側が + 渡す値で失敗の形が変わる関数になる** +- **`_resolve_dev_service` の名前を残すのは、テストが差し替えているためである。** + `tests/cli/test_base_image_staleness.py:158 / 173 / 188` がこの名前を `monkeypatch.setattr` で + 差し替える。名前を変えると、この束の外の 3 つのテストを書き換えることになる +- **`_read_compose_services` の名前は残さない。** 呼び出し元が 1 つ(`_ensure_images`)で、 + 契約は `_compose_config_services` と同じである。同じ契約の名前を 2 つ持つと、次に読む人が + 違いを探す + +## 実装の分け方(決定 10): 2 本の Pull Request に分ける + +| # | 名前 | 内容 | 触るファイル | 依存 | +| --- | --- | --- | --- | --- | +| 1 | 振る舞いと仕様 | 確定仕様の書き換え(決定 1)・`cmd_scale` の起動を共通経路へ(決定 3・4)・`cmd_login` の 1 行(決定 2)・現状固定テストの新設 | `docs/specifications/compose-profiles.md`、`lib/devbase/commands/container.py`(`cmd_scale` の `[4/5]` と `cmd_login`)、`tests/commands/test_container_scale_order.py`(新設)、`tests/utils/test_docker_profiles.py` | 無し(base は `release/v3.7.0`) | +| 2 | 構造 | `cmd_scale` の段階の抽出(決定 7・8)・config 読み取りの統合(決定 9)・棚卸しのコメントの更新 | `lib/devbase/commands/container.py`(`cmd_scale` / `_resolve_dev_service` / `_read_compose_services` / `_ensure_images`)、`tests/utils/test_docker_profiles.py` | **Pull Request 1 の `:マージ` が要る** | + +依存の理由: どちらも `lib/devbase/commands/container.py` の `cmd_scale` の同じ区画を触る。 +2 を先に出すと、1 が書き換える `[4/5]` の行が別の関数へ移っており、レビューした差分と入る差分が +変わる。**`tests/utils/test_docker_profiles.py` も両方が触る**(1 は経路のテストを足し、 +2 は棚卸しのコメントと関数名を直す)。 + +#192 が指定した順序(仕様 → 共通経路 → 現状固定テスト → 関数を分ける)は、この 2 本の中で +次のように並ぶ。 + +| 順序 | どこで | 備考 | +| --- | --- | --- | +| 1. 仕様 | Pull Request 1 の 1 つ目のコミット | 決定 1 を確定仕様へ書く | +| 2. 共通経路へ寄せる | Pull Request 1 の 2 つ目のコミット | 新しいコマンド列と子プロセスの環境を固定するテストを先に書いて落とす(`tdd-cycle`)。**構造を触らないので、この時点では「気づけない」対象が無い** | +| 3. 現状固定テストを足す | Pull Request 1 の 3 つ目のコミット | 正常系の手順(順序・範囲)を固定する。**構造を変える前に緑にする** | +| 4. 関数を分ける | Pull Request 2 | 3 のテストを書き換えずに緑のまま通す。書き換えが要るなら振る舞いが変わっている | + +**3 を 2 より先に置かない理由**: 2 はコマンド列と子プロセスの環境を意図して変える。先に +現状(`env` 無し・サービス名無し)を固定すると、2 でそのテストを書き換えることになり、 +「固定したものを自分で書き換えた」記録が残る。2 が変えるのは 1 行の呼び出しで、構造は +触らないため、固定が無くても差分を目で追える。**構造を変える 4 の前には、3 が緑で入っている。** + +採らなかった案: + +| 案 | 退けた理由 | +| --- | --- | +| 1 本にまとめる | 振る舞いの変更と構造の変更が同じ差分に混ざる。レビューで「この行はどちらの目的か」が読めない | +| 3 本に分ける(仕様 / 振る舞い / 構造) | 確定仕様の 1 節と実装の 1 行は同じ約束の裏表で、別々にマージすると仕様と実装が食い違う版が中間に残る | +| 現状固定テストだけを先に 1 本出す | 上の「3 を 2 より先に置かない理由」と同じ | + +## テスト設計 + +新設するのは `tests/commands/test_container_scale_order.py` の 1 ファイルである。既存の流儀に +合わせる。`container` モジュールの属性を `monkeypatch.setattr` で差し替え、共有の `calls` へ +追記させて最後に並びを比べる。`tests/commands/test_container_up_order.py:46-92` の +`up_harness` と同型である。コマンド列と子プロセスの `env` は +`tests/utils/test_docker_profiles.py:19-30` の `FakeRun` を流用して拾う。 + +**実 docker と実 `DEVBASE_ROOT` には触れない。** `DEVBASE_ROOT` は各テストが自分で +`monkeypatch.setenv` する(要求仕様の前提 4)。 + +| 受け入れ条件 | 何で確かめるか | +| --- | --- | +| A-1 | `grep -rn "'docker', 'compose'\|\"docker\", \"compose\"" lib/` が 3 件。残った 3 件を 1 つずつ辿る | +| A-2 / A-3 | `grep -n "cmd_scale" docs/specifications/compose-profiles.md` と、書き換えた 2 つの表の目視 | +| A-4 | `git diff --name-only` に `docs/plugin-dev/compose-profiles.md` が出ないこと | +| A-5 | `tests/utils/test_docker_profiles.py` の棚卸しの節に、変更後の経路(`_compose_run` / `_compose_lines` / `cmd_login` / `editor._query_container_name`)が並ぶ | +| B-1 | `monkeypatch.setenv('COMPOSE_PROFILES', 'web')` のうえで `cmd_scale(2)`。`FakeRun` が拾った `env['COMPOSE_PROFILES'] == docker.NO_PROFILE` | +| B-2 | 同じテストの後で `os.environ['COMPOSE_PROFILES'] == 'web'` | +| B-3 | `FakeRun` の `cmd` が `['docker', 'compose', '-f', <生成物>, 'up', '-d', '--no-recreate', 'dev-1', 'dev-2']`。`default_services` を差し替えて一覧を固定する | +| B-4 | `FakeRun(returncode=1)` で `cmd_scale(2) == 1`、`caplog` に `Failed to start new containers`、例外が外へ出ないこと | +| B-5 | `default_services` を実物にし、`_compose_lines` の標準出力を差し替えて、`profiles:` を持つサービスが一覧に出ないことを見る | +| C-1 | `calls` の並びが `['group', 'write_scale', 'volumes', 'network', 'generate', 'default_services', 'up', 'wait', 'bao', 'deploy']` | +| C-2 | `bao` の `start` が `current + 1`、`deploy` の `indices` が `range(current + 1, new + 1)` | +| C-3 | 既存の 4 か所を書き換えずに `pytest tests/commands/test_container_up_order.py tests/commands/test_container_context.py tests/cli/test_project_dispatch.py` | +| C-4 / E-1 | `pytest tests/` の全件を変更の前後で実行し、件数と結果を Pull Request 本文へ並べる | +| D-1 | `grep -rn "'config', '--format', 'json'" lib/` が 1 件 | +| D-2 | `_compose_config_services` と `_resolve_dev_service` の単体テスト 3 通り(終了コード非 0 / 不正 JSON / 正常)。不正 JSON で前者は `json.JSONDecodeError`、後者は `None` | +| D-3 | `cmd_scale` の `def` から次の `def` までの行数 | +| D-4 | この文書の段階の対応表と、`grep -n "/5\]" lib/devbase/commands/container.py` の出力を並べる | +| E-2 | 生成物に `profiles:` を持つサービスを置き、`up` のコマンド列にそのサービス名が出ないことを見る | +| E-3 | `scale` の `calls` に停止(`down` / `stop` / `rm`)が 1 件も出ないことを見る | + +## 未確認のまま残ること + +| 項目 | 内容 | +| --- | --- | +| `--no-recreate` とサービス名の明示の組み合わせの実 docker の挙動 | 確かめるには実環境のプロジェクトが要る(要求仕様の前提 2)。確定仕様 92-95 行目の「打ち消し用のプロファイル名を入れると、依存先としての起動が止まる」を前提にする | +| `COMPOSE_PROFILES` を置いたうえで `devbase scale` を打っている利用者の有無 | 分からない。いた場合、この変更でプロファイルのサービスが起動しなくなる。切り戻しは配布の版を戻すこと | +| `cmd_scale` が `_report_missing_repos` / `_apply_window_titles` を新しいインスタンスへ行わないこと | この束の対象外。**#224 として起票済み** | +| CI での検査 | `release/v3.7.0` を base にした Pull Request では検査ジョブが 1 件も動かない(#216)。証跡は手元で採って Pull Request 本文へ載せる | diff --git a/issues/PLAN65_scale-compose-path.md b/issues/PLAN65_scale-compose-path.md new file mode 100644 index 00000000..cb9b012c --- /dev/null +++ b/issues/PLAN65_scale-compose-path.md @@ -0,0 +1,276 @@ +# PLAN65: `devbase scale` の Compose 呼び出しを共通経路へ寄せ、`cmd_scale` の段階を分ける + +対象 issue: devbasex/devbase#192 + +- ワークフローモード: `standard` + - 根拠: `devbase scale` の本番の振る舞いを変える。変わるのは子プロセスへ渡る環境と、 + 起動の対象に入るサービスの集合である。確定仕様 `docs/specifications/compose-profiles.md` も + 変える。構造変更の側も本番の振る舞いの変更を伴うため、対象のテストが薄くても + `legacy-refactor` ではなく `standard` にする +- ベースブランチ: `release/v3.7.0`(release Pull Request は #212) +- 設計文書: `issues/PLAN65_scale-compose-path-design.md` + +## 目的 +- **確定仕様が約束している「`COMPOSE_PROFILES` を端末や `.env` に置いても devbase 経由の操作には + 効かない」が `devbase scale` でも成り立つ。** いまは成り立たない +- **`devbase up` と `devbase scale` が、同じ生成物に対して同じ規則で Compose を呼ぶ。** 起動の + 対象の決め方と子プロセスの環境が経路によって違わない +- **`cmd_scale` の段階に名前が付き、`cmd_up` と同じ形で読める。** `docker compose config + --format json` を読む経路が 1 つになる +- **`devbase scale` の正常系の手順が、テストで固定される。** いまは 1 つも無い + +## 実測: Compose を起動する経路(2026-09-22 / `release/v3.7.0` の先頭 688efde) +推測ではなく、この作業ツリーで採った値である。 +`os.execvp` 系は 0 件、`Popen` / `check_output` / `subprocess.call` で Compose を起動する箇所も +0 件だった。Compose の起動はすべて `subprocess.run` である。 + +``` +$ grep -rn "'docker', 'compose'\|\"docker\", \"compose\"" lib/ +lib/devbase/utils/docker.py:56: cmd = ['docker', 'compose'] +lib/devbase/commands/container.py:266: cmd = ['docker', 'compose'] # _compose_base_args +lib/devbase/commands/container.py:1648: ['docker', 'compose', '-f', str(override_file), 'up', '-d', '--no-recreate'], +lib/devbase/commands/container.py:1792: ['docker', 'compose', 'config', '--format', 'json'], +lib/devbase/commands/container.py:1970: ['docker', 'compose', 'config', '--format', 'json'], +lib/devbase/editor/opener.py:396: cmd = ["docker", "compose"] +``` + +**この grep だけでは数え足りない。** `_compose_base_args()`(266 行目)が返した配列を使う +呼び出し元は、行の上に `['docker', 'compose']` を持たない。呼び出し元まで辿ると、Compose を +起動する箇所は 8 か所で、**`compose_env()` を渡していないのは 2 か所**である。 + +| # | 場所 | 関数 | `compose_env()` | 起動するもの | +| --- | --- | --- | --- | --- | +| 1 | `utils/docker.py:64` | `docker_compose()` | 渡す | 共通経路そのもの | +| 2 | `container.py:281` | `_compose_run()` | 渡す | `ps` / `logs` | +| 3 | `container.py:291` | `_compose_lines()` | 渡す | `config --services` / `--profiles` | +| 4 | **`container.py:1413`** | **`cmd_login()`** | **渡さない** | `exec - bash` | +| 5 | **`container.py:1648`** | **`cmd_scale()`** | **渡さない** | `up -d --no-recreate` | +| 6 | `container.py:1792` | `_resolve_dev_service()` | 渡す | `config --format json` | +| 7 | `container.py:1970` | `_read_compose_services()` | 渡す | `config --format json` | +| 8 | `editor/opener.py:396` | `_query_container_name()` | 渡す | `ps --format json` | + +issue 本文は 5 だけを挙げているが、**4(`cmd_login`)も同じ穴である**。`tests/utils/test_docker_profiles.py:102` +のコメントも「決定 7 の棚卸しの 4 か所」と書いており、この 2 か所が棚卸しから漏れている。 +`cmd_login` の扱いは設計の決定 2 で決める。 + +## 実測: 現状固定テスト +``` +$ grep -rn "no-recreate" tests/ +(0 件) +$ grep -rn "no-recreate" lib/ +lib/devbase/commands/container.py:1645 +lib/devbase/commands/container.py:1648 +``` + +`cmd_scale` を呼ぶテストは 4 か所ある(`tests/cli/test_project_dispatch.py:366` は +`cmd_scale` を差し替える側で、本体は動かない)。 + +| 場所 | 何を固定しているか | `[4/5]` まで届くか | +| --- | --- | --- | +| `tests/commands/test_container_up_order.py:257` | グループ不一致で 1 を返す | 届かない | +| `tests/commands/test_container_up_order.py:290` | `_build_scaled_override` の例外で 1 を返す | 届かない | +| `tests/commands/test_container_context.py:254` | 接続先(`DOCKER_CONTEXT` / `DOCKER_GID`)の伝播。`@parametrize` で 2 ケース | **届く(唯一)** | +| `tests/cli/test_project_dispatch.py:362,366` | dispatch の引数の伝播(本体は差し替え) | 届かない | + +**正常系を通る 1 本(`test_container_context.py:254`)はあるが、固定しているのは接続先の +伝播だけである。** 起動のコマンド列・子プロセスの `COMPOSE_PROFILES`・`_push_bao_token` と +`./deploy` の順序と範囲は、どのテストも固定していない。issue 本文の「正常系の手順を固定する +テストは無い」は、この意味では成り立つ。 + +## 確定仕様の食い違い +`docs/specifications/compose-profiles.md` は同じ文書の中で相反する 2 つを書いている。 + +| 行 | 内容 | +| --- | --- | +| 83-84 | 「`cmd_scale` が直接呼ぶ `docker compose -f <生成物> up -d --no-recreate` はこの対象に含めない。プロファイルの入口ではないためである」 | +| 388-389 | 「`COMPOSE_PROFILES` を端末や `.env` に置いても devbase 経由の操作には効かない。素の `docker compose` には従来どおり効く」 | + +同じ約束は利用者向けの文書にもある(`docs/plugin-dev/compose-profiles.md:82`)。 + +## 影響 +| 対象 | 影響 | +| --- | --- | +| 公開インタフェース | 変わらない(CLI の引数・環境変数・サブコマンドは増減しない) | +| データ | 変わらない(`project.yml` / 生成物 `.docker-compose.scale.yml` の形は変わらない) | +| 既存の振る舞い | **変わる。** `devbase scale` の子プロセスの `COMPOSE_PROFILES` が常に `__devbase_none__` になり、起動の対象が既定のサービスに限られる。端末または `.env` に `COMPOSE_PROFILES` を置いた人にとっては、`devbase scale` がプロファイルのサービスを起動しなくなる | +| 確定仕様 | **変わる。** `docs/specifications/compose-profiles.md` の経路の表・コマンド列の表・運用・テスト観点 | +| ログ | 変わらない(`[1/5]`〜`[5/5]` の文字列と `Failed to start new containers` を保つ) | +| 利用者の操作 | 追加の操作は要らない。配布された版を使えばそのまま効く | + +## 対象範囲 +含む: + +- `lib/devbase/commands/container.py` + - `cmd_scale` の `docker compose ... up -d --no-recreate` を共通経路(`utils/docker.py` の + `docker_compose()`)へ寄せる + - `cmd_scale` の段階(`[1/5]`〜`[5/5]`)を関数へ抽出する + - `_resolve_dev_service` と `_read_compose_services` の `docker compose config --format json` を + 1 つの関数へ統合し、共通経路へ寄せる + - `cmd_login` の `docker compose exec` へ `compose_env()` を渡す(設計の決定 2) +- `docs/specifications/compose-profiles.md` — 経路の表・組み立てるコマンド列の表・運用・テスト観点 +- `tests/commands/` — `devbase scale` の正常系の手順を固定するテスト(新規) +- `tests/utils/test_docker_profiles.py` — 「決定 7 の棚卸しの 4 か所」のコメントと、そこに並ぶ + 経路のテスト(棚卸しの中身が変わるため) + +含まない: + +- `cmd_up` / `_run_deploy_pipeline` / `cmd_down` / `cmd_profile_*` の振る舞い(読み替えの対象に + 入るだけで、コマンド列も順序も変えない) +- `docker_compose_up()` / `docker_compose_down()` / `compose_env()` のシグネチャの変更 +- `_compose_run`(`devbase ps` / `devbase logs`)と `editor/opener.py` の `_query_container_name`。 + どちらも既に `compose_env()` を渡しており、この束の対象ではない +- `devbase scale` へのプロファイルの指定の口(`scale --profile` のような引数)の追加 +- `pytest` の `DEVBASE_ROOT` の隔離(PR #217 の範囲) +- `cmd_scale` の前提の検査そのものの見直し(`new_scale <= current_scale` を警告で 1 にする + 今の判定は変えない) + +## 前提 +- **前提 1: 実害の観測は無い。** issue 本文のとおり、確定仕様の約束と実装が食い違っていることまでが + 分かっている。`COMPOSE_PROFILES` を置いたうえで `devbase scale` を打った事例の報告は無い。 + よって「壊れているものを直す」ではなく「約束と実装のどちらかへ揃える」変更である +- **前提 2: 実環境のプロジェクトで `devbase up` / `devbase scale` を実行して確かめない。** 実環境の + コンテナは本番の系である。確認はコマンド列を組み立てる関数の単体の水準(`subprocess.run` の + 差し替え)で行う。実 docker と実 `DEVBASE_ROOT` には触れない +- **前提 3: `release/v3.7.0` を base にした Pull Request では CI が 1 件も動かない。** + `.github/workflows/ci.yml` の `on.pull_request.branches` が `main` だけのためである。 + `gh pr checks` は `no checks reported` を返し、`mergeStateStatus` は `CLEAN` を返す(#216)。 + 検証は手元で行い、証跡を Pull Request 本文へ載せる +- **前提 4: pytest は実環境の `DEVBASE_ROOT` を継承する。** その隔離は別の束(PR #217、未マージ)が + 入れる。この束のテストは既存の流儀(各テストが自分で `monkeypatch.setenv`)に合わせる +- **前提 5: `docs/specifications/compose-profiles.md` は別の束(PR #213、未マージ)も触る。** + #213 の hunk は 193 行目付近の 1 行だけで、この束が触る節(74-84 / 107-120 / 386-390 / 445 付近) + とは重ならない。後からマージする側が競合を解く +- **前提 6: プロファイルを持たないプロジェクトの振る舞いは変わらない。** 確定仕様 141 行目の + 「プロファイルを持たないプロジェクトでは、`up` / `down` / `scale` が扱うコンテナの集合と順序は + 変わらない」を保つ + +## 受け入れ条件: 仕様と振る舞い(A・B) +### 仕様の食い違いの解消(A-1〜A-5) + +- [ ] **A-1: `lib/` の中で `compose_env()` を渡さずに `docker compose` を起動する箇所が 0 件になる。** + 上の実測の 8 か所を 1 件ずつ辿り、`compose_env()` を直接渡すか `docker_compose()` を経由する + ことを確かめる。現状は 2 件(`cmd_scale:1648` と `cmd_login:1413`)が満たしていない。 + `grep -rn "'docker', 'compose'\|\"docker\", \"compose\"" lib/` の件数は 6 → 3 に減る。 + 残るのは `utils/docker.py` の共通経路・`_compose_base_args`・`editor/opener.py` の 3 つである +- [ ] **A-2: `docs/specifications/compose-profiles.md` の中に、`cmd_scale` を共通経路の対象から + 外す記述が残っていない。** 現状 83-84 行目の「`cmd_scale` が直接呼ぶ …… この対象に含めない」が + 消え、経路の表(74-81 行目)に `scale` の起動の経路が載る +- [ ] **A-3: 「組み立てるコマンド列」の表に `devbase scale` の行がある。** 行は + `up -d --no-recreate <既定のサービス...>` と、子プロセスの `COMPOSE_PROFILES` が + `__devbase_none__` であることを示す +- [ ] **A-4: `docs/plugin-dev/compose-profiles.md:82` の約束に例外を足さずに済む。** + その約束は「端末と `.env` の `COMPOSE_PROFILES` は devbase 経由の操作に効かない」である。 + 足す必要が出たら、共通経路へ寄せる判断(設計の決定 1)を見直す +- [ ] **A-5: `tests/utils/test_docker_profiles.py` の「棚卸しの N か所」のコメントと、そこに + 並ぶテストが、変更後の経路の一覧と一致する。** いまのコメントは 4 か所と書き、 + `cmd_scale` と `cmd_login` を数えていない + +### `devbase scale` の振る舞い(B-1〜B-5) + +`subprocess.run` を差し替えた単体のテストで確かめる。実 docker には触れない。 + +- [ ] **B-1: `devbase scale` が起動に使う子プロセスの環境の `COMPOSE_PROFILES` が + `__devbase_none__` である。** 呼び出し側の `os.environ` に `COMPOSE_PROFILES=web` を置いた + 状態でも同じ値になる +- [ ] **B-2: 呼び出し側の `os.environ` は書き換わらない。** `cmd_scale` の前後で + `os.environ.get('COMPOSE_PROFILES')` が変わらない +- [ ] **B-3: 組み立てるコマンド列が + `docker compose -f <生成物> up -d --no-recreate <既定のサービス...>` である。** + `-f` に渡るのは `_build_scaled_override` が返したパスで、サービス名は + `default_services(<生成物>)` が返した一覧の全件、その並びのままである +- [ ] **B-4: 起動が 0 以外で終わったら、`Failed to start new containers` を error で出して 1 を返す。** + 例外は送出しない(`subprocess.CalledProcessError` が `cmd_scale` の外へ出ない) +- [ ] **B-5: プロファイルを持たない生成物では、起動の対象に入るサービスの集合が変更前と一致する。** + 比べるのは、`default_services` が返す一覧と、変更前にサービス名を付けずに起動したときの + 対象である。`config --services` の出力を差し替えて示す + +## 受け入れ条件: テスト・構造・退行(C・D・E) +### 正常系の手順の固定(C-1〜C-4) + +- [ ] **C-1: `devbase scale` の正常系を通すテストが存在し、次の順序を固定する。** + `grep -rn "no-recreate" tests/` が 1 件以上になる + + ``` + _check_group_consistency → write_scale → ensure_volumes → ensure_network + → _build_scaled_override → default_services → 起動 → wait_for_containers_ready + → _push_bao_token → ./deploy + ``` +- [ ] **C-2: `_push_bao_token` と `_run_deploy_script_for_instances` に渡る範囲が + `current_scale + 1` から `new_scale` までである。** 既にあるインスタンスを含めない +- [ ] **C-3: 既にある 4 か所の `cmd_scale` のテストが、書き換えずに通る。** + グループ不一致で 1、`_build_scaled_override` の例外で 1、context の伝播、dispatch の伝播 +- [ ] **C-4: `pytest tests/` の全件が、変更の前後で同じ結果になる**(前提 4 のとおり、実行時は + 各テストが自分で `monkeypatch.setenv` する既存の流儀に従う) + +### 構造(D-1〜D-4) + +- [ ] **D-1: `docker compose config --format json` を起動する箇所が 1 か所になる。** + `grep -rn "'config', '--format', 'json'" lib/` が 1 件(現状 2 件) +- [ ] **D-2: 統合した後も 2 つの呼び出し元の契約が変わらない。** + `_resolve_dev_service` は、終了コードが非 0 のときと JSON として読めないときに `None` を返す。 + `_read_compose_services` の契約を引き継ぐ側は、JSON として読めないときに + `json.JSONDecodeError` を伝播する +- [ ] **D-3: `cmd_scale` の本体が 40 行以下になる**(現状 86 行)。抽出した段階の関数が + `[1/5]`〜`[5/5]` のログ文字列をそのまま持つ +- [ ] **D-4: `cmd_scale` と `cmd_up` の段階の対応が、設計文書の表と一致する。** 段階の番号の + 文字列(`[2.5/5]` を含む)は変えない + +## 受け入れ条件: 退行しないこと(E) + +- [ ] **E-1: `devbase up` のコマンド列と順序が変わらない。** + `tests/commands/test_container_up_order.py` を書き換えずに通す +- [ ] **E-2: `devbase scale` はプロファイルのサービスを複製しない**(確定仕様 386-387 行目)。 + 生成物の `profiles:` は保たれたままで、`scale` の対象に入らない +- [ ] **E-3: 既に動いているプロファイルのサービスを `devbase scale` が停止しない。** + `--no-recreate` の起動で、対象に入らないサービスへは触れない(`up` と違い、`scale` は + 停止の段を持たない) + +## 検証手段 +| 条件 | 手段 | +| --- | --- | +| A-1 / D-1 | `grep -rn` の出力を Pull Request 本文へ貼る | +| A-2 / A-3 / A-4 | 変更後の `docs/specifications/compose-profiles.md` の該当の節と `grep -n "cmd_scale" docs/specifications/compose-profiles.md` | +| B-1〜B-5 / C-1〜C-2 | 新しいテスト `tests/commands/test_container_scale_order.py` の `pytest` | +| C-3 / C-4 / E-1 | `pytest tests/` の全件。変更前の結果と並べて Pull Request 本文へ載せる | +| D-2 | 統合した関数の単体のテスト(終了コード非 0 / 不正 JSON / 正常の 3 通り) | +| D-3 | `python - <<'EOF'` で `cmd_scale` の行数を数える、または差分の行数 | +| E-2 / E-3 | 生成物に `profiles:` を含むサービスを置いたテストで、起動の対象の一覧を検査 | + +CI は動かない(前提 3)。上の手段はすべて手元で実行し、証跡を Pull Request 本文へ載せる。 + +## 実装計画 +設計文書の「実装の分け方」の節が決めた 2 本の Pull Request の内訳・対象ファイル・依存の順序を +持つ。この文書は受け入れ条件の側だけを持つ。 + +## 未確認のまま残ること +- **`--no-recreate` と明示したサービス名の組み合わせで、Compose が依存先をどう扱うか**の実測は + 行わない。確定仕様 92-95 行目に、打ち消し用のプロファイル名を入れれば依存先としての起動も + 止まると書いてある。それを前提にする。実 docker で確かめるには実環境のプロジェクトが要る + ため(前提 2)行わない +- **`COMPOSE_PROFILES` を置いたうえで `devbase scale` を打っている利用者がいるか**は分からない。 + いた場合、この変更でプロファイルのサービスが起動しなくなる。切り戻しは配布の版を戻すこと + (`devbase scale` に専用の退避の口は設けない) + +## 境界 +| 区分 | 内容 | +| --- | --- | +| 常に行う | 変更範囲の pytest の実行、既存テストの全件実行、`grep` による数え直し | +| 確認してから行う | 確定仕様の約束の書き換え(この文書の受け入れ条件がその確認である)、`utils/docker.py` の関数のシグネチャの変更 | +| 行わない | 実環境のプロジェクトでの `devbase up` / `devbase scale` の実行、`cmd_up` の振る舞いの変更、依頼範囲外のリファクタリング | + +## 前提とする取り決め +- ブランチ戦略と Pull Request の運用は `ndf-policies` に従う。base は `release/v3.7.0` +- 構造を変える変更は、振る舞いを変えないことをテストで守る(`refactoring`) +- コミットと Pull Request の本文の作法は `markdown-writing` に従う + +## 依頼(原文) +issue #192 の本文から、決めることと採る手・順序の指定を原文のまま写す。 + +> ## 決めること +> +> - `devbase scale` の Compose 呼び出しを共通経路へ寄せるか、それとも仕様の「devbase 経由の +> 操作には効かない」を `scale` 除外込みへ書き直すか。決め方によって、抽出した後の関数の境界が変わる + +> **採る手**: 統合(`consolidate_duplication`)。Compose の呼び出しを共通経路へ寄せてから、段階を抽出する。 +> +> **順序**: 仕様(`scale` を共通経路の対象にするか決める)→ 共通経路へ寄せる → 現状固定テストを足す → 関数を分ける。 From bf7166c469ed74312ba51539003ce10d98b99f1f Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 11:39:17 +0900 Subject: [PATCH 2/3] =?UTF-8?q?docs(PLAN65):=20=E6=B1=BA=E5=AE=9A=2010=20?= =?UTF-8?q?=E3=82=92=E6=B1=BA=E5=AE=9A=E3=81=AE=E8=A8=98=E9=8C=B2=E3=81=AE?= =?UTF-8?q?=E4=B8=80=E8=A6=A7=E3=81=B8=E8=BC=89=E3=81=9B=E3=82=8B=20(#192)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pull Request 本文の「決めたこと」の節は設計文書の `## 決定の記録` の `###` 見出しから 作られるため、章へ切り出した決定 10 が一覧から漏れていた。章を指す見出しを置く。 Co-Authored-By: Claude Opus 5 (1M context) --- issues/PLAN65_scale-compose-path-design.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/issues/PLAN65_scale-compose-path-design.md b/issues/PLAN65_scale-compose-path-design.md index 0c6bc96d..483cd1d0 100644 --- a/issues/PLAN65_scale-compose-path-design.md +++ b/issues/PLAN65_scale-compose-path-design.md @@ -410,7 +410,11 @@ Scale failed: %s 契約は `_compose_config_services` と同じである。同じ契約の名前を 2 つ持つと、次に読む人が 違いを探す -## 実装の分け方(決定 10): 2 本の Pull Request に分ける +### 決定 10: 実装は 2 本の Pull Request に分ける + +内訳・触るファイル・依存の順序・採らなかった案は、次の章「実装の分け方」にある。 + +## 実装の分け方(決定 10) | # | 名前 | 内容 | 触るファイル | 依存 | | --- | --- | --- | --- | --- | From 847865f60f8e806ee9fcb41725ac4e20b8b5cdca Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Tue, 22 Sep 2026 11:55:53 +0900 Subject: [PATCH 3/3] =?UTF-8?q?docs(PLAN65):=20config=20=E8=AA=AD=E3=81=BF?= =?UTF-8?q?=E5=8F=96=E3=82=8A=E3=81=AE=E7=B5=B1=E5=90=88=E3=82=92=201=20?= =?UTF-8?q?=E6=9C=AC=E7=9B=AE=E3=81=AE=20Pull=20Request=20=E3=81=B8?= =?UTF-8?q?=E7=A7=BB=E3=81=99=20(#192)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 確定仕様の経路の表は _resolve_dev_service / _read_compose_services の 2 行を docker_compose の用途へ畳む。畳んだ表を 1 本目で入れて統合を 2 本目に置くと、 1 本目のマージの時点で「共通経路を通る」と書いた仕様と、直接 subprocess.run を 呼ぶ実装が食い違った版が残る。受け入れ条件 A-1(grep が 6 → 3)・A-5(棚卸しの 一致)・D-1 も 1 本目では満たせない。 分け目を「Compose の呼び出しを共通経路へ寄せる」と「cmd_scale の段階を分ける」に 改め、受け入れ条件とどちらの Pull Request が対応するかの表を足した。 Co-Authored-By: Claude Opus 5 (1M context) --- issues/PLAN65_scale-compose-path-design.md | 124 +++++++++++---------- issues/PLAN65_scale-compose-path.md | 8 +- 2 files changed, 72 insertions(+), 60 deletions(-) diff --git a/issues/PLAN65_scale-compose-path-design.md b/issues/PLAN65_scale-compose-path-design.md index 483cd1d0..c380ea22 100644 --- a/issues/PLAN65_scale-compose-path-design.md +++ b/issues/PLAN65_scale-compose-path-design.md @@ -44,9 +44,7 @@ | 図に現れない要素 | 理由 | | --- | --- | | `_compose_run` | この束では触らず、確定仕様の経路の表に並ぶだけである | -| `docs/specifications/compose-profiles.md` | 実行時の呼び出しを持たない | -| `tests/commands/test_container_scale_order.py` | 同じ | -| `tests/utils/test_docker_profiles.py` | 同じ | +| 確定仕様 1 本とテスト 2 本 | 実行時の呼び出しを持たない | `devbase scale` の経路: @@ -133,41 +131,30 @@ docker compose exec --index= <開発サービス名> bash # 生成 ```python def _check_scale_request(new_scale: int, current_scale: int) -> bool: - """``new_scale`` が受け付けられるかを判定し、受け付けないときは案内を出す。 - - ``new_scale < 1`` と ``new_scale <= current_scale`` の 2 つで False を返す。 - ログの文言と出し分け (error / warning + info 2 行) は変更前のまま。 - """ + """``new_scale`` を受け付けるかを判定し、受け付けないときは案内を出す。""" def _run_scale_pipeline(project_name: str, new_scale: int, current_scale: int, config, target: docker_context.DockerTarget, dev_service_name: str) -> Optional[Path]: - """``[1/5]``〜``[5/5]`` の本体。生成した override compose のパスを返す。 - - 起動が 0 以外で終わったときだけ ``None`` を返す (error ログ - ``Failed to start new containers`` はこの関数が出す)。それ以外の失敗は - ``DevbaseError`` / ``DockerError`` のまま呼び出し元へ伝播する。 - """ + """``[1/5]``〜``[5/5]`` の本体。生成した override compose のパスを返す。""" def _compose_config_services() -> tuple[int, dict]: - """``docker compose config --format json`` の (終了コード, services) を返す。 - - 非 0 なら ``services`` は空の辞書。JSON として読めなければ - ``json.JSONDecodeError`` を伝播する。``_read_compose_services`` の契約と同じで、 - 実行は共通経路 ``docker_compose`` を通る。 - """ + """``docker compose config --format json`` の (終了コード, services) を返す。""" def _resolve_dev_service() -> Optional[dict]: - """compose config から dev サービス定義を取得する。失敗時は None。 - - 名前・引数・戻り値の契約は変えない (終了コードが非 0、JSON として読めない、 - のどちらでも ``None``)。 - """ + """compose config から dev サービス定義を取得する。失敗時は None。""" ``` +| 関数 | 契約 | +| --- | --- | +| `_check_scale_request` | `new_scale < 1` と `new_scale <= current_scale` の 2 つで `False` を返す。ログの文言と出し分け(error / warning + info 2 行)は変更前のまま | +| `_run_scale_pipeline` | 起動が 0 以外で終わったときだけ `None` を返す(`Failed to start new containers` はこの関数が出す)。それ以外の失敗は `DevbaseError` / `DockerError` のまま伝播する | +| `_compose_config_services` | 非 0 なら `services` は空の辞書。JSON として読めなければ `json.JSONDecodeError` を伝播する。`_read_compose_services` の契約と同じで、実行は `docker_compose` を通る | +| `_resolve_dev_service` | 名前・引数・戻り値の契約は変えない。終了コードが非 0 でも、JSON として読めなくても `None` を返す | + `_read_compose_services` は削除する。唯一の呼び出し元 `_ensure_images`(`container.py:2019`)は `_compose_config_services` を呼ぶ。`_resolve_dev_service` の名前を残すのは、 `tests/cli/test_base_image_staleness.py` の 3 か所がこの名前を差し替えているためである。 @@ -222,9 +209,8 @@ graph TD | 起動が 0 以外 | `_run_scale_pipeline` が `None` を返す | `Failed to start new containers` を出して 1。変更前のまま | **既定のサービスの解決を足すと、失敗の経路が 1 つ増える。** `default_services` は -`_compose_lines` を通り、非 0 の終了を `DevbaseError` にする。`cmd_up` は同じ解決を既に -行っており、`scale` は `up` の後にしか使えない(生成物を作り直すのも同じ関数である)ため、 -`up` が通るプロジェクトでこの解決だけが失敗する経路は無い。 +`_compose_lines` を通り、非 0 の終了を `DevbaseError` にする。`cmd_up` が同じ解決を既に +行っているため、`up` が通るプロジェクトでこの解決だけが失敗する経路は無い。 ## 決定の記録 @@ -272,17 +258,15 @@ graph TD - **決定 1 は「経路の表が devbase の Compose の起動を網羅している」ことを仕様として言い直す 決定である。** 網羅していない状態のまま表を書き直すと、同じ食い違いを別の行で作る - **観測できる振る舞いは変わらない。** `docker compose exec <サービス> bash` は既に動いている - コンテナを名指しする。開発サービスは `profiles:` を持たない既定のサービスなので、 - `COMPOSE_PROFILES` の値で選ばれ方が変わらない。差分は 1 行(`env=compose_env()`)で、 - `exec` の中で動く `bash` の環境はコンテナ側から来るため影響を受けない + コンテナを名指しする。開発サービスは `profiles:` を持たないため、`COMPOSE_PROFILES` の値で + 選ばれ方が変わらない。`exec` の中で動く `bash` の環境はコンテナ側から来る - **棚卸しのコメントが実装と合っていない。** `tests/utils/test_docker_profiles.py:102` は - 「決定 7 の棚卸しの 4 か所」と書き、`cmd_scale` と `cmd_login` を数えていない。片方だけ直すと - コメントを 5 か所へ直すことになり、次に読む人が残りの 1 つを穴と気づけない + 「決定 7 の棚卸しの 4 か所」と書き、`cmd_scale` と `cmd_login` を数えていない。片方だけ直すと、 + 次に読む人が残りの 1 つを穴と気づけない 採らなかった案: `cmd_login` を別の課題として起票し、この束では触らない。退けたのは、 -経路の表を書き直す作業がこの束にあるためである(表に「`cmd_login` は未対応」と書くか、 -誤った表を残すかのどちらかになる)。ただし**この 1 行は独立して切り出せる**ので、レビューで -範囲外と判断されたら分ける(決定 10 の Pull Request 1 の中で独立したコミットにする)。 +経路の表を書き直す作業がこの束にあるためである。**この 1 行は独立したコミットにする**ので、 +範囲外と判断されたら切り出せる。 ### 決定 3: `docker_compose_up()` は拡張せず、`docker_compose()` を直接呼ぶ @@ -304,8 +288,8 @@ docker_compose(['up', '-d', '--no-recreate', *services], 素通りし、traceback で落ちる。`cmd_up` は `except subprocess.CalledProcessError` も持つが、 `cmd_scale` は持たない。**共通経路へ寄せる実装を素直に書くと、ここを踏む** - `docker_compose()` は `env=compose_env()` を渡す唯一の共通経路であり、目的(決定 1)は - これを通すことで達する。`profile up` / `profile down` / `profile list` も同じく - `docker_compose()` を直接呼んでいる(`container.py:1503` / `1531` / `1547`) + これを通すことで達する。`profile up` / `profile down` / `profile list` も + `docker_compose()` を直接呼ぶ(`container.py:1503` / `1531` / `1547`) 採らなかった案: @@ -348,25 +332,20 @@ Scale failed: %s === Scale completed successfully === ``` -理由: 振る舞いの変更を「子プロセスの環境」と「起動の対象」の 2 点だけに絞る。ログを同時に -変えると、現状固定テストが何を守っているのかが読めなくなる。 +理由: 振る舞いの変更を「子プロセスの環境」と「起動の対象」の 2 点だけに絞る。 ### 決定 6: `_previous_scale_compose()` は使わない `cmd_up` は生成に失敗したとき旧構成を書き戻すために `_previous_scale_compose()` を使うが、 -`cmd_scale` には入れない。 - -理由: `scale` は既存のコンテナを止めない。止める段が無いので、旧構成で停止する必要が無い。 -入れると `scale` が失敗したときに生成物だけが巻き戻り、`project.yml` の `scale` の値 -(`[1/5]` で既に書き換わっている)と食い違う。**変更前の振る舞いを保つ**。 +`cmd_scale` には入れない。理由: `scale` は既存のコンテナを止めない。止める段が無いので、旧構成で停止する必要が無い。 +入れると失敗したときに生成物だけが巻き戻り、`project.yml` の `scale` の値(`[1/5]` で既に +書き換わっている)と食い違う。 ### 決定 7: 段階の番号の文字列は変えない `[1/5]`〜`[5/5]` と `[2.5/5]` をそのまま持つ。`default_services` の呼び出しには段階の番号を -付けない(`cmd_up` も `[2/6]` と `[3/6]` の間で番号を持たない)。 - -理由: 番号を振り直すと、`cmd_scale` の出力を読んでいる人にとっての差分が増える。`[2.5/5]` の -ような中途の番号は `cmd_up` の `[1.5/6]` と同じ流儀で、この束で整えるものではない。 +付けない(`cmd_up` も `[2/6]` と `[3/6]` の間で番号を持たない)。理由: 番号を振り直すと、出力を読んでいる人にとっての差分が増える。`[2.5/5]` のような中途の +番号は `cmd_up` の `[1.5/6]` と同じ流儀で、この束で整えるものではない。 ### 決定 8: 抽出は 2 つの関数に分け、`cmd_up` と対称にする @@ -416,23 +395,49 @@ Scale failed: %s ## 実装の分け方(決定 10) +**分け目は「Compose の呼び出しを共通経路へ寄せる」と「`cmd_scale` の段階を分ける」である。** +確定仕様が約束する経路の一覧は、1 本目のマージの時点で実装と一致させる。 + | # | 名前 | 内容 | 触るファイル | 依存 | | --- | --- | --- | --- | --- | -| 1 | 振る舞いと仕様 | 確定仕様の書き換え(決定 1)・`cmd_scale` の起動を共通経路へ(決定 3・4)・`cmd_login` の 1 行(決定 2)・現状固定テストの新設 | `docs/specifications/compose-profiles.md`、`lib/devbase/commands/container.py`(`cmd_scale` の `[4/5]` と `cmd_login`)、`tests/commands/test_container_scale_order.py`(新設)、`tests/utils/test_docker_profiles.py` | 無し(base は `release/v3.7.0`) | -| 2 | 構造 | `cmd_scale` の段階の抽出(決定 7・8)・config 読み取りの統合(決定 9)・棚卸しのコメントの更新 | `lib/devbase/commands/container.py`(`cmd_scale` / `_resolve_dev_service` / `_read_compose_services` / `_ensure_images`)、`tests/utils/test_docker_profiles.py` | **Pull Request 1 の `:マージ` が要る** | +| 1 | Compose の呼び出しを共通経路へ寄せる | 確定仕様の書き換え(決定 1)・`cmd_scale` の起動(決定 3・4)・`cmd_login` の 1 行(決定 2)・config 読み取りの統合(決定 9)・現状固定テストの新設・棚卸しのコメントと一覧の更新 | `docs/specifications/compose-profiles.md`、`lib/devbase/commands/container.py`(`cmd_scale` の `[4/5]` / `cmd_login` / `_resolve_dev_service` / `_read_compose_services` / `_ensure_images`)、`tests/commands/test_container_scale_order.py`(新設)、`tests/utils/test_docker_profiles.py` | 無し(base は `release/v3.7.0`) | +| 2 | `cmd_scale` の段階を分ける | 段階の抽出(決定 7・8)。振る舞いは変えない | `lib/devbase/commands/container.py`(`cmd_scale` のみ) | **Pull Request 1 の `:マージ` が要る** | + +依存の理由: 2 は `cmd_scale` の本体を関数へ割る。1 が書き換える `[4/5]` の行も動かすため、 +2 を先に出すとレビューした差分と入る差分が変わる。 + +**config 読み取りの統合(決定 9)を 1 本目へ入れる理由。** 確定仕様の経路の表は、 +`_resolve_dev_service` と `_read_compose_services` の 2 行を `docker_compose` の用途へ畳む。 +畳んだ表を 1 本目で入れて統合を 2 本目に置くと、**1 本目のマージの時点で仕様と実装が食い違った +版が残る。** 統合そのものは Compose の起動を共通経路へ寄せる変更で、#192 が指定した順序の +「共通経路へ寄せる」に入る。2 本目に残すのは「関数を分ける」だけである。 -依存の理由: どちらも `lib/devbase/commands/container.py` の `cmd_scale` の同じ区画を触る。 -2 を先に出すと、1 が書き換える `[4/5]` の行が別の関数へ移っており、レビューした差分と入る差分が -変わる。**`tests/utils/test_docker_profiles.py` も両方が触る**(1 は経路のテストを足し、 -2 は棚卸しのコメントと関数名を直す)。 +## 受け入れ条件とどちらの Pull Request が対応するか + +| 受け入れ条件 | Pull Request 1 | Pull Request 2 | +| --- | --- | --- | +| A-1(`compose_env()` を渡さない起動が 0 件。`grep` が 6 → 3) | **満たす** | 変わらない | +| A-2 / A-3 / A-4(確定仕様と利用者向けの文書) | **満たす** | 変わらない | +| A-5(棚卸しのコメントと一覧が経路と一致) | **満たす** | 変わらない | +| B-1〜B-5(`devbase scale` の振る舞い) | **満たす** | 緑のまま通す | +| C-1 / C-2(正常系の手順の固定) | **満たす** | 緑のまま通す | +| C-3 / C-4 / E-1〜E-3(退行しないこと) | 満たす | **書き換えずに満たす** | +| D-1 / D-2(config 読み取りの統合と契約) | **満たす** | 変わらない | +| D-3 / D-4(`cmd_scale` の本体 40 行以下と段階の対応) | 満たさない | **満たす** | + +`grep -rn "'docker', 'compose'" lib/` は現状 6 件である。Pull Request 1 で消えるのは +`container.py` の 1648(`cmd_scale`)・1792・1970 の 3 件である。残るのは +`utils/docker.py:56` と `container.py:266` と `opener.py:396` で、Pull Request 2 はこの +件数を変えない。`cmd_login`(1413)は `_compose_base_args()` の戻り値を使うため、この +`grep` には現れない。 #192 が指定した順序(仕様 → 共通経路 → 現状固定テスト → 関数を分ける)は、この 2 本の中で 次のように並ぶ。 | 順序 | どこで | 備考 | | --- | --- | --- | -| 1. 仕様 | Pull Request 1 の 1 つ目のコミット | 決定 1 を確定仕様へ書く | -| 2. 共通経路へ寄せる | Pull Request 1 の 2 つ目のコミット | 新しいコマンド列と子プロセスの環境を固定するテストを先に書いて落とす(`tdd-cycle`)。**構造を触らないので、この時点では「気づけない」対象が無い** | +| 1. 仕様 | Pull Request 1 の 1 つ目のコミット | 決定 1 を確定仕様へ書く。経路の表は畳んだ後の 5 行にする | +| 2. 共通経路へ寄せる | Pull Request 1 の 2 つ目のコミット | `cmd_scale` の起動・`cmd_login` の 1 行・config 読み取りの統合。新しいコマンド列と子プロセスの環境を固定するテストを先に書いて落とす(`tdd-cycle`)。**`cmd_scale` の本体の構造は触らない** | | 3. 現状固定テストを足す | Pull Request 1 の 3 つ目のコミット | 正常系の手順(順序・範囲)を固定する。**構造を変える前に緑にする** | | 4. 関数を分ける | Pull Request 2 | 3 のテストを書き換えずに緑のまま通す。書き換えが要るなら振る舞いが変わっている | @@ -443,10 +448,11 @@ Scale failed: %s 採らなかった案: -| 案 | 退けた理由 | +| 採らなかった形 | 退けた理由 | | --- | --- | -| 1 本にまとめる | 振る舞いの変更と構造の変更が同じ差分に混ざる。レビューで「この行はどちらの目的か」が読めない | +| 1 本にまとめる | `cmd_scale` の本体を関数へ割る差分と、Compose の呼び出しを寄せる差分が同じ差分に混ざる。レビューで「この行はどちらの目的か」が読めない | | 3 本に分ける(仕様 / 振る舞い / 構造) | 確定仕様の 1 節と実装の 1 行は同じ約束の裏表で、別々にマージすると仕様と実装が食い違う版が中間に残る | +| config 読み取りの統合を 2 本目に置く | 1 本目のマージの時点で、畳んだ経路の表と直接 `subprocess.run` を呼ぶ実装が食い違う。受け入れ条件 A-1・A-5・D-1 も 1 本目では満たせない | | 現状固定テストだけを先に 1 本出す | 上の「3 を 2 より先に置かない理由」と同じ | ## テスト設計 diff --git a/issues/PLAN65_scale-compose-path.md b/issues/PLAN65_scale-compose-path.md index cb9b012c..67278404 100644 --- a/issues/PLAN65_scale-compose-path.md +++ b/issues/PLAN65_scale-compose-path.md @@ -239,9 +239,15 @@ lib/devbase/commands/container.py:1648 CI は動かない(前提 3)。上の手段はすべて手元で実行し、証跡を Pull Request 本文へ載せる。 ## 実装計画 -設計文書の「実装の分け方」の節が決めた 2 本の Pull Request の内訳・対象ファイル・依存の順序を + +設計文書の「実装の分け方」の章が、2 本の Pull Request の内訳・対象ファイル・依存の順序を 持つ。この文書は受け入れ条件の側だけを持つ。 +**どの条件がどちらの Pull Request で満たされるかは、設計文書の「受け入れ条件とどちらの +Pull Request が対応するか」の表が決める。** A-1 の `grep` の件数(6 → 3)と A-5 の棚卸しの +一致は 1 本目で満たす。D-3・D-4(`cmd_scale` の本体 40 行以下と段階の対応)だけが 2 本目で +満たす条件である。 + ## 未確認のまま残ること - **`--no-recreate` と明示したサービス名の組み合わせで、Compose が依存先をどう扱うか**の実測は 行わない。確定仕様 92-95 行目に、打ち消し用のプロファイル名を入れれば依存先としての起動も