From 88c578db69a1b99f958bb5d2febe4ea01d6d8ef6 Mon Sep 17 00:00:00 2001 From: "takemi.ohama" Date: Thu, 17 Sep 2026 09:19:19 +0900 Subject: [PATCH] =?UTF-8?q?docs:=20PLAN58=20=E3=82=92=20Compose=20profiles?= =?UTF-8?q?=20=E3=81=AE=E7=A2=BA=E5=AE=9A=E4=BB=95=E6=A7=98=E3=81=B8?= =?UTF-8?q?=E5=8F=96=E3=82=8A=E8=BE=BC=E3=82=80=20(#189)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 付随サービス群の後からの起動・停止を docs/specifications/compose-profiles.md へ as-is 仕様として書き、 計画・設計・決定の記録・実装計画のファイルを削除する。 Co-Authored-By: Claude Opus 5 (1M context) --- docs/specifications/compose-profiles.md | 461 ++++++++++++++++++++ issues/PLAN58_compose-profiles-decisions.md | 209 --------- issues/PLAN58_compose-profiles-design.md | 395 ----------------- issues/PLAN58_compose-profiles-impl.md | 141 ------ issues/PLAN58_compose-profiles.md | 194 -------- 5 files changed, 461 insertions(+), 939 deletions(-) create mode 100644 docs/specifications/compose-profiles.md delete mode 100644 issues/PLAN58_compose-profiles-decisions.md delete mode 100644 issues/PLAN58_compose-profiles-design.md delete mode 100644 issues/PLAN58_compose-profiles-impl.md delete mode 100644 issues/PLAN58_compose-profiles.md diff --git a/docs/specifications/compose-profiles.md b/docs/specifications/compose-profiles.md new file mode 100644 index 00000000..b135ba5f --- /dev/null +++ b/docs/specifications/compose-profiles.md @@ -0,0 +1,461 @@ +# 付随サービス群の後からの起動・停止(Compose の profiles) + +## 概要 + +dev のほかに app / db などのサービスを持つプロジェクトで、`devbase up` の既定では +`profiles:` を持たないサービス(dev を含む)だけを起動し、`profiles:` を付けた付随サービス群は +`devbase project profile up|down` で後から起動・停止する。起動・停止のどちらでも dev +コンテナは操作の対象に入らず、再作成も再起動もされない。 + +プロジェクトは `compose.yml` の各サービスへ `profiles:` を書くだけでよい。プロファイル名は +プロジェクトが決め、devbase は既定のプロファイル名を持たない。devbase が担うのは、 +プロファイルを指定して起動・停止する入口、停止(`devbase down` と `up` 冒頭)を全プロファイルへ +効かせること、devbase 経由の Compose で有効なプロファイルを devbase 自身が決めること、 +フックと `devbase list` の TUI への反映である。 + +プロジェクト作者向けの書き方(`profiles:` と `depends_on.required` の例、フックでの分岐)は +[テスト用サーバを後から起動・停止する](../plugin-dev/compose-profiles.md) にある。 + +## 用語 + +| 用語 | 意味 | +| --- | --- | +| プロファイル | Compose の `profiles:` に書いた名前。付随サービス群をまとめる単位 | +| 既定のサービス | `profiles:` を持たないサービス。`devbase up` で起動する | +| プロファイルのサービス | そのプロファイルに属し、既定のサービスに含まれないサービス | +| 打ち消し用のプロファイル名 | `__devbase_none__`(定数 `NO_PROFILE`)。どのプロジェクトも定義しない名前で、devbase が子プロセスの `COMPOSE_PROFILES` へ入れて利用者の指定を無効にする | +| 生成物 | `devbase up` がプロジェクト直下に作る `.docker-compose.scale.yml`。プロファイルの解決と操作はすべてこのファイルを `-f` で渡して行う | +| 開発サービス名 | `get_dev_service_name()` が返す名前(`DEV_SERVICE_NAME`、既定 `dev`)。生成物では `<開発サービス名>-1`..`-N` へ複製される | + +## 構成要素 + +| 要素 | 置き場所 | 責務 | +| --- | --- | --- | +| 子プロセスの環境と Compose の呼び出し | `lib/devbase/utils/docker.py` | `NO_PROFILE` / `compose_env`。`docker_compose` が `env=compose_env()` を渡す。`docker_compose_up(services=...)` が起動の対象を受け、`docker_compose_down` が `--profile '*'` を付ける | +| プロファイルの解決 | `lib/devbase/commands/container.py` | `_compose_lines` / `default_services` / `profile_services`。TUI 向けに対象プロジェクトへ切り替えて解決する `project_profile_names` | +| プロファイルの操作 | `lib/devbase/commands/container.py` | 共通の前段 `_profile_targets`、`cmd_profile_up` / `cmd_profile_down` / `cmd_profile_list`、`_dev_instance_indices`、`_running_services` / `_running_label` | +| 振り分け | `lib/devbase/commands/container.py` | `_dispatch_lifecycle` の handlers の `profile` と `_dispatch_profile`。名前を指定したときの切替 `_enter_project`。`cmd_container` の非推奨の警告 | +| `devbase up` の起動 | `lib/devbase/commands/container.py` | `_run_deploy_pipeline` が停止より前に `default_services` を求め、`docker_compose_up` へ渡す | +| フックの環境変数 | `lib/devbase/project/runtime.py`、`commands/container.py` | `active_profiles_env` / `hook_env(config, active_profiles=())`。`_hook_vars`、`_run_deploy_script_for_instances(..., active_profiles=()) -> bool`、`_run_pre_up_hook` | +| 引数の受け口 | `lib/devbase/cli.py` | `_add_profile_subparser(sub, with_name=...)`、`SUBCMD_MAP` と `SUBCMD_PREFIX_PREFERENCES` | +| 一覧の操作メニュー | `lib/devbase/tui/actions_project.py` | `_PROFILE_OPS` / `_profile_names` / `_running_ops` / `_op_profile`、`_BACK_TO_TOP_OPS` | +| 復元境界 | `lib/devbase/tui/dispatch.py`、`lib/devbase/env/runtime.py` | `_preserve_cwd_env` が CWD・`os.environ`・機密の注入履歴(`snapshot_injected` / `restore_injected`)を戻す | +| エディタのコンテナ名の照会 | `lib/devbase/editor/opener.py` | `_query_container_name` の `ps --format json` に `env=compose_env()` を渡す | +| シェル補完 | `etc/devbase-completion.bash`、`etc/_devbase` | `project` / `container` の `profile` と、その下の `up down list` | + +型(クラス)は追加していない。モジュール関数の並びで構成する。 + +```mermaid +graph TD + CLI[引数の受け口 cli.py] --> DL[_dispatch_lifecycle / _dispatch_profile] + TUI[一覧の操作メニュー] -->|dispatch_lifecycle| DL + TUI -->|_preserve_cwd_env の中| PN[project_profile_names] + DL --> OP[cmd_profile_up / down / list] + OP --> RS[profile_services / default_services] + PN --> RS + OP --> HK[_run_deploy_script_for_instances] + HK --> HE[hook_env / active_profiles_env] + OP --> DC[docker_compose] + RS --> CE[compose_env] + DC --> CE + CE --> COMPOSE[(docker compose)] +``` + +## 仕様 + +### 有効なプロファイルの決め方 + +devbase 経由の Compose では、有効なプロファイルを devbase が経路ごとに `--profile` で決める。 +端末の環境変数やプロジェクトの `.env` の `COMPOSE_PROFILES` で、起動・表示の対象を変えない。 + +`compose_env(environ=None)` は `environ`(既定は `os.environ`)の複製を作り、`COMPOSE_PROFILES` +を `__devbase_none__` にして返す。元の環境は変えない。次の経路が子プロセスへこの環境を渡す。 + +| 経路 | 場所 | 用途 | +| --- | --- | --- | +| `docker_compose` | `utils/docker.py` | `up` / `down` / `profile up` / `profile down` / `profile list` の `ps` | +| `_compose_lines` | `commands/container.py` | `config --profiles` / `config --services`(プロファイルの解決) | +| `_compose_run` | `commands/container.py` | `devbase ps` / `devbase logs` | +| `_resolve_dev_service` | `commands/container.py` | `build --expires` / `rebuild` が読む `config --format json` | +| `_read_compose_services` | `commands/container.py` | 起動前のイメージ確認が読む `config --format json` | +| `_query_container_name` | `editor/opener.py` | エディタを開くときの `ps --format json` | + +`cmd_scale` が直接呼ぶ `docker compose -f <生成物> up -d --no-recreate` はこの対象に含めない。 +プロファイルの入口ではないためである。 + +値の決め方には次の理由がある。 + +- **キーを外すだけにしない。** Compose は環境変数が無ければプロジェクトの `.env` の値を採る。 + 値を入れておけば、環境変数がファイルより優先される規則で `.env` の指定も無効になる +- **空文字列にしない。** 空の値が「空の一覧」と「未設定」のどちらに解釈されるかは版に依る + 可能性がある。打ち消し用のプロファイル名なら、どちらでも「その名前のプロファイルは無い」になる +- **`.env` は書き換えない。** `.env` は利用者とプロジェクトの持ち物で、書き換えると素の + `docker compose` の挙動まで変わる。環境変数の上書きなら影響は devbase 経由に閉じる +- 打ち消し用のプロファイル名を入れると、既定のサービスがプロファイルのサービスへ `depends_on` + を持つ構成でも、依存先としての起動が止まる。既定のサービスどうしの依存の待ち合わせは残る +- `--profile` と `COMPOSE_PROFILES` は和集合として扱われるため、`--profile` を明示する経路の + 対象は変わらない。環境変数だけでプロファイルを有効にする使い方は約束しない + +`__devbase_none__` はプロジェクトが使ってはならない予約名である。定義すると、そのプロファイルが +devbase 経由の操作で常に有効になる。 + +### 組み立てるコマンド列 + +`docker_compose` は `docker compose -f <生成物>` の後に配列をそのまま並べる。`--profile` は +subcommand より前に置く。`<サービス...>` はプロファイル X に属するサービスの全件である。 + +| 操作 | コマンド列(`docker compose -f <ファイル>` の後) | 子プロセスの `COMPOSE_PROFILES` | +| --- | --- | --- | +| `devbase up` の起動 | `up -d <既定のサービス...>`(`--profile` なし) | `__devbase_none__` | +| `devbase down` / `devbase up` 冒頭の停止 | `--profile '*' down -t0` | `__devbase_none__` | +| `profile up X` | `--profile X up -d --no-deps <サービス...>` | `__devbase_none__` | +| `profile down X`(1 段目) | `--profile X stop <サービス...>` | `__devbase_none__` | +| `profile down X`(2 段目) | `--profile X rm -f <サービス...>` | `__devbase_none__` | +| `profile list` の稼働状況 | `--profile '*' ps --format json` | `__devbase_none__` | +| 既定のサービスの解決 | `config --services` | `__devbase_none__` | +| プロファイル名の解決 | `config --profiles` | `__devbase_none__` | +| プロファイル X の解決 | `--profile X config --services` | `__devbase_none__` | + +`docker_compose_up(compose_file, detach=True, services=())` は `services` が空なら従来どおり +サービス名を付けない。`docker_compose_down` は引数を増やさず、常に `--profile '*'` を付ける。 + +### `devbase up` と `devbase down` + +`devbase up` は、その構成で開発環境を作り直す操作である。起動の前に全プロファイルを含めて +停止し、起動するのは既定のサービスだけにする。プロファイルのサービスが動いていても、`up` の後は +既定の状態へ揃う。プロファイルのサービスを使い続けるときは `up` の後にもう一度 `profile up` する。 +後の状態が直前の操作に依存しないようにするためである。 + +`_run_deploy_pipeline` は `_previous_scale_compose` の中で、生成物を作った直後、既存コンテナの +停止**より前**に `default_services(override_file)` を求める。`config --services` が補間エラーなどで +失敗しても、稼働中の環境を止めず、旧構成の生成物を書き戻して止まる。求めた一覧は停止の後の +`docker_compose_up(..., services=services)` へ渡す。生成物は `up` のたびに作り直し、必ず +`<開発サービス名>-1`..`-N` を含むため、一覧は最新で空にならない。 + +起動の対象を明示するのは、打ち消し用のプロファイル名が効かない形でプロファイルが有効に +なっても、一覧に無いサービスを起動しないためである。停止は `--profile '*'` で対象を広げる向きの +指定のため、`.env` が別のプロファイルを有効にしても対象は狭まらない。`--profile '*'` を付けない +と、プロファイルのサービスが動いたまま残り、network の削除にも失敗する。 + +プロファイルを持たないプロジェクトでは、`up` / `down` / `scale` が扱うコンテナの集合と順序は +変わらない。プロファイルのサービスは scale の対象にせず、複製されるのは開発サービスだけである。 + +### プロファイルの解決 + +プロファイル名とサービスの対応は、生成物を `-f` で渡して `docker compose config` に問い合わせて +得る(`_compose_lines` が標準出力を空行を除いた行の一覧にし、非 0 の終了は `DevbaseError` +にする)。 + +| 関数 | 求め方 | 並び | +| --- | --- | --- | +| `default_services(compose_file, environ=None) -> list[str]` | `config --services` | Compose の出力順 | +| `profile_services(compose_file, environ=None) -> dict[str, list[str]]` | `config --profiles` で名前を得る。名前が無ければ `{}`。各 X について `--profile X config --services` から既定のサービスを差し引く | 名前もサービスも Compose の出力順 | + +- 生成物を devbase が読んで解決しないのは、生成物に `profiles: ["${TEST_PROFILE:-test}"]` の + ような変数の式が残るためである。Compose が展開した名前を使わないと、`list` が式を表示し、 + `profile up test` が未知の名前になる。既存の `_expand_env_vars` は `${VAR:-default}` を + 解釈しないため使わない +- `config --format json` を 1 回だけ呼ぶ形は採らない。この出力は有効でないプロファイルの + サービスを含まないため、対応を作れない +- `config` はデーモンへ接続しない。名前と対応はデーモンへ接続できなくても解決できる + +### コマンドの入出力 + +| コマンド | 入力 | 成功 | 失敗 | +| --- | --- | --- | --- | +| `devbase project profile up [name] ` | プロファイル名(必須)、プロジェクト名(省略時は現在地) | サービスを起動し、`./deploy` があれば呼び直して 0 | 下表 | +| `devbase project profile down [name] ` | 同上 | サービスを停止してコンテナを削除し 0。フックは呼ばない | 下表 | +| `devbase project profile list [name]` | プロジェクト名(省略可) | 表を標準出力へ出して 0 | 下表 | +| `devbase container profile up ` / `down ` / `list`(`ct` も同じ) | プロファイル名のみ | `project profile` と同じ | 同じ | + +どれも `--context NAME` を受け付ける([別ホストの Docker への dev コンテナ起動](remote-docker-context.md))。 + +| 状態 | `up` / `down` | `list` | +| --- | --- | --- | +| 生成物が無い | `devbase up` を促すエラーを出して 1。Compose を呼ばない | 同じく 1 | +| `profile_services` が失敗した | エラーを出して 1 | 同じく 1 | +| 未知のプロファイル名 | 使えるプロファイルの一覧(無ければ `(なし)`)を出して 1 | 該当しない | +| `./deploy` があり `project.yml` を読めない(`DevbaseError`) | `up` は Compose を呼ばずに 1 | 該当しない | +| Compose が非 0 で終わった(デーモンへ接続できないときを含む) | その終了コードをそのまま返す。`down` は `stop` が失敗したら `rm` を呼ばない | 稼働状況の列を `不明` にして 0 | +| `./deploy` が 1 インスタンスでも失敗した | `up` は 1 | 該当しない | +| `profile` の後に操作が無い | エラーを出して 1 | 同じ | + +引数の受け方は次のとおりである。 + +- `project profile up|down` の位置引数は既存の `scale` と同じ並びで、値が 1 個ならプロファイル名、 + 2 個なら(プロジェクト名、プロファイル名)になる。名前を渡すと `_dispatch_lifecycle` が + `_enter_project` で切り替える(切替元の機密を落とし、`projects/` へ移動し、切替先の機密を + 注入する)。結果は、そのプロジェクトのディレクトリで名前を省いて実行した場合と同じである +- `container profile` / `ct profile` は `[name]` を受け付けず、現在地のプロジェクトで動く。 + `container` 群の既存の規約(`project` の `login` / `build` と同じ)に従う。`cmd_container` が + 非推奨の警告を 1 行出す +- 入れ子の subparser は `dest='profile_subcommand'` を使う。親の `subcommand` を再利用すると、 + `cli._dispatch` が `project profile list` を `project list`(プロジェクト一覧)へ流すためである +- 前方一致の省略は `project p` / `container p` を従来どおり `ps` に解決し(`SUBCMD_PREFIX_PREFERENCES`)、 + `project pr` は `profile` に解決する +- `bin/devbase` の `_PROJECT_NAME_SUBCOMMANDS`(`up down ps logs scale rebuild`)に `profile` は + 入れない。wrapper は 3 番目の引数をプロジェクト名として解決するが、`profile` ではそこに + `up` / `down` / `list` が来るため、同名のプロジェクトが実在すると誤って移動する。名前の解決は + Python 側の `_dispatch_lifecycle` が行う + +### `profile up` + +`cmd_profile_up(profile, context=None)` は次の順で動く。 + +1. `_profile_targets`: `_prepare_compose(context)` で接続先を反映して機密を注入し、生成物の存在・ + `profile_services`・名前を検査する +2. `./deploy` があれば `project_runtime.current_project_config()` を読む +3. 対象を `logger.info` で 1 行残し、`--profile X up -d --no-deps <サービス...>` を呼ぶ +4. `./deploy` があれば `_run_deploy_script_for_instances(deploy, _dev_instance_indices(生成物), config, active_profiles=(X,))` + を呼ぶ。`./pre-up` は呼ばない(コンテナ起動前の準備は `up` の役目である) + +サービス名をすべて明示し、`--no-deps` を付けるのは、既定のサービスを操作の対象に入れないため +である。 + +- サービス名を省くと既定のサービスも照合の対象に入る。機密の値はプロセスの環境から解決される + ため、値が変わっていれば dev が再作成されうる +- 生成物では、プロファイルのサービスの `depends_on: dev` が `dev-1`..`dev-N` へ書き換わる + (`_build_scaled_services` が `_rewrite_depends_on` を非 dev サービスにも適用する)。Compose は + 依存先を対象へ取り込むため、明示だけでは dev が対象に入る。`required: false` は依存先が不在の + ときのエラーを緩めるだけで、対象から外さない +- `--no-deps` は依存先を自動起動しないため、プロファイルのサービスは 1 つも漏らさず渡す。渡す集合は + `profile_services` が生成物から求める +- 生成のときに devbase が `required: false` を補うことはしない。依存が必須かはプロジェクトの意図で + あり、対象から外す役目は `--no-deps` が担う + +`depends_on` の書き方(`[dev]` / `required: false` 付き)によらず、dev は対象に入らない。 + +### `profile down` + +`cmd_profile_down(profile, context=None)` は `_profile_targets` の後、対象を `logger.info` で 1 行 +残し、`--profile X stop <サービス...>` と `--profile X rm -f <サービス...>` を順に呼ぶ。 + +- `down <サービス...>` は使わない。Compose の対象選択は指定したサービスの依存元(そのサービスへ + `depends_on` を持つ側)も含めるため、dev が db へ `depends_on` を持つ構成で dev まで消える。 + `required: false` はこれも止めない。`stop` / `rm` は依存元をたどらず、`down` のサービス指定が + 使える版を調べる必要も無くなる +- `stop` に `-t` を付けず、既定の猶予(10 秒)で止める。プロファイルに入るのはデータベースなど + 状態を持つサービスで、開発環境を残したまま止める操作だからである(`devbase down` は環境ごと + 畳むため `-t0`) +- `rm` には `-f`(確認の省略)だけを付け、`-v` は付けない。名前付きボリュームは残る +- `docker_compose_down` は通さず、この関数に引数も足さない。その呼び出し側は `devbase down` と + `up` 冒頭の停止だけで、どちらも全体を `-t0` で落とす + +### `profile list` + +`cmd_profile_list(context=None)` は `_profile_targets` で対応を得て、次の表を出す。 + +| 列 | 内容 | +| --- | --- | +| PROFILE | プロファイル名(`config --profiles` の順) | +| SERVICES | そのプロファイルのサービス名をカンマ区切りで並べたもの | +| RUNNING | `稼働数/総数 状態語`、または `不明` | + +| 稼働数 | 状態語 | +| --- | --- | +| 総数と同じ | `running`(例: `3/3 running`) | +| 1 以上で総数未満 | `partial`(例: `1/3 partial`) | +| 0 | `stopped`(例: `0/3 stopped`) | + +稼働状況は `_running_services` が `--profile '*' ps --format json` で求め、`State` が `running` +のサービスだけを数える(`exited` / `paused` は数えない)。`--profile '*'` は、有効でない +プロファイルのサービスを `ps` に出さない版でも `stopped` に張り付かないためである。出力は +1 行 1 JSON と JSON 配列の両方を受ける。実行の失敗(`OSError`)・非 0 の終了・JSON として +読めない出力はすべて `不明` にして 0 で終わる。プロファイルが 1 つも無ければ `ps` を呼ばず、 +見出しの行だけを出して 0 で終わる。 + +### フックの約束 + +`hook_env(config, active_profiles=())` は既存の `DEVBASE_PRIMARY_DIR` などに加えて、 +`active_profiles_env(active_profiles)` が作る `DEVBASE_ACTIVE_PROFILES` を持つ。名前と区切り +(カンマ)を決めるのは `active_profiles_env` だけである。キーは値が空でも必ず持ち、呼び出し元の +環境に同名の値が残っていても上書きする。`project.yml` を渡さない呼び出し(`config=None`)でも +`_hook_vars` がこのキーだけは渡す。 + +| フック | 呼ぶ経路 | `DEVBASE_ACTIVE_PROFILES` | +| --- | --- | --- | +| `./pre-up` | `devbase up` のみ | 常に空 | +| `./deploy` | `devbase up` / `devbase scale` | 空 | +| `./deploy` | `devbase project profile up X` | `X` | + +- `profile up` の後に新しいフックを作らず `./deploy` を呼び直す。`./deploy` は既にインスタンス + ごとに冪等に書かれ、`DEVBASE_ACTIVE_PROFILES` で分岐できる。フック名を増やすと、プロジェクトが + 持つ約束とどちらに書くかの判断が増える +- 値は常にプロファイル名 1 つである。同時に 2 つ以上を起動する操作は無く、カンマ区切りは将来の + 拡張のための予約である。`profile up` を 2 回呼ぶと、2 回目のフックに渡るのは 2 回目の名前だけ +- `profile up` が `./deploy` を呼ぶ番号は、`_dev_instance_indices` が生成物の `services` から + `<開発サービス名>-<数字>` に完全一致する名前を読んだ昇順の番号である。`project.yml` の `scale` + は使わない。`up` の後に書き換えられると稼働中のインスタンスと食い違うためで、生成物は `up` が + 作った稼働中の構成と一致する。開発サービス名は `get_dev_service_name()` に従う +- `_run_deploy_script_for_instances` は失敗したインスタンスがあっても残りを実行し、全インスタンスで + 成功したかを返す。`profile up` は失敗を終了コード 1 に反映し、`up` / `scale` は従来どおり警告 + だけにとどめる + +### `devbase list` の操作メニュー + +`devbase list` で起動中の行を選ぶと `_operation_menu` が操作の一覧を出す。プロファイルを持つ +プロジェクトにだけ、`_RUNNING_OPS` の後へ次の 2 項目を足す(`_running_ops`)。 + +| 項目 | 値 | 選んだ後 | +| --- | --- | --- | +| テスト用サーバ起動 (profile up) | `profile-up` | プロファイル名を選ばせ `profile up` を実行し、一覧へ戻る | +| テスト用サーバ停止 (profile down) | `profile-down` | プロファイル名を選ばせ `profile down` を実行し、一覧へ戻る | + +選べる操作は、選べば動くものに限る。持たないプロジェクトで出すと、選んだ後に「プロファイルが +ありません」と戻ることになるためである。 + +**出し分けの解決。** `_profile_names(name)` は `_preserve_cwd_env()` の中で +`container.project_profile_names(name)` を呼ぶ。`project_profile_names` は次のように動く。 + +1. `docker_context.reset()` の後、`_enter_project(name)` で対象プロジェクトへ切り替え、その `env` + と機密を載せる。生成物が対象の `env` にだけある変数(`${VAR:?required}` など)を参照して + いても解決できるようにするためである +2. 切替に失敗したか生成物が無ければ `[]`。あれば `profile_services` の名前の一覧を返す +3. `DevbaseError` / `OSError` は「持たない」として `[]` にする。操作メニューを出すこと自体を + 止めないためである +4. 最後に `docker_context.reset()` と `runtime.release_store()` を行う + +解決にデーモンは要らないため、接続できない状態でも項目は出る。 + +**復元境界。** `_preserve_cwd_env` は入口で CWD・`os.environ`・機密の注入履歴 +(`runtime.snapshot_injected()`)を控え、終わりに 3 つを同時に戻す(`restore_injected`)。 +ハンドラの中で別プロジェクトへ切り替えると注入履歴は切替先のものになる。値だけを戻すと、次の +`clear_injected` が切替元固有の機密を知らずに残し、次に操作するプロジェクトの Compose 子プロセスへ +渡すためである。メニュー表示時の解決と `dispatch_lifecycle` の両方がこの境界を通る。 + +**選択と委譲。** `_op_profile(operation)` は、選ばれた時点でもう一度 `_profile_names` を解決する。 + +- 解決が空(項目を出した後の状態変化・解決失敗)なら警告を出し、選択を出さずにサブメニューへ戻る +- 名前が 1 つだけでも `menu.select` で選択として出す。Esc / ← はサブメニューへ戻る +- 選ばれた名前で `dispatch_lifecycle('profile', name, profile_subcommand=operation, profile=...)` + へ委譲する。TUI はコマンドの中身を持たない +- 2 項目は `_BACK_TO_TOP_OPS` に入る。コンテナの数が変わるため、実行後は一覧へ戻って STATUS を + 更新して見せる + +questionary が無い端末の代替経路(番号入力して `up`)は変わらない。 + +### 対応する Docker Compose の版 + +最低対応版は **Docker Compose 2.20.0** である。 + +| 使う機能 | 版の下限 | +| --- | --- | +| `--no-deps`、サービスを明示した `up` / `stop` / `rm -f` | 作らない(2 系全般で使える) | +| `depends_on.required` | 2.20.0。プロジェクトが書き、案内する構成例が使う。2.20.0 未満では `required: false` を書いた構成の検証に失敗するため対象外とする | +| `--profile '*'` | 公式ドキュメントに版の記載が無い | + +**未確認の事項(この仕様の制約)。** 動作を確かめたのは Docker Compose v5.1.4 だけである。 +`--profile '*'` は v5.1.4 でのみ確認済みで、2.20.0 以上 5.x 未満は未検証である。ワイルドカードを +解釈しない版で `*` がリテラルのプロファイル名として扱われ、停止の対象が従来と同じに留まるか +(想定)も未確認である。この経路は `devbase down` と `up` 冒頭の停止に入り、プロファイルを +使わない全プロジェクトを通る。 + +## データ・設定 + +### `compose.yml` と生成物 + +プロジェクトはサービスへ `profiles:` を書く。`devbase up` が作る生成物 +`.docker-compose.scale.yml` は、プロファイルのサービスの `profiles:` をそのまま保つ。 +`depends_on` の長い書式(`condition` / `required`)も保ち、依存先が開発サービスなら +`<開発サービス名>-1`..`-N` の各要素へ写す。開発サービスがプロファイルのサービスへ持つ +`depends_on` の `required` も保つ。 + +### 環境変数 + +| 名前 | 向き | 意味 | +| --- | --- | --- | +| `COMPOSE_PROFILES` | 出力(devbase 経由の Compose の子プロセス) | 常に `__devbase_none__`。利用者の端末の値と `.env` の値は効かない | +| `DEVBASE_ACTIVE_PROFILES` | 出力(`./pre-up` / `./deploy`) | 有効なプロファイル名。`profile up X` の `./deploy` だけ `X`、それ以外は空 | +| `DEV_SERVICE_NAME` | 入力 | 開発サービス名。`profile up` がフックを呼ぶ番号の読み取りに使う | + +## セキュリティ + +プロファイルのサービスへ渡る機密は、そのサービスが元々 `env_file` で参照していた由来のキーに +限る(`volume/compose.py` の `_services_receiving_secrets` の規則)。プロファイルの操作は生成物を +そのまま使い、実行前に `_prepare_compose` で既存と同じ注入を通すため、新しい注入経路を作らない。 +素の `docker compose` ではなく devbase を通すのは、機密の注入と対象サービスの限定をこの規則の中で +行うためである。 + +TUI では、復元境界が機密の注入履歴も戻すため、別プロジェクトを続けて操作しても前のプロジェクト +固有の機密が次の Compose の子プロセスへ渡らない。 + +## 運用 + +- `devbase up` はプロファイルのサービスも止め、既定のサービスだけを起動する。テスト用サーバを + 使い続けるなら `up` の後に `profile up` をやり直す +- `devbase down` はプロファイルのサービスも含めて削除する。`devbase scale` はプロファイルの + サービスを複製しない +- `COMPOSE_PROFILES` を端末や `.env` に置いても devbase 経由の操作には効かない。素の + `docker compose` には従来どおり効く +- プロファイル名に `__devbase_none__` を使わない +- 起動・停止の記録は `logger.info` の 1 行(プロファイル名と対象サービス)で残る。Compose の出力は + そのまま標準出力へ流れる +- 最低対応版と未確認の事項は「対応する Docker Compose の版」のとおり + +## テスト観点 + +自動テストは `subprocess.run` を差し替えて、組み立てたコマンド列・子プロセスの環境・終了コードを +検査する。実 docker と実 `DEVBASE_ROOT` には触れない。 + +- `compose_env` が `COMPOSE_PROFILES` を打ち消し用の名前にし(未設定でも入れる)、プロセスの環境を + 変えないこと。`docker_compose`・`ps` / `logs`・`config` を読む 2 経路・エディタの `ps` が + その環境を渡すこと。`down` が `--profile '*' down -t0` になり、`up` がサービス無しで従来の形、 + サービス有りで `--profile` 無しに名前を並べること(`tests/utils/test_docker_profiles.py`) +- `devbase up` が生成物の既定のサービスを起動へ渡し、`default_services` が失敗したときは + コンテナを止めないこと(`tests/commands/test_container_up_order.py`)。既存の `up` の harness は + `default_services` を差し替える(`test_up_roundtrips.py` / `test_container_context.py` / + `test_container_bao.py` / `tui/test_dispatch.py`) +- `default_services` が打ち消し用の名前で `config --services` を呼ぶこと、`profile_services` が + 既定のサービスを差し引き、Compose が展開した名前を使い、プロファイルが無ければ空、解決の失敗で + `DevbaseError` になること。生成物が無いときに Compose を呼ばずに 1、未知の名前で起動・停止を呼ばずに + 既知の名前を出して 1、`profile up` が全 + サービスと `--no-deps` を渡し、Compose の終了コードを返し、生成物の番号と `DEV_SERVICE_NAME` に + 従って `./deploy` を走らせ、`./deploy` の失敗と `project.yml` の `DevbaseError` で 1 になること。 + `profile down` が `stop` → `rm -f`(`-v` なし)で、`stop` の失敗で `rm` を呼ばず、`rm` の失敗で + 非 0 になること。`profile list` の 3 つの状態語、`--profile '*'` 付きの `ps`、デーモン不通・ + `ps` の失敗・読めない JSON での `不明` と 0、JSON 配列の受け取り、プロファイルが無いときの + 見出しだけの出力(`tests/commands/test_container_profile.py`) +- `project profile` の `[name] ` の割り当て、`list` の任意の名前、`container profile` が + 名前を拒むこと、`project profile list` が `project list` へ流れないこと、名前の解決が先に行われる + こと、`container` / `ct` が同じ委譲で警告を 1 行だけ出すこと、操作なしで 1、`p` が `ps`・`pr` が + `profile` に解決されること(`tests/cli/test_profile_dispatch.py`) +- `DEVBASE_ACTIVE_PROFILES` が `up` の `./pre-up` / `./deploy` で空、`profile up` の `./deploy` で + 全インスタンスにプロファイル名、`config` 無しでも渡ること、1 インスタンスの失敗を返すこと + (`tests/commands/test_hook_env.py`) +- TUI の 2 項目がプロファイルを持つときだけ出て、解決が空・失敗・生成物なしでは出ないこと、1 件でも + 選択を出して委譲の属性が `profile` / 名前 / `profile_subcommand` / `profile` になること、選択の + Back と 2 回目の解決が空のときにサブメニューへ戻ること、実行後に一覧へ戻ること、解決が対象 + プロジェクトの CWD・`env`・機密の上で行われ、CWD と `os.environ` が戻ること + (`tests/cli/tui/test_profile_menu.py`) +- `_preserve_cwd_env` が注入履歴を戻すこと(`tests/cli/tui/test_dispatch.py`)、 + `snapshot_injected` / `restore_injected` の往復・後の注入で控えが変わらないこと・空の控えで履歴を + 消すこと(`tests/env/test_runtime.py`) +- 生成物が非 dev サービスの `profiles` と、`depends_on` の `condition` / `required` を保つこと + (`tests/volume/test_compose_profiles.py`) +- シェル補完の `profile` と `up down list`(`tests/cli/test_completion.py`) + +実コンテナが要る条件は手動確認で判定する(Docker Compose v5.1.4、`alpine:3` の最小構成)。 + +| 確認すること | 手順 | +| --- | --- | +| `up` は既定のサービスだけを起動し、`profile up` / `profile down` の前後で dev-1..N の Container ID と `StartedAt` が変わらない。`profile down` 後も名前付きボリュームが残り、`down` で全コンテナと network が消える | `up` → `profile up X` → `profile down X` → `down` を通し、`docker inspect` の値を段ごとに比べる。`project.yml` の `scale` を書き換えた後の `profile up` で `./deploy` が生成物の全インスタンスへ走ることも見る | +| 依存を `depends_on: [dev]` で書いても dev が変わらない | 同じ手順 | +| dev の環境変数の値を変えても `profile up` が dev を再作成しない | 値を変えてから `profile up X`。`--no-deps` 無しでは再作成されることを dry-run で見る | +| dev がプロファイルのサービスへ `depends_on`(`required: false`)を持っても `profile down` が dev を止めない | `profile up X` → `profile down X` の前後で比べる | +| 端末の環境変数と `.env` の `COMPOSE_PROFILES` が `up` に効かない | 両方の置き方で `up` を通し、dev だけが動くこと、続く `profile up` / `down` が効くことを見る | +| プロファイルを持たない構成の `up` / `down` が従来どおり | 全サービスの起動と、`down` の後に何も残らないことを見る | +| デーモン不通で `list` が `不明` と 0、`up` / `down` が 1 | `DOCKER_HOST` を存在しないソケットへ向けて実行する | + +実プロジェクトでの `devbase up` 全経路、`devbase list` の TUI の目視、Compose 2.20.0 以上 5.x 未満 +での `--profile '*'` は、配布後の確認の対象である。 + +## 関連リンク + +- [テスト用サーバを後から起動・停止する(プロジェクト作者向け)](../plugin-dev/compose-profiles.md) +- [CLI リファレンス: `devbase project profile`](../user/cli-reference/02-project.md#devbase-project-profile) +- [プラグイン開発クイックスタート: フックへ渡る環境変数](../plugin-dev/quickstart.md#フックへ渡る環境変数) +- [別ホストの Docker への dev コンテナ起動(docker context)](remote-docker-context.md) +- [機密ストアの保存先の差し替え](secret-backend.md) +- 課題: devbasex/devbase#189。設計 PR: #190、実装 PR: #191 +- [Compose file reference: `depends_on`](https://docs.docker.com/reference/compose-file/services/#depends_on) +- [Using profiles with Compose](https://docs.docker.com/compose/how-tos/profiles/) diff --git a/issues/PLAN58_compose-profiles-decisions.md b/issues/PLAN58_compose-profiles-decisions.md deleted file mode 100644 index ef7febd5..00000000 --- a/issues/PLAN58_compose-profiles-decisions.md +++ /dev/null @@ -1,209 +0,0 @@ -# 189: 決定の記録 - -設計は [PLAN58_compose-profiles-design.md](PLAN58_compose-profiles-design.md) にある。このファイルは、その設計で選んだ結論と理由、採らなかった案だけを持つ。 - -## 決定の記録 - -### 決定 1: プロファイルの解決は Compose に行わせる - -プロファイル名とサービスの対応は、`docker compose` へ問い合わせて得る。読むのは生成済みの `.docker-compose.scale.yml` で、`-f` で渡す。 - -| 求めるもの | 呼び方 | -| --- | --- | -| プロファイル名の一覧 | `config --profiles` | -| 既定のサービス | `config --services`(打ち消し用のプロファイル名を入れた環境で呼ぶ) | -| プロファイル X のサービス | `--profile X config --services` から既定のサービスを差し引く | - -**生成物を自分で読んで解決する案は採らない。** 生成物は元の `compose.yml` の値をそのまま持ち、`profiles: ["${TEST_PROFILE:-test}"]` のような変数の式が残る。Compose は実行時にこれを展開するため、式のまま扱うと `profile list` が式を表示し、`profile up test` が未知の名前になる。 - -**既存の `_expand_env_vars`(`commands/container.py:416`)で展開する案も採らない。** この関数が解釈するのは `$VAR` と `${VAR}` だけで、既定値付きの `${VAR:-default}` は残る。Compose の変数展開は既定値・必須・入れ子を持つ仕様であり、その部分実装を devbase 側に持つと、書ける構成と解決できる構成が食い違う。 - -実測(Docker Compose v5.1.4、`profiles: ["${TEST_PROFILE:-test}"]` と `profiles: [test]` を持つ構成、`.env` に `COMPOSE_PROFILES=test`): - -| 呼び方 | 返った値 | -| --- | --- | -| `config --profiles` | `test`(`TEST_PROFILE=demo` を与えると `demo`) | -| `config --services`(打ち消し用のプロファイル名あり) | `dev` | -| `--profile test config --services`(打ち消し用のプロファイル名あり) | `cache` `db` `dev` | - -差し引きで `test` のサービスは `cache` と `db` になる。 - -**この解決にデーモンへの接続は要らない。** `config` は構成を解釈するだけで、デーモンへ問い合わせない。実測(Docker Compose v5.1.4、`DOCKER_HOST` を存在しないソケットへ向けた状態)では次のようになった。 - -| 呼び方 | 終了コード | -| --- | --- | -| `config --profiles` / `config --services` | 0 | -| `ps` | 1 | -| `up -d <サービス>` | 1 | - -そこで失敗の扱いを 2 つに分ける。**名前と対応は接続できなくても出せる。** 稼働状況は `ps` が要るため出せない。 - -| 操作 | デーモンへ接続できないとき | -| --- | --- | -| `profile list` | 名前と対応を出し、稼働状況の列を `不明` にして終了コード 0 | -| `profile up` / `profile down` | Compose が 1 で落ちる。その終了コードをそのまま返す | - -`config --format json` を 1 回だけ呼ぶ案は採らない。**この出力は、有効でないプロファイルのサービスを含まない**(v5.1.4 で確認)。1 回の呼び出しでは対応を作れない。 - -### 決定 2: プロファイルの操作はサービス名をすべて明示し、`--no-deps` を付ける - -サービス名を省いて `--profile X up -d` とすると、既定のサービスも照合の対象に入る。機密は名前だけを列挙する形で生成物に書かれる。値はプロセスの環境から解決される。**照合の対象に入った時点で dev の構成が変わりうる。** そのため対象を明示する。 - -**サービス名の明示だけでは足りない。** `_build_scaled_services` は非 dev サービスにも `_rewrite_depends_on` を適用する(`lib/devbase/volume/compose.py:488`)。プロファイルのサービスが `depends_on: dev` を書いていると、生成物では `dev-1`..`dev-N` になる。Compose は依存先を解決して対象へ取り込む。受け入れ条件の「既定のサービスの Container ID と `StartedAt` が変わらない」はこれで崩れる。 - -**`required: false` はこれを止めない。** この属性が緩めるのは「依存先が不在のときのエラー」だけである。依存先を操作の対象から外す働きは持たない。Compose v5.1.4 の dry-run でも dev の起動が含まれる。依存先の構成が変わったときに再作成する実装のため、機密などの値が変わった状態では Container ID が変わりうる。 - -そこで `--no-deps` を使う。起動のコマンド列は `--profile X up -d --no-deps <対象サービス...>` になる。この選択肢は依存先を操作の対象から外す。 - -**`--no-deps` は依存先を自動起動しない。** そのため、プロファイルに属するサービスをすべて明示して渡すことが前提になる(要求仕様の前提 5)。1 つでも落とすと、そのサービスは起動しない。渡す集合は「プロファイルの解決」が生成物から求めるため、取りこぼしは起きない。 - -| 組み立て方 | `profile up X` が触るもの | 判定 | -| --- | --- | --- | -| `--profile X up -d`(サービス名なし) | 既定のサービスを含む全体 | 不可。dev を再作成しうる | -| `--profile X up -d <一部のサービス>` | 渡したサービスだけ | 不可。残りが起動しない | -| `--profile X up -d <対象サービス...>`(`--no-deps` なし) | そのサービスと依存先の dev-1..N | 不可。dev を再作成しうる | -| `--profile X up -d --no-deps <対象サービス...>` | そのサービスだけ | 可 | - -`depends_on` の書き方は前提にしない。`depends_on: {dev: {condition: service_started, required: false}}` でも `depends_on: [dev]` でも、`--no-deps` を付ければ dev は対象に入らない。 - -生成のときに devbase が `required: false` を補う案は採らない。依存が必須かどうかはプロジェクトが決める意図であり、生成物が黙って緩めると `devbase up` の起動順の意図が読めなくなる。対象から外す役目は `--no-deps` が担うため、補う必要もない。 - -### 決定 3: プロファイル起動の後は `./deploy` を呼び直す。新しいフックは作らない - -`./deploy` は既にインスタンスごとに呼ばれ、プロジェクト側で冪等に書かれている。プロファイルの有無は `DEVBASE_ACTIVE_PROFILES` で分岐できる。フック名を増やす理由がない。 - -`./post-profile-up` のような新しい名前を足す案は採らない。プロジェクトが持つ約束の数が増える。どちらに書くべきかの判断も各プロジェクトに生まれる。 - -`hook_env` は `_run_pre_up_hook` も呼んでいる。そのため `./pre-up` にも `DEVBASE_ACTIVE_PROFILES` が渡る。`./pre-up` を呼ぶのは `cmd_up` だけなので、値は常に空である。`profile up` は `./pre-up` を呼ばない。コンテナの起動前に済ませる準備は `up` の役目だからである。要求仕様が `./pre-up` を挙げるのは、この空の値を含めた約束のことである。 - -| フック | 呼ぶ経路 | `DEVBASE_ACTIVE_PROFILES` | -| --- | --- | --- | -| `./pre-up` | `cmd_up` のみ | 常に空 | -| `./deploy` | `cmd_up` | 空 | -| `./deploy` | `cmd_profile_up` | 起動したプロファイル名 1 つ | - -### 決定 4: 停止は全プロファイルを対象にし、`up` は既定の状態へ揃える - -`devbase up` は「その構成で開発環境を作り直す」操作である。プロファイルのサービスだけが前の状態のまま残ると、`up` の後の状態が直前の操作に依存する。 - -`up` の冒頭の停止を dev-1..N だけに絞る案は採らない(利用者の指示、2026-09-16)。テストを続けたい場合は `up` の後にもう一度プロファイルを起動する。 - -### 決定 5: プロファイルの停止は `down` を使わず、`stop` と `rm -f` の 2 段で行う - -`cmd_profile_down` はまず `docker_compose(['--profile', X, 'stop', <サービス...>])` を呼ぶ。続けて `docker_compose(['--profile', X, 'rm', '-f', <サービス...>])` を呼ぶ。`<サービス...>` はプロファイル X の全件である(決定 2)。1 段目が失敗したら 2 段目は呼ばず、1 を返す。 - -`down <サービス...>` を渡す案は採らない。理由は 2 つある。 - -| 理由 | 中身 | -| --- | --- | -| 対象が広がらない | `stop` / `rm` のサービス指定は古くから安定しており、依存元をたどって対象を広げない | -| 版の下限を作らない | `down [SERVICES]` の対応版を調べる必要がなくなり、最低対応版の根拠が `depends_on.required` の 2.20.0 だけで閉じる | - -**`down <サービス...>` は依存元も削除する。** Compose の対象選択は、指定したサービスの祖先も含める。祖先とは、そのサービスへ `depends_on` を持つ側である。根拠は [v5.1.4 の実装](https://github.com/docker/compose/blob/v5.1.4/pkg/compose/dependencies.go#L104-L129)である。dev が `depends_on: {db: {condition: service_started, required: false}}` を持つ構成では、`down db` が dev も消す。受け入れ条件「既定のサービスの Container ID と `StartedAt` が変わらない」はこれで崩れる。`required: false` はこれを止めない(決定 2 と同じ理由)。 - -**`down [SERVICES]` は版の下限を左右する。** この位置引数は比較的新しい追加で、宣言している最低対応版 2.20.0 が受け付ける保証が無い。受け付けない版では、`down` がプロジェクト全体(dev を含む)を落とす。`stop` / `rm` へ寄せると、対応版を調べる必要そのものが消える。 - -**猶予は既定の 10 秒とする。** プロファイルに入るのはデータベースのような状態を持つサービスである。`devbase down` は開発環境ごと畳む操作のため `-t0` で即座に落とす。プロファイルの停止は稼働中の開発環境を残したまま行うため、`stop` に `-t` を付けず、既定の猶予で落とす。 - -**ボリュームは消さない。** `rm` には `-f` だけを付け、`-v` は付けない。`-v` は匿名ボリュームを消す選択肢である。名前付きボリュームが残ることは受け入れ条件にある。`-f` は削除の確認を省くためだけに要る。 - -`docker_compose_down` に `services` と `timeout` の引数を足す案は採らない。この関数の呼び出し側は `devbase down` と `up` 冒頭の停止だけであり、どちらも全体を `-t0` で落とす。引数を増やすと、使われない組み合わせが関数の表に残る。変更は `--profile '*'` を足すことに留める(決定 4)。 - -| 関数 | 用途 | 使う subcommand | 猶予 | サービスの指定 | -| --- | --- | --- | --- | --- | -| `docker_compose_down` | `devbase down` / `up` 冒頭の停止 | `down` | `-t0` | しない(全体) | -| `cmd_profile_down` | プロファイルの停止 | `stop` → `rm -f` | 既定(10 秒) | する | - -### 決定 6: プロジェクト名は位置引数で受け、`bin/devbase` は変えない - -プロジェクト名を受けるのは `project profile` だけである。既存の `scale` と同じ並び(`[name] <値>`)に揃える。`container profile` は受けない。`container` 群のサブコマンドは現在地のプロジェクトで動く、という既存の規約に従う。`project` でも `login` / `build` が同じ理由で `[name]` を持たない。 - -`bin/devbase` は変えない。`_PROJECT_NAME_SUBCOMMANDS`(`up down ps logs scale rebuild`)の名前解決は 3 番目の引数だけを見る。`profile` をその一覧へ足すと、`up` / `down` / `list` がプロジェクト名として解決されうる。Python 側の `_dispatch_lifecycle` には名前を解決して移動する経路が既にある。そちらに寄せる。 - -| 入口 | `[name]` | 名前の解決 | -| --- | --- | --- | -| `devbase project profile up [name] ` | 受ける | `_dispatch_lifecycle`(Python 側) | -| `devbase container profile up ` | 受けない | しない(現在地で動く) | -| `devbase ct profile up ` | 受けない | しない(現在地で動く) | - -### 決定 7: 有効なプロファイルは devbase が決め、起動の対象も明示する - -`docker_compose` は現在 `subprocess.run(cmd, ...)` を `env=` なしで呼ぶ(`lib/devbase/utils/docker.py:14-55`)。子プロセスは `os.environ` を暗黙に継承する。`COMPOSE_PROFILES=test` が設定された端末では、`devbase up` の `compose up -d` が test のサービスまで起動する。受け入れ条件「`up` の後は既定のサービスだけが動く」はこれで崩れる。v5.1.4 の最小構成で確認済みである。 - -そこで対策を 2 つ重ねる。**環境変数を子へ渡さないことと、起動の対象をサービス名で明示することである。** - -**1. 環境変数を devbase の値で上書きする。** `docker_compose` で `env=` を新たに構築する。`os.environ` の複製の `COMPOSE_PROFILES` へ、**どのプロジェクトも定義しない名前 `__devbase_none__`** を入れて渡す。以下ではこれを打ち消し用のプロファイル名と呼ぶ。有効なプロファイルは経路ごとに `--profile` で明示する。 - -キーを外すだけでは足りない。**Compose はプロジェクトの `.env` を自分で読み、環境変数が無ければその値を採る。** 値を入れておけば、環境変数がファイルより優先される規則に乗って `.env` の指定を無効にできる。 - -**2. 起動の対象を明示する。** `devbase up` の起動は、**`config --services` で得た既定のサービスをすべて渡す**(決定 1)。1 が効かない形でプロファイルが有効になっても、対象に入らないサービスは起動しない。 - -2 つは役割が違う。 - -| 対策 | 何を防ぐか | -| --- | --- | -| 打ち消し用のプロファイル名を渡す | 端末の環境変数と `.env` の両方によるプロファイルの有効化。依存の待ち合わせはそのまま残る | -| 対象を明示する | 対象の広がり。`up` に名前を渡すことで、一覧に無いサービスは起動しない | - -**打ち消し用のプロファイル名を入れるだけで `depends_on` 経由の起動も止まる。** 既定のサービスがプロファイルのサービスへ `depends_on` を持つ構成では、プロファイルが有効なかぎり依存先として起動する。対象の明示では止まらない。打ち消し用のプロファイル名で無効にすれば起動しない。`--no-deps` で止める案は採らない。既定のサービスどうしの `depends_on` の待ち合わせまで失う。 - -実測(Docker Compose v5.1.4、`.env` に `COMPOSE_PROFILES=test`、dev が `depends_on: {db: {condition: service_started, required: false}}` を持つ構成): - -| 渡し方 | 起動したもの | -| --- | --- | -| そのまま `up -d dev` | dev と db | -| キーを外して `up -d dev` | dev と db | -| `COMPOSE_PROFILES=__devbase_none__` で `up -d dev` | dev だけ | -| `--no-deps` を付けて `up -d dev` | dev だけ(依存の待ち合わせを失う) | - -既定のサービスの一覧は `default_services` が `.docker-compose.scale.yml` から作る(決定 1)。`devbase up` は起動の直前に生成物を作り直すため、一覧は常に最新である。 - -| 経路 | プロファイルの指定 | 対象の渡し方 | `COMPOSE_PROFILES` | -| --- | --- | --- | --- | -| `devbase up` の起動 | 付けない | 既定のサービスをすべて明示する | 打ち消し用のプロファイル名を入れる | -| `devbase down` と `up` 冒頭の停止 | `--profile '*'` | 渡さない(全体が対象) | 打ち消し用のプロファイル名を入れる | -| `profile up X` | `--profile X` | そのプロファイルのサービスをすべて明示し、`--no-deps` を付ける | 打ち消し用のプロファイル名を入れる | -| `profile down X` | `--profile X` | 同じ一覧を `stop` と `rm -f` へ渡す | 打ち消し用のプロファイル名を入れる | - -**この方式の前提と限界。** 前提は、生成物が `up` のたびに作り直されることである。限界は、対象を明示するため、生成物に無いサービスは `up` で起動しないことである。生成物には必ず `dev-1`..`dev-N` が入るため、一覧が空になることはない。 - -**停止には要らない。** 停止は `--profile '*'` で対象を広げる向きの指定である。`.env` が別のプロファイルを有効にしても、対象が狭まることはない。 - -**`docker compose` を呼ぶ経路の棚卸し。** 現在は 6 か所ある。扱いは次のとおりである。 - -| 経路 | 場所 | 扱い | -| --- | --- | --- | -| `docker_compose` | `lib/devbase/utils/docker.py:14` | 対象。`up` / `down` / `profile` の各操作はここを通る | -| `_compose_run`(`ps` / `logs`) | `lib/devbase/commands/container.py:269` | 対象。`docker_compose` を経由せず直接 `subprocess.run` するため、同じ `env=` を別に組み立てる | -| `_resolve_dev_service`(`config --format json`) | `lib/devbase/commands/container.py:1430` | 対象。`config` は有効なプロファイルのサービスを解決結果へ含めるため、読む集合が利用者の設定で変わる | -| `_read_compose_services`(`config --format json`) | `lib/devbase/commands/container.py:1608` | 対象。同上。イメージの確認が読むサービスの集合を devbase が決める | -| エディタを開く経路(`ps --format json`) | `lib/devbase/editor/opener.py:395` | 対象。`ps` の解釈もプロファイルに依るため、`_compose_run` と同じ扱いにする | -| `cmd_scale` の直接呼び出し | `lib/devbase/commands/container.py:1282` | 範囲外。プロファイルの入口ではない。要求仕様の「対象範囲・含まない」に挙げる | - -**5 か所を対象にする。** どれも「devbase が読むサービスの集合」か「devbase が起動するサービスの集合」を決める経路である。集合が利用者の設定で変わると、同じプロジェクトでも devbase の判断が端末ごとに変わる。 - -`ps` / `logs` はコンテナを起動しない読み取りの操作である。それでも対象に含めるのは、`COMPOSE_PROFILES` が効くと Compose が解釈するサービスの集合が変わり、表示の中身が利用者の環境に左右されるためである。devbase の表示は、devbase が決めたプロファイルに揃える。 - -**空文字列にはしない。** `COMPOSE_PROFILES=` を渡す形は、版によって「空の一覧」と「未設定」のどちらに解釈されるかを調べる必要が出る。打ち消し用のプロファイル名なら、どちらに解釈されても「その名前のプロファイルは無い」という同じ結果になる。 - -**打ち消し用のプロファイル名は `__devbase_none__` とする。** プロジェクトがこの名前のプロファイルを定義すると、そのプロファイルが常に有効になる。前置きの下線 2 つは Compose の慣習的な名前と衝突しにくく、文書でこの名前を予約として案内する。 - -**`.env` は書き換えない。** `.env` は利用者とプロジェクトの持ち物であり、devbase が値を消すと素の `docker compose` を叩いたときの挙動まで変わる。環境変数の除去と起動の対象の明示なら、影響は devbase 経由の呼び出しだけに閉じる。 - -| 案 | 効く範囲 | 判定 | -| --- | --- | --- | -| `.env` から `COMPOSE_PROFILES` を消す | 素の `docker compose` にも及ぶ | 不可。利用者の持ち物を変える | -| 環境変数のキーを外す | devbase 経由のみ。`.env` には効かない | 足りない。`.env` の値がそのまま効く | -| 環境変数を空文字列にする | devbase 経由のみ | 不可。空の解釈が版に依る | -| 環境変数へ打ち消し用のプロファイル名を入れ、起動の対象も既定のサービスへ限る | devbase 経由のみ | 可 | - -`--profile` を明示する経路では、打ち消し用のプロファイル名を入れても対象は変わらない。`--profile` と `COMPOSE_PROFILES` は和集合として扱われるため、外して困るのは「環境変数だけでプロファイルを有効にしていた」場合である。devbase はその使い方を約束していない。プロファイルの起動は `profile up` が唯一の入口である。 - -### 決定 8: 一覧の 2 項目は、プロファイルを持つプロジェクトにだけ出す - -操作の一覧は固定の並びだったが、この 2 項目だけ出し分ける。プロファイルを持たないプロジェクトで選べてしまうと、選んだ後に「プロファイルがありません」と戻ることになる。選べる操作は、選べば動くものに限る。 - -常に出して選んだ後に知らせる案は採らない。一覧の項目が 8 個から 10 個に増え、そのうち 2 個が多くのプロジェクトで動かない状態になる。 - -実行後は一覧へ戻す(`_BACK_TO_TOP_OPS` に入れる)。起動と停止はコンテナの数を変えるため、一覧の状態表示を更新して見せる。 - diff --git a/issues/PLAN58_compose-profiles-design.md b/issues/PLAN58_compose-profiles-design.md deleted file mode 100644 index 723df0d6..00000000 --- a/issues/PLAN58_compose-profiles-design.md +++ /dev/null @@ -1,395 +0,0 @@ -# 189: Compose の profiles で付随サービス群を後から起動・停止する - -要求と受け入れ条件は [PLAN58_compose-profiles.md](PLAN58_compose-profiles.md) にある。この文書は「どう作るか」だけを扱う。 - -## 機能一覧 - -| # | 機能 | 誰が使うか | -| --- | --- | --- | -| F1 | プロファイルのサービスを起動する | プロジェクトの利用者 | -| F2 | プロファイルのサービスを停止して削除する | プロジェクトの利用者 | -| F3 | プロファイルの一覧と稼働状況を見る | プロジェクトの利用者 | -| F4 | 停止(`down` と `up` 冒頭)を全プロファイルへ効かせる | プロジェクトの利用者 | -| F5 | 有効なプロファイルをフックへ伝える | プロジェクトの作者 | -| F6 | `devbase list` の一覧から起動・停止する | プロジェクトの利用者 | - -## 構成要素 - -| 要素 | 責務 | -| --- | --- | -| プロファイルの解決 | 生成済みの構成ファイルを `-f` で渡して `docker compose config` を呼び、プロファイル名とサービス名の対応を得る。既定のサービスの一覧も同じ経路で求める。稼働状況は持たない | -| プロファイルの操作 | 起動・停止・一覧の 3 つの入口。接続先の反映と機密の注入を済ませてから Compose を呼ぶ | -| Compose の呼び出し | `docker compose` のコマンド列を組み立てて実行する。起動には `--no-deps` を付ける。プロファイルの停止は `stop` と `rm -f` の 2 段で行う。全体の停止には全プロファイルを指定する。有効なプロファイルは devbase が `--profile` で決め、`COMPOSE_PROFILES` には打ち消し用のプロファイル名を入れて渡す。`devbase up` の起動は既定のサービスを明示して渡す(決定 7) | -| フックの実行 | プロジェクトの `./deploy` を、有効なプロファイルを環境変数へ載せて呼ぶ。同じ環境変数は `./pre-up` にも渡る | -| 引数の受け口 | `project` / `container` の配下に `profile` のサブコマンドを足す | -| 一覧の操作メニュー | `devbase list` の起動中の行で、プロファイルの起動・停止を選ばせる。選ばせた後は共有のハンドラへ委譲する(F6) | - -```mermaid -graph TD - subgraph 入口 - CLI[引数の受け口] - TUI[一覧の操作メニュー] - end - subgraph 操作 - OP[プロファイルの操作] - RS[プロファイルの解決] - HK[フックの実行] - end - subgraph 実行 - DC[Compose の呼び出し] - end - CLI --> OP - TUI --> CLI - OP --> RS - OP --> DC - OP --> HK - HK --> DC -``` - -## システム構成 - -### 文脈 - -```mermaid -graph LR - 利用者 --> devbase[devbase の配布物] - devbase --> compose[Docker Compose] - compose --> daemon[Docker デーモン] - devbase --> hook[プロジェクトのフック] - devbase --> store[機密の置き場] -``` - -Docker Compose と Docker デーモンは、こちらが変えられない外部の系である。プロジェクトのフックは各プロジェクトが持つ。devbase が定めるのは呼び出しの約束(環境変数)だけである。 - -### 配置 - -```mermaid -graph TD - subgraph 利用者の端末 - W[wrapper: bin/devbase] - P[Python: lib/devbase] - F[生成物: .docker-compose.scale.yml] - end - subgraph Docker の実行基盤 - D1[dev-1..N] - D2[プロファイルのサービス] - end - W -->|引数と env| P - P -->|読む| F - P -->|コマンド列 + 機密の環境変数| D2 - P -.->|触らない| D1 -``` - -境界をまたぐのは 2 つである。Compose へ渡すコマンド列と、`_inject_secrets` がプロセスの環境へ載せた機密である。この環境からは `COMPOSE_PROFILES` を取り除く。有効なプロファイルを決めるのは devbase の `--profile` だけにするためである(決定 7)。プロファイルの操作はサービス名をすべて明示して渡す。起動はさらに `--no-deps` を付ける。停止は対象を広げない `stop` と `rm -f` を使う(決定 5)。そのため dev-1..N はどちらの操作の対象にも入らない。 - -## 置き場所 - -```text -lib/devbase/ -├── cli.py # profile サブコマンドの登録(変更) -├── commands/ -│ └── container.py # cmd_profile_up / down / list、default_services、_compose_run、dispatch(変更) -├── project/ -│ └── runtime.py # hook_env に有効なプロファイルを足す(変更) -├── tui/ -│ └── actions_project.py # 起動中の行の操作へ profile の 2 項目を足す(変更) -├── utils/ -│ └── docker.py # docker_compose が env= を組んで打ち消し用のプロファイル名を入れる、docker_compose_up が対象サービスを受ける、docker_compose_down が全プロファイルを対象にする(変更) -└── volume/ - └── compose.py # profiles を保つことの確認のみ(変更なし) - -tests/ -├── cli/ -│ └── tui/ -│ └── test_profile_menu.py # 新設(項目の出し分けと委譲の属性の検査) -├── commands/ -│ └── test_container_profile.py # 新設 -├── utils/ -│ └── test_docker_profiles.py # 新設(子プロセスの env の COMPOSE_PROFILES が打ち消し用のプロファイル名になることの検査) -└── volume/ - └── test_compose_profiles.py # 新設 -``` - -## 構造 - -型は追加しない。変わるのは処理の順序と、関数の責務の境目である。 - -| 関数 | 変更 | 責務 | -| --- | --- | --- | -| `profile_services(compose_file, environ) -> dict[str, list[str]]` | 新設(`commands/container.py`) | `config --profiles` で名前を取り、各プロファイル X について `--profile X config --services` を呼び、`config --services`(既定のサービス)を差し引いて対応を作る(決定 1)。デーモンへの接続は要らない | -| `default_services(compose_file, environ) -> list[str]` | 新設(`commands/container.py`) | `config --services` を打ち消し用のプロファイル名を入れた環境で呼び、既定のサービス名を返す。`cmd_up` が起動の対象として渡す(決定 1・決定 7) | -| `cmd_profile_up(profile, context)` | 新設 | F1。プロファイルのサービスをすべて明示し、`--no-deps` を付けて起動する(決定 2)。終了コードを返す | -| `cmd_profile_down(profile, context)` | 新設 | F2。`docker_compose_down` は通さず、`stop` と `rm -f` の 2 段をサービス名付きで組む(決定 5)。終了コードを返す | -| `cmd_profile_list(context)` | 新設 | F3。終了コードを返す | -| `docker_compose(command, ...)` | 変更(`utils/docker.py`) | F1 / F2 / F4 の共通の土台。現在は `subprocess.run(cmd, ...)` を `env=` なしで呼ぶ。`env=` を新たに構築し、`COMPOSE_PROFILES` へ打ち消し用のプロファイル名 `__devbase_none__` を入れて渡す(決定 7)。コマンド列の組み立て方は変えない | -| `docker_compose_up(compose_file, detach=True, services=())` | 変更(`utils/docker.py`) | 起動の対象を受け取り、`['up', '-d', *services]` を組む。空なら現在と同じ `['up', '-d']` になる | -| `_compose_run(subcommand, ...)` | 変更(`commands/container.py:269`) | `docker_compose` を経由せず直接 `subprocess.run` する経路(`ps` / `logs`)。同じ `env=` を組み立てて渡す(決定 7) | -| `_resolve_dev_service` / `_read_compose_services` | 変更(`commands/container.py`) | `config --format json` を直接呼ぶ経路。同じ `env=` を渡し、読むサービスの集合を devbase が決める(決定 7) | -| エディタを開く経路の `ps` | 変更(`editor/opener.py:395`) | `ps --format json` を直接呼ぶ経路。同じ `env=` を渡す(決定 7) | -| `_run_deploy_pipeline(...)` | 変更(`commands/container.py`) | 生成した `.docker-compose.scale.yml` を `config --services` へ渡して既定のサービスを求め、`docker_compose_up` へ渡す | -| `docker_compose_down(compose_file)` | 変更(`utils/docker.py`) | F4。引数は増やさず、内部で無条件に `--profile '*'` を足す。`['down', '-t0']` という固定の形は変えない(決定 5) | -| `hook_env(config, active_profiles=())` | 変更(`project/runtime.py`) | F5。`DEVBASE_ACTIVE_PROFILES` を足す。`_run_pre_up_hook` と `_run_deploy_script_for_instances` の両方に効く | -| `_run_deploy_script_for_instances(deploy_script, indices, config=None, active_profiles=()) -> bool` | 変更(`commands/container.py`) | 全インスタンスで成功したかを返す。`active_profiles` を受け取り、加工せず `hook_env` へ渡す。`cmd_up` は戻り値を使わず、現在の警告だけの扱いを保つ | -| `_dispatch_lifecycle` | 変更 | `profile` を handlers へ 1 つ足し、`args.profile_subcommand` で 3 つの入口へ振り分ける | - -`active_profiles` は `cmd_profile_up` からフックまで、引数として順に手渡す。`_run_deploy_script_for_instances` がこの引数を持たないと、`hook_env` へ値を届ける経路が無い。既存の呼び出しを壊さないため、既定は空のタプルとする。 - -| 呼び出し元 | 渡す値 | `./deploy` が受け取る `DEVBASE_ACTIVE_PROFILES` | -| --- | --- | --- | -| `cmd_up` | 省略(既定の `()`) | 空文字列 | -| `cmd_profile_up` | `(X,)`(起動したプロファイル名) | `X` | - -`_run_deploy_script_for_instances` は受け取った値を加工せず `hook_env(config, active_profiles=active_profiles)` へ渡す。環境変数の名前と区切り(カンマ)を決めるのは `hook_env` だけである。 - -この変更では、渡る値は常にプロファイル名 1 つである。`cmd_profile_up` が受ける名前が 1 つだけで、同時に 2 つ以上を起動する操作を作らないためである。カンマ区切りは将来の拡張のための予約であり、現時点でその形になる経路は無い(「未確認のまま残ること」)。 - -`_run_deploy_script_for_instances` へ渡す `indices` は、**操作に使う生成物が持つ開発コンテナの番号**である。`.docker-compose.scale.yml` の `services` から `<開発サービス名>-1`..`<開発サービス名>-N` を読み、その番号を渡す。 - -**開発サービス名は固定ではない。** `get_dev_service_name()`(`DEV_SERVICE_NAME` または既定の `dev`)が返す名前を使う。生成処理も同じ名前で複製するため(`volume/compose.py` の `_build_scaled_services`)、`dev-*` を固定で探すと、既定以外の名前を使うプロジェクトで 1 件も拾えない。 - -`project.yml` の `config.scale` は使わない。`up` の後に `project.yml` を書き換えてから `profile up X` を呼ぶと、設定の値と稼働中のインスタンスが食い違う。減らした後なら稼働中の `dev-2` へフックが走らず、増やした後なら作られていない番号へ走る。生成物は `up` が作ったもので、稼働中の構成と一致する。 - -| 何を数えるか | 減らした後 | 増やした後 | -| --- | --- | --- | -| `project.yml` の `config.scale` | 稼働中の 2 台目を飛ばす | 未作成の番号へ走る | -| 生成物の開発コンテナ | 稼働中の全インスタンスへ走る | 同左 | - -`cmd_up` は生成の直後に呼ぶため、どちらの数え方でも同じ値になる。こちらは現在の実装を変えない。 - -`profile` の subparser は `dest` を親と分ける。親の `project` / `container` は `dest='subcommand'` のままとし、入れ子側は `dest='profile_subcommand'` を使う。`cli.py` の `_dispatch` は `args.subcommand == 'list'` を見て `project list`(プロジェクト一覧)へ振り分けるためである。入れ子で `subcommand` を再利用すると、`devbase project profile list` がそちらへ流れてしまう。`_dispatch_lifecycle` の handlers には `'profile'` を 1 つだけ足す。その中で `profile_subcommand` を見て up / down / list を選ぶ。 - -## 入出力の契約 - -仕様記述の置き場所は設けない。OpenAPI の対象になる経路が無く、変わる約束はコマンドだけである。 - -### 変わる約束 - -| 名前 | 入力 | 出力(成功) | 失敗の形 | -| --- | --- | --- | --- | -| `devbase project profile up [name] ` | プロファイル名(必須)、プロジェクト名(省略時は現在地) | 対象サービスを起動し、フックを実行して 0 | 構成ファイルが無い / 未知のプロファイル / Compose かフックが失敗 → 1 | -| `devbase project profile down [name] ` | 同上 | 対象サービスを停止し、そのコンテナを削除して 0 | 同上。`stop` と `rm -f` のどちらかが失敗すれば 1(フックは呼ばない) | -| `devbase project profile list [name]` | プロジェクト名(省略可) | プロファイルと稼働状況を表で出して 0 | 構成ファイルが無い → 1。デーモンへ接続できないときは稼働状況を `不明` にして 0 | -| `devbase container profile up ` / `down ` / `list`(`ct` も同じ) | プロファイル名のみ。プロジェクト名は受け付けない | 同上。非推奨の警告を 1 行出す | 同上 | - -`project` の `[name]` と `` の並びは既存の `scale` と同じ規則に従う。値が 1 個ならプロファイル名に割り当てられる。2 個なら(プロジェクト名、プロファイル名)になる。 - -`container` / `ct` は `[name]` を持たない。値は常にプロファイル名である。`container` 群のサブコマンドは現在地のプロジェクトで動く既存の規約に従う。この非対称は `project` / `container` の既存の作りと同じである。 - -### 一覧の操作メニュー - -`devbase list` で起動中の行を選ぶと操作の一覧が出る。そこへ 2 項目を足す。 - -| 項目 | 選んだ後 | 実行後 | -| --- | --- | --- | -| テスト用サーバ起動 (profile up) | プロファイル名を選ばせ、`profile up` を実行する | 一覧へ戻る | -| テスト用サーバ停止 (profile down) | 同じくプロファイル名を選ばせ、`profile down` を実行する | 一覧へ戻る | - -**2 項目が出るのは、そのプロジェクトがプロファイルを持つときだけである**(決定 8)。持たないプロジェクトでは一覧の中身が現在と同じになる。 - -プロファイル名の選択は、名前が 1 つだけのときも選択として出す。名前は `profile_services` が返す一覧を使う(決定 1)。生成物が無い(`devbase up` の前)ときは 2 項目を出さない。解決にデーモンは要らないため、接続できない状態でも項目は出る。 - -実行は `dispatch_lifecycle('profile', name, profile_subcommand='up', profile='<名前>')` で共有のハンドラへ渡す。TUI はコマンドの中身を持たない。 - -### プロファイルの決め方 - -有効なプロファイルは devbase が経路ごとに明示して決める。利用者の環境や `.env` の値で、起動する対象が変わらないようにする(決定 7)。 - -| 経路 | `--profile` | 対象の渡し方 | `COMPOSE_PROFILES` | -| --- | --- | --- | --- | -| `devbase up` の起動 | 付けない | 既定のサービスをすべて明示する | 打ち消し用のプロファイル名 `__devbase_none__` を入れる | -| `devbase down` と `up` 冒頭の停止 | `--profile '*'` | 渡さない | 同上 | -| `profile up X` / `profile down X` | `--profile X` | 対象のサービスをすべて明示する | 同上 | - -`COMPOSE_PROFILES` は空文字列にしない。どのプロジェクトも定義しない打ち消し用のプロファイル名を入れる。空文字列の扱いが版で違う可能性を調べずに済むためである。 - -**キーを外すだけでは足りない。** Compose は環境変数が無ければプロジェクトの `.env` を読む。値を入れて上書きし、あわせて `devbase up` の起動では既定のサービス名も明示する(決定 7)。 - -### `profile list` の表 - -| 列 | 内容 | -| --- | --- | -| PROFILE | プロファイル名。生成物に現れた順で並べる | -| SERVICES | そのプロファイルに属するサービス名。宣言順にカンマ区切りで並べる | -| RUNNING | `稼働数/総数` と状態語。例: `3/3 running`、`1/3 partial`、`0/3 stopped` | - -状態語の決め方は次のとおりである。 - -| 稼働数 | 状態語 | -| --- | --- | -| 総数と同じ | `running` | -| 1 以上で総数未満 | `partial` | -| 0 | `stopped` | - -稼働の判定には `docker compose ps --format json` を使う。`State` が `running` のサービスだけを数える。`exited` や `paused` は稼働に数えない。プロファイルが 1 つも無ければ、見出しの行だけを出して 0 で終わる。 - -### 互換性の扱い - -| 変更 | 既存の呼び出し側への影響 | -| --- | --- | -| `profile` サブコマンドの追加 | 無い。既存の引数の形を変えない | -| `docker compose down` に `--profile '*'` を付ける | プロファイルを持たないプロジェクトでは対象が同じになる。Docker Compose v5.1.4 で確認済み。2.20.0 以上 5.x 未満は未検証 | -| `DEVBASE_ACTIVE_PROFILES` の追加 | 無い。既存のフックは読まなければ従来どおり動く | -| `COMPOSE_PROFILES` を子プロセスへ渡さない | devbase 経由の Compose だけが対象。素の `docker compose` を手で叩く経路には影響しない | -| `devbase up` の起動に既定のサービス名を明示する | 対象は現在と同じ(`profiles` を持たないサービスの全件)。生成物は `up` のたびに作り直すため、一覧が古くなることはない | - -`--profile '*'` の行だけは、影響の範囲が広い。この経路は `devbase down` と `devbase up` 冒頭の停止に入るため、プロファイルを使わない既存の全プロジェクトを通る。だから「退行しないこと」の受け入れ条件に直結する。 - -対象が変わらないと言えるのは、確かめた版だけである。ワイルドカードを解釈しない版で `*` がリテラルのプロファイル名として扱われるかは「未確認のまま残ること」に載せたままであり、ここでも断定しない。 - -| 版 | `--profile '*'` を付けた `down` の対象 | 根拠 | -| --- | --- | --- | -| v5.1.4 | 現在と同じ | 手元で確認済み | -| 2.20.0 以上 5.x 未満 | 現在と同じになる想定 | 未検証 | - -`bin/devbase` の `_PROJECT_NAME_SUBCOMMANDS` は変えない。現在の中身は `up down ps logs scale rebuild` である。この一覧は「3 番目の引数をプロジェクト名として解決してよいサブコマンド」を表す。`profile` ではその位置に `up` / `down` / `list` が来る。一覧へ足すと、`up` という名前のプロジェクトが実在したときにそちらへ移動してしまう。プロジェクト名の解決は Python 側の `_dispatch_lifecycle` に任せる。 - -同じ一覧から `login` / `build` も外れている。この 2 つは `project` でも `[name]` を受け付けない。`profile` が `container` で `[name]` を受け付けないのも同じ筋である。 - -### 検査の手段 - -`uv run pytest tests/commands/test_container_profile.py -q` が、組み立てたコマンド列と終了コードを検査する。`uv run pytest tests/utils/test_docker_profiles.py -q` が、子プロセスへ渡す環境の `COMPOSE_PROFILES` が打ち消し用のプロファイル名になっていることを検査する。同じテストで、`devbase up` の起動が既定のサービス名をすべて渡すことも検査する。 - -## 処理の流れ - -```mermaid -sequenceDiagram - participant U as 利用者 - participant CLI as 引数の受け口 - participant OP as プロファイルの操作 - participant RS as プロファイルの解決 - participant DC as Compose の呼び出し - participant HK as フックの実行 - U->>CLI: devbase project profile up local-app - CLI->>OP: cmd_profile_up("local-app") - OP->>OP: 接続先を反映し機密を注入する - OP->>RS: 生成済みの構成を読む - alt 構成ファイルが無い - RS-->>OP: 無し - OP-->>U: devbase up を促して 1 - else 未知のプロファイル - RS-->>OP: 該当なし - OP-->>U: 既知の名前を並べて 1 - else - RS-->>OP: サービス名の集合 - OP->>DC: compose --profile local-app up -d --no-deps <サービス...> - DC-->>OP: 終了コード - OP->>HK: ./deploy(DEVBASE_ACTIVE_PROFILES=local-app) - HK-->>OP: 全インスタンスの成否 - OP-->>U: 0 または 1 - end -``` - -停止(F2)は同じ並びから、フックの呼び出しを除いたものである。Compose を呼ぶ回数だけが 2 回になる。 - -```mermaid -sequenceDiagram - participant U as 利用者 - participant OP as プロファイルの操作 - participant RS as プロファイルの解決 - participant DC as Compose の呼び出し - U->>OP: cmd_profile_down("local-app") - OP->>RS: 生成済みの構成を読む - RS-->>OP: サービス名の集合 - OP->>DC: compose --profile local-app stop <サービス...> - DC-->>OP: 終了コード - alt stop が失敗 - OP-->>U: 1(rm は呼ばない) - else - OP->>DC: compose --profile local-app rm -f <サービス...> - DC-->>OP: 終了コード - OP-->>U: 0 または 1 - end -``` - -`down` は使わない。`docker_compose_down` も通らない。理由は決定 5 に書く。 - -`--profile` は subcommand より前に置く必要がある。`docker_compose` は `-f` の後に、渡された配列をそのまま並べる。そのため `--profile X` を配列の先頭へ入れる。組み立てるコマンド列は次のとおりである。`<サービス...>` はどれもプロファイル X に属するサービスの全件である(決定 2)。 - -| 操作 | コマンド列 | 子プロセスの `COMPOSE_PROFILES` | -| --- | --- | --- | -| プロファイルの起動 | `['--profile', X, 'up', '-d', '--no-deps', <サービス...>]` | 打ち消し用の名前 | -| プロファイルの停止(1 段目) | `['--profile', X, 'stop', <サービス...>]` | 打ち消し用の名前 | -| プロファイルの停止(2 段目) | `['--profile', X, 'rm', '-f', <サービス...>]` | 打ち消し用の名前 | -| `devbase up` の起動 | `['up', '-d', <既定のサービス...>]`(`--profile` を付けない) | 打ち消し用の名前 | -| `devbase down` / `up` 冒頭の停止 | `['--profile', '*', 'down', '-t0']` | 打ち消し用の名前 | - -`devbase up` の `<既定のサービス...>` は、`config --services` が返す既定のサービスの全件である(決定 1)。`--profile` を付けないことと合わせて、プロファイルのサービスは対象に入らない。 - -`up` と `down` の冒頭の停止(F4)は次のように変わる。`COMPOSE_PROFILES` の除去は `docker_compose` と `_compose_run` の両方に共通で効く(決定 7)。 - -```mermaid -graph LR - A[devbase down] --> B["env の COMPOSE_PROFILES へ打ち消し用の名前を入れる → compose --profile '*' down -t0"] - C[devbase up] --> D["env の COMPOSE_PROFILES へ打ち消し用の名前を入れる → compose --profile '*' down -t0"] - D --> E["env の COMPOSE_PROFILES へ打ち消し用の名前を入れる → compose up -d 既定のサービス一覧、--profile なし"] - E --> F[既定のサービスだけが動く] -``` - -この上書きが無いと、`COMPOSE_PROFILES=test` を持つ端末で `devbase up` が test のサービスまで起動する。`up` 冒頭の停止は `--profile '*'` で全部を落とすため、残骸ではなく新しい起動として現れる。`.env` に書かれた値も、環境変数が優先される規則で無効になる(決定 7)。 - -## 非機能の実現方式 - -| 大項目 | 要求の条件 | 実現方式 | 確かめ方 | -| --- | --- | --- | --- | -| 運用・保守性 | プロファイルのサービスを起動・停止した記録が、既存のログと同じ体裁(`logger.info`)で残る | 対象サービス名とプロファイル名を `logger.info` で 1 行ずつ出す。Compose の出力はそのまま標準出力へ流す | `caplog` で起動・停止の各 1 行を検査する | -| セキュリティ | プロファイルのサービスへ渡す機密は、そのサービスが元々 `env_file` で参照していた由来のキーだけに限る | 生成済みの `.docker-compose.scale.yml` をそのまま使う。機密の列挙はこのファイルが既に持つため、プロファイルの操作は新しい注入経路を作らない。実行前に `_prepare_compose` で既存と同じ注入を通す | 生成物のサービスごとの `environment` が、プロファイルの有無で変わらないことをテストで検査する | -| システム環境 | Docker Compose 2.20.0 以上で動く。devbase が使うのは `--profile '*'`、`--no-deps`、サービスを明示した `up` / `stop` / `rm -f` である | 最低対応版を 2.20.0 とする。`--no-deps` は起動にだけ、`--profile '*'` は全体の停止にだけ使う。プロファイルの停止は `stop` と `rm -f` で行い、`down` のサービス指定は使わない(決定 5)。ワイルドカードを解釈しない版では「`*` という名前のプロファイル」として扱われ、対象が現在と同じになる想定である(未検証)。あわせて `docker_compose` が `COMPOSE_PROFILES` へ打ち消し用のプロファイル名を入れ、`devbase up` の起動では既定のサービスを明示する(決定 7)。サービスを明示した `up` も 2 系全般で使えるため、どちらも版の下限を作らない | v5.1.4 で全サービスが消えることを手動で確かめる。同じ版で、プロファイルを持たないプロジェクトの `up` / `down` が従来どおり動くことも確かめる。`COMPOSE_PROFILES=test` を環境変数と `.env` の両方に置いて `devbase up` を通し、既定のサービスだけが動くことも確かめる。`--profile '*'` が使える最古の版は公式ドキュメントに記載が無く、2.20.0 以上 5.x 未満は未検証のまま「未確認のまま残ること」に載せる | - -起動で既定のサービスを対象から外すのは `--no-deps` である。停止で対象から外すのは、サービスを明示した `stop` / `rm -f` である。どれも古くからある形で、版の下限を作らない。下限を決めるのは `depends_on.required` だけである。この属性は 2.20.0 からの機能で、受け入れ条件と文書の構成例が使う。そのため 2.20.0 未満では、構成の検証そのものに失敗する。 - -| 版 | 扱い | -| --- | --- | -| 2.20.0 以上 | 対応する。手元で確かめたのは v5.1.4 | -| 2.20.0 未満の v2 | 対象外。`required: false` を書いた構成の検証に失敗する | - -`depends_on.required: false` を書くのはプロジェクト側であり、devbase の実装には現れない。文書で案内する。ただし dev を対象から外す働きは持たない。その役目は `--no-deps` が担う(決定 2)。 - -## 決定の記録 - -結論と理由、採らなかった案は [PLAN58_compose-profiles-decisions.md](PLAN58_compose-profiles-decisions.md) にある。決定は 8 件である。 - -## テスト設計 - -| 受け入れ条件 | 何で確かめるか | -| --- | --- | -| `profiles` 付きのサービスは `devbase up` で起動しない | Compose の既定の挙動。生成物が `profiles` を保つことを `tests/volume/test_compose_profiles.py` で検査する | -| `profile up X` で対象サービスだけが起動する | `subprocess.run` を差し替え、組み立てた引数列が `--profile X up -d --no-deps <対象サービス...>` であり、dev を含まないことを検査する | -| プロファイルのサービスをすべて渡す | 同じ引数列に、プロファイル X に属するサービスが全件並ぶことを検査する。1 つでも欠けると `--no-deps` で起動しないため(決定 2) | -| 既定のサービスの Container ID と `StartedAt` が変わらない | 手動確認(`alpine:3` の最小構成で `docker inspect` の値を前後で比較する) | -| `depends_on: dev` を持つプロファイルサービスで dev が再作成されない | 手動確認。`depends_on: {dev: {condition: service_started, required: false}}` の構成と `depends_on: [dev]` の構成の両方で `profile up X` を実行し、dev-1..N の Container ID と `StartedAt` を前後で比べる | -| dev の環境変数の値が変わっても dev が再作成されない | 手動確認。機密など dev の環境変数の値を変えてから `profile up X` を実行し、dev-1..N の Container ID と `StartedAt` を前後で比べる。`--no-deps` が無い組み立てでは再作成が起きることも確かめ、この選択肢が要ることを示す | -| 生成物が `depends_on` の `required` を保つ | `generate_scaled_compose` の出力で、`depends_on: {dev: {condition: service_started, required: false}}` が `dev-1`..`dev-N` へ写り、各要素に `condition` と `required` が残ることを検査する | -| `profile down X` でコンテナが削除され、ボリュームは残る | 2 回の呼び出しが `--profile X stop <対象サービス...>` と `--profile X rm -f <対象サービス...>` であり、`down` も `-v` / `--volumes` も含まないことを検査する。`stop` が失敗したときに `rm` が呼ばれず 1 で終わることも見る | -| dev がプロファイルのサービスへ `depends_on` を持っても dev が止まらない | 手動確認。dev に `depends_on: {db: {condition: service_started, required: false}}` を書いた構成で `profile up X` → `profile down X` を通し、dev-1..N の Container ID と `StartedAt` を停止の前後で比べる。`down db` の組み立てでは dev が消えることも確かめ、`stop` / `rm` へ寄せる必要を示す(決定 5) | -| `profile list` がプロファイル名と稼働状況を出す | 稼働中のサービスを返す偽の `docker compose ps` を与え、出力の行を検査する。全稼働・一部稼働・全停止の 3 通りで `3/3 running` / `1/3 partial` / `0/3 stopped` を確かめる | -| `project profile list` が `project list` へ流れない | `profile_subcommand` の分離を、`devbase project profile list` の解析結果と呼ばれたハンドラで検査する | -| 構成ファイルが無いときに 1 で止まる | 空の一時ディレクトリで呼び、終了コードと、コンテナを作る呼び出しが発生しないことを検査する | -| 未知のプロファイル名で 1 で止まる | 既知の名前が出力に並ぶことと終了コードを検査する | -| `project profile up <名前> X` が同じ結果になる | 引数の解釈(`[name] ` の割り当て)を `tests/cli` の既存の書き方で検査する | -| `devbase down` が全プロファイルを消し、0 で終わる | 引数列に `--profile *` が含まれることを検査する。全体の削除は手動確認 | -| `devbase up` の後は既定のサービスだけが動く | 冒頭の停止が `--profile *` を通ることと、起動の引数列に `--profile` が入らないことを検査する。実際の状態は手動確認 | -| `COMPOSE_PROFILES` が環境変数に設定されていても `devbase up` は既定のサービスだけを起動する | `COMPOSE_PROFILES=test` を `monkeypatch.setenv` で置き、`docker_compose` と `_compose_run` が組み立てた `env` の値が `__devbase_none__` であることを検査する。`up` の起動・`down`・`profile up` / `down` の各経路で見る。実際の状態は手動確認 | -| `.env` に `COMPOSE_PROFILES` が書かれていても `devbase up` は既定のサービスだけを起動する | 組み立てた `env` の値と、`up -d` の引数へ並ぶ既定のサービス名を検査する。既定のサービスがプロファイルのサービスへ `depends_on` を持つ構成での実際の起動は手動確認で見る | -| フックが `DEVBASE_ACTIVE_PROFILES` を受け取る | `hook_env` の戻り値と、`subprocess.run` へ渡された `env` を検査する。`cmd_up` 経由は空文字列、`cmd_profile_up` 経由はプロファイル名 1 つになることを見る。複数値はこの範囲では作れないため検査しない | -| フックの失敗が終了コードへ出る | 失敗する `./deploy` を置き、戻り値が 1 になることを検査する | -| `profiles` に変数の式が書かれていても名前が一致する | `config --profiles` と `config --services` の出力を差し替え、`profile_services` が展開後の名前で対応を作ることを検査する。実際の展開は Compose が行うため、式を持つ構成での `list` と `profile up test` は手動確認で見る | -| `container profile` / `ct profile` が非推奨の警告を出す | 両方の入口を呼び、警告が 1 行だけ出ることと、委譲先の引数が `project profile` と同じであることを `caplog` で検査する | -| `DEV_SERVICE_NAME` が既定以外でもフックが全インスタンスへ走る | `DEV_SERVICE_NAME=workspace` と scale 2 の生成物を与え、`_run_deploy_script_for_instances` へ渡る番号が 1 と 2 になることを検査する | -| デーモンへ接続できないとき `profile list` が稼働状況を `不明` にして 0 で終わる | `ps` が失敗する状態を与え、名前と対応が出ること、稼働状況の列が `不明` になること、終了コードが 0 であることを検査する | -| デーモンへ接続できないとき `profile up` / `down` が 1 で終わる | Compose の終了コード 1 をそのまま返すことを検査する | -| 一覧の操作に 2 項目が並ぶ | プロファイルを持つ構成で `_running_ops` の戻り値を検査する | -| プロファイルを持たないプロジェクトでは 2 項目が出ない | 同じ関数へプロファイルの無い構成を与え、現在と同じ並びになることを検査する | -| 一覧から実行しても dev が変わらない | 委譲へ渡る属性が `profile` のサブコマンドと名前であることを検査する。実際の状態は手動確認 | -| プロファイルを持たないプロジェクトの挙動が変わらない | 既存の `tests/commands/test_container_up_order.py` と `tests/cli/test_up_roundtrips.py` が通ること | -| プロファイルを持たないプロジェクトで `up` / `down` が従来どおり動く | 手動確認(確認済みの v5.1.4 で実施)。`profiles:` を持たない既存のプロジェクトで `devbase up` と `devbase down` を通し、起動するコンテナの集合と `down` 後に残らないことを確かめる。`--profile '*'` がこの経路に入るため | -| 生成物が `profiles` を保つ | `generate_scaled_compose` の出力を読み、非 dev サービスの `profiles` が残ることを検査する | - -テストは実 docker と実 `DEVBASE_ROOT` に触れない。`subprocess.run` を差し替える。作業ディレクトリは `tmp_path` を使う。 - -## 未確認のまま残ること - -| 項目 | 内容 | -| --- | --- | -| `--profile '*'` が使える最古の版 | 公式ドキュメント([profiles](https://docs.docker.com/compose/how-tos/profiles/))に版の記載が無く、確かめられなかった。最低対応版は `depends_on.required` の 2.20.0 を根拠に定めた | -| 古い Compose での `--profile '*'` | 確認済みは v5.1.4 のみ。2.20.0 以上 5.x 未満は未検証。ワイルドカードを解釈しない版で `*` がリテラルのプロファイル名として扱われるか(対象が現在と同じに留まる想定)は未確認 | -| プロファイルが複数同時に有効な場合 | 同時に 2 つ以上を起動する操作は作らない。よって `DEVBASE_ACTIVE_PROFILES` は常に単一値で、カンマ区切りは将来の拡張のための予約である。`profile up` を 2 回呼ぶと、2 回目のフックへ渡るのは 2 つ目の名前だけになる | diff --git a/issues/PLAN58_compose-profiles-impl.md b/issues/PLAN58_compose-profiles-impl.md deleted file mode 100644 index cb6d55de..00000000 --- a/issues/PLAN58_compose-profiles-impl.md +++ /dev/null @@ -1,141 +0,0 @@ -# PLAN58: Compose の profiles で付随サービス群を後から起動・停止する — 実装計画 - -## 関連リンク - -- 課題: devbasex/devbase#189 -- 要求と受け入れ条件: [PLAN58_compose-profiles.md](PLAN58_compose-profiles.md) -- 設計: [PLAN58_compose-profiles-design.md](PLAN58_compose-profiles-design.md) -- 決定の記録: [PLAN58_compose-profiles-decisions.md](PLAN58_compose-profiles-decisions.md) -- 設計 Pull Request: devbasex/devbase#190(マージ済み・2026-09-17 承認) - -## モード - -`standard`。公開インタフェース(`devbase project profile` / `container profile`、TUI の操作メニュー、フックの環境変数)を追加し、`up` / `down` の本番の振る舞いを変える。 - -## 目的と非目的 - -達成したい状態は要求仕様の「目的」のとおりである。受け入れ条件・前提・対象範囲は要求仕様を唯一の置き場とし、ここへ写さない。 - -やらないこと(要求仕様の「含まない」に加えて、この計画で決めたもの): - -- `cmd_scale` が直接呼ぶ `compose up -d --no-recreate` の `env=`(要求仕様で範囲外) -- `docker_compose_down` の引数追加(決定 5) -- `bin/devbase` の `_PROJECT_NAME_SUBCOMMANDS` の変更(決定 6) - -## 修正対象 - -| ファイル | 変更 | -| --- | --- | -| `lib/devbase/utils/docker.py` | `compose_env()` を新設。`docker_compose` が `env=` を渡す。`docker_compose_up` が `services` を受ける。`docker_compose_down` が `--profile '*'` を足す | -| `lib/devbase/commands/container.py` | `default_services` / `profile_services` / `_dev_instance_indices` / `cmd_profile_up` / `cmd_profile_down` / `cmd_profile_list` を新設。`_compose_run` / `_resolve_dev_service` / `_read_compose_services` が `env=` を渡す。`_run_deploy_pipeline` が既定のサービスを渡す。`_run_deploy_script_for_instances` が成否と `active_profiles` を持つ。`_dispatch_lifecycle` に `profile` | -| `lib/devbase/editor/opener.py` | `ps --format json` の呼び出しへ `env=` を渡す | -| `lib/devbase/project/runtime.py` | `hook_env(config, active_profiles=())` | -| `lib/devbase/cli.py` | `project profile` / `container profile` の subparser(`dest='profile_subcommand'`)、`SUBCMD_MAP` | -| `lib/devbase/tui/actions_project.py` | `_running_ops(devbase_root, name)` で 2 項目を出し分け、プロファイル名の選択と委譲 | -| `docs/plugin-dev/compose-profiles.md` | 新設。プロジェクト作者向けの書き方(`profiles:`、`required: false`、予約名 `__devbase_none__`、最低対応版 2.20.0) | -| `docs/plugin-dev/quickstart.md` | フックへ渡る環境変数の表へ `DEVBASE_ACTIVE_PROFILES` | -| `tests/utils/test_docker_profiles.py` | 新設 | -| `tests/commands/test_container_profile.py` | 新設 | -| `tests/cli/tui/test_profile_menu.py` | 新設 | -| `tests/volume/test_compose_profiles.py` | 新設 | -| `tests/commands/test_hook_env.py` | `DEVBASE_ACTIVE_PROFILES` の検査を追加 | -| 既存の `up` のテストの harness(`test_container_up_order.py` / `test_up_roundtrips.py` / `test_container_context.py` / `test_container_bao.py` / `tui/test_dispatch.py`) | `default_services` を差し替える(実 docker へ問い合わせないため) | - -## タスク分解 - -機能単位で切る。各タスクは「失敗するテスト → 通す最小実装 → 整理」で進め、終わるたびに `uv run pytest tests/ -q` を通す。 - -### Task 1: devbase 経由の Compose へ打ち消し用のプロファイル名を渡す(決定 7 の 1) - -- **対象:** `utils/docker.py`、`commands/container.py`(`_compose_run` / `_resolve_dev_service` / `_read_compose_services`)、`editor/opener.py` -- **変更:** `compose_env(environ=None) -> dict` を `utils/docker.py` に置き、`os.environ` の複製の `COMPOSE_PROFILES` を `__devbase_none__` にして返す。定数 `NO_PROFILE = '__devbase_none__'`。5 経路が `env=compose_env()` を渡す -- **満たす受け入れ条件:** 停止の網羅「`COMPOSE_PROFILES` が端末の環境変数に設定…」「`.env` に書かれた…」の自動検査の部分 -- **テスト:** `COMPOSE_PROFILES=test` を `monkeypatch.setenv` で置き、各経路の `subprocess.run` に渡った `env['COMPOSE_PROFILES']` が打ち消し用の名前であること。他の環境変数が保たれること - -### Task 2: プロファイルの解決(決定 1) - -- **対象:** `commands/container.py` -- **変更:** `default_services(compose_file, environ=None) -> list[str]`(`config --services`)と `profile_services(compose_file, environ=None) -> dict[str, list[str]]`(`config --profiles` → 各 X で `--profile X config --services` から既定を差し引く)。どちらも `compose_env` を渡し、失敗は `DevbaseError` にする(呼び出し側で扱いを分ける)。並びは Compose の出力順を保つ -- **満たす受け入れ条件:** `profile list` の名前と対応、変数の式を含む `profiles` の名前一致(テスト設計の該当行) -- **テスト:** `subprocess.run` を偽物に差し替え、`config --profiles` / `config --services` の出力から対応が作られること。引数列に `-f <生成物>` と `--profile X` が subcommand より前に並ぶこと - -### Task 3: `up` の起動は既定のサービスを明示し、停止は全プロファイルを対象にする(F4、決定 4・7 の 2) - -- **対象:** `utils/docker.py`(`docker_compose_up` / `docker_compose_down`)、`commands/container.py`(`_run_deploy_pipeline`)、既存テストの harness -- **変更:** `docker_compose_up(compose_file, detach=True, services=())` が `['up', '-d', *services]` を組む。`docker_compose_down` は `['--profile', '*', 'down', '-t0']`。`_run_deploy_pipeline` は生成直後に `default_services(override_file)` を求めて渡す -- **満たす受け入れ条件:** 停止の網羅の 6 件(自動検査の部分)、退行しないこと -- **テスト:** `docker_compose_down` の引数列、`docker_compose_up` の引数列(空なら従来どおり `['up', '-d']`)、`_run_deploy_pipeline` が既定のサービスを `docker_compose_up` へ渡すこと、起動の引数列に `--profile` が入らないこと - -### Task 4: フックへ有効なプロファイルを伝える(F5、決定 3) - -- **対象:** `project/runtime.py`、`commands/container.py`(`_run_deploy_script_for_instances`) -- **変更:** `hook_env(config, active_profiles=())` が `DEVBASE_ACTIVE_PROFILES=','.join(active_profiles)` を足す。`_run_deploy_script_for_instances(..., active_profiles=()) -> bool`。`config` が無いときも `DEVBASE_ACTIVE_PROFILES` は渡す。`cmd_up` / `cmd_scale` は戻り値を使わない -- **満たす受け入れ条件:** フックの「`./pre-up` と `./deploy` は受け取る。`up` からは空」、「フックの失敗が終了コードへ出る」の関数側 -- **テスト:** `tests/commands/test_hook_env.py` の既存の dump スクリプトで値を確かめる。失敗する `./deploy` で `False` が返ること - -### Task 5: `cmd_profile_up` / `cmd_profile_down` / `cmd_profile_list`(F1〜F3、決定 2・5) - -- **対象:** `commands/container.py` -- **変更:** - - 共通の前段: `_prepare_compose(context)` → 生成物が無ければ `devbase up` を促して 1 → `profile_services` → 未知の名前なら既知の一覧を出して 1 - - `up`: `docker_compose(['--profile', X, 'up', '-d', '--no-deps', *services], check=False)`。0 以外ならその終了コードを返す。`./deploy` があれば `_dev_instance_indices(生成物)`(`get_dev_service_name()` の `<名前>-<数字>` を生成物の `services` から読む)へ `active_profiles=(X,)` で走らせ、1 つでも失敗すれば 1 - - `down`: `stop` → 失敗なら 1(`rm` を呼ばない)→ `rm -f`。フックは呼ばない - - `list`: `ps --format json` を `compose_env` で呼び、`State == running` のサービスを数えて `PROFILE / SERVICES / RUNNING` の表を出す。`ps` が失敗したら RUNNING を `不明` にして 0。生成物が無ければ 1 - - 起動・停止の対象を `logger.info` で 1 行ずつ残す -- **満たす受け入れ条件:** 起動と停止の自動検査の部分すべて、フックの `profile up` 側(`DEVBASE_ACTIVE_PROFILES=X`、生成物の番号で全インスタンス、`DEV_SERVICE_NAME=workspace`、`./pre-up` を呼ばない、失敗で 0 以外) -- **テスト:** `tests/commands/test_container_profile.py`。偽の `subprocess.run` で引数列・呼び出し回数・終了コード・出力を検査する(テスト設計の表の該当行を 1 つずつ) - -### Task 6: 引数の受け口(決定 6) - -- **対象:** `cli.py`、`commands/container.py`(`_dispatch_lifecycle`) -- **変更:** `project profile {up,down} [name] ` と `project profile list [name]`、`container profile {up,down} ` / `list`。入れ子は `dest='profile_subcommand'`。`--context` も付ける。`_dispatch_lifecycle` の handlers へ `'profile'` を足し、`profile_subcommand` で振り分ける。`SUBCMD_MAP` の `project` / `container` へ `profile` を足す -- **満たす受け入れ条件:** `project profile up <プロジェクト> X` が現在地と同じ結果、`container` / `ct` が同じ結果で非推奨の警告 1 行、`project profile list` が `project list` へ流れない -- **テスト:** 既存の `tests/cli/test_project_dispatch.py` の書き方に合わせ、解析結果と呼ばれたハンドラの引数を検査する - -### Task 7: 一覧の操作メニュー(F6、決定 8) - -- **対象:** `tui/actions_project.py` -- **変更:** `_running_ops(devbase_root, name) -> list` を新設し、`projects//.docker-compose.scale.yml` があり `profile_services` が 1 件以上返すときだけ「テスト用サーバ起動 (profile up)」「テスト用サーバ停止 (profile down)」を足す。解決はそのプロジェクトのディレクトリを作業ディレクトリにして行い、失敗は「持たない」として扱う。選択後は `menu.select` でプロファイル名を選ばせ(1 件でも選択を出す)、`dispatch_lifecycle('profile', name, profile_subcommand=..., profile=...)` へ委譲する。2 項目は `_BACK_TO_TOP_OPS` に入れる -- **満たす受け入れ条件:** TUI の 6 件の自動検査の部分 -- **テスト:** `tests/cli/tui/test_profile_menu.py`。`profile_services` を差し替えて項目の出し分け、委譲の属性、`back_after` を検査する。フォールバック(番号入力)は既存テストが通ること - -### Task 8: 生成物が `profiles` と `depends_on.required` を保つことの固定 - -- **対象:** `tests/volume/test_compose_profiles.py`(実装の変更は想定しない) -- **満たす受け入れ条件:** 退行しないこと「生成物は `profiles:` を保つ」、テスト設計「生成物が `depends_on` の `required` を保つ」 -- **進め方:** 現状固定テスト。失敗したときだけ `volume/compose.py` を直す - -### Task 9: プロジェクト作者向けの文書 - -- **対象:** `docs/plugin-dev/compose-profiles.md`(新設)、`docs/plugin-dev/quickstart.md`、`docs/README.md` の索引 -- **満たす受け入れ条件:** 対象範囲「プロファイルを使うプロジェクト作者向けの文書」 -- **進め方:** テスト駆動は当たらない(文書)。コマンド例は Task 6 の実装と突き合わせる - -### Task 10: 手動確認 - -- **対象:** 要求仕様「検証手段」の手動確認 7 行 -- **進め方:** `alpine:3` の最小構成を scratchpad に作り、この作業ツリーの `bin/devbase` で通す。結果(Container ID / `StartedAt` の前後)は Pull Request 本文へ貼る。実 docker を使うため、自分のプロジェクトとは別の `COMPOSE_PROJECT_NAME` で行う - -## 実装中に範囲へ入れたもの - -- `tui/dispatch.py` の `_preserve_cwd_env()` が機密の注入履歴を戻さず、TUI で別プロジェクトを 2 回続けて操作すると最初のプロジェクト固有の機密が次の Compose へ渡る欠陥(PR 前からの `dispatch_lifecycle` 経路にもある)。Task 7 のメニュー表示時の照会が同じ欠陥を操作前に踏ませるため、原因と形が同じとして範囲に入れ、`runtime.snapshot_injected` / `restore_injected` で両経路を 1 か所で直した(PR #191 レビュー round 3) - -## リスクと対処 - -| リスク | 対処 | -| --- | --- | -| `commands/container.py` は 1962 行あり、Task 1〜6 の多くが触る | 先に整える対象にはしない。追加は既存の関数の並び(ヘルパー → dispatch → `cmd_*`)へ足し、構造の見直しは構造改善(`cross-refactoring`)へ回す。タスクごとに全テストを通す | -| `_run_deploy_pipeline` が `config` を呼ぶため、既存テストの harness が実 docker へ問い合わせる | Task 3 で各 harness へ `default_services` の差し替えを足す。足し漏れは `docker` の無い環境で失敗として現れるため、差し替え前にテストを流して対象を洗い出す | -| TUI で行を選ぶたびに `docker compose config` が 2 回以上走る | 起動中の行を選んだときだけ呼ぶ。遅さが目立てば構造改善で対応を検討する | -| 生成物が `${VAR:?}` を含み、機密を注入しない TUI の解決で `config` が失敗する | 失敗は「プロファイルを持たない」として 2 項目を出さない。CLI の `profile list` では注入済みで呼ぶ | -| `--profile '*'` を古い Compose が解釈しない | 設計の「未確認のまま残ること」のまま。手動確認は v5.1.4 で行う | - -## 切り戻し手順 - -データ移行は無い。Pull Request を revert すれば戻る。プロジェクト側の `compose.yml` に書いた `profiles:` は、revert 後の devbase では従来の Compose の挙動(`up` で起動しない・`down` で残る)に戻る。 - -## 完了の定義 - -- [ ] 要求仕様の受け入れ条件がすべて満たされ、条件ごとに自動テストか手動確認の結果が対応している -- [ ] `uv run pytest tests/ -q` と `uv run ruff check lib/ tests/` が通る -- [ ] 手動確認 7 行の結果を Pull Request 本文へ記録した diff --git a/issues/PLAN58_compose-profiles.md b/issues/PLAN58_compose-profiles.md deleted file mode 100644 index 7fefee43..00000000 --- a/issues/PLAN58_compose-profiles.md +++ /dev/null @@ -1,194 +0,0 @@ -# PLAN58: Compose の profiles で付随サービス群を後から起動・停止する - -対象 issue: devbasex/devbase#189 -関連: volareinc/devbase-ext#31(carmo-system-console 側の適用) - -## 依頼(原文) - -> carmo-system-consoleでは、devコンテナと同時にテスト用のapp / dbコンテナなども全て立ち上がってしまいますが、defaultはdevコンテナだけとし、 -> あとから追加でテスト用サーバ群も立ち上げられるようにしたい。 -> -> 方法を大まかに検討してissuesを起票してください - -> 追加: -> テストコンテナ群は群後から起動も停止もできるようにしたい(この際、devコンテナの稼働には影響がないようにすること) - -## 目的 - -dev のほかに app / db などのサービスを持つプロジェクトで、`devbase up` の既定では dev だけを起動し、付随するサービス群は後から起動・停止できるようにする。その起動・停止で dev コンテナを再作成も再起動もしない。 - -判定に使うのは Compose の `profiles` である。プロジェクトは `compose.yml` の各サービスへ `profiles:` を書くだけでよく、devbase 側は「プロファイルを指定して起動・停止する口」と「停止を全プロファイルへ効かせる」ことを担う。 - -## 前提 - -- 前提 1: プロファイル名は devbase が決めず、プロジェクトが `compose.yml` に書いた名前をそのまま受ける。devbase は既定のプロファイル名を持たない -- 前提 2: プロファイル付きのサービスは scale の対象にしない。dev だけが `dev-1`..`dev-N` へ複製される現在の仕組みは変えない -- 前提 3: `devbase up` はテスト用サーバが起動していても、既定の状態(dev だけ)へ揃える。継続したい利用者は `up` の後にもう一度プロファイルを起動する(利用者の指示、2026-09-16) -- 前提 4: プロファイルを使っていないプロジェクトの `up` / `down` / `scale` の挙動は変えない -- 前提 5: プロファイルの起動は `--no-deps` を付けて Compose を呼ぶ。そのため既定のサービス(dev を含む)は操作の対象に入らない。あわせて、そのプロファイルに属するサービスをすべて明示して渡す。`--no-deps` は依存先を自動起動しないためである。1 つでも渡し漏らすと、そのサービスは起動しない(設計の決定 2) - -## 対象範囲 - -含む: - -- `profiles:` 付きのサービスを `devbase up` で起動しないこと(Compose の既定の挙動をそのまま通す) -- プロファイル単位で起動・停止するコマンド -- 停止(`devbase down` と `up` 冒頭の停止)を全プロファイルへ効かせること -- プロジェクトのフックへ、有効なプロファイルを伝えること -- `devbase list` の TUI から、プロファイルを起動・停止できること -- プロファイルを使うプロジェクト作者向けの文書 - -含まない: - -- プロファイル付きサービスの scale(インスタンスごとの複製) -- `project.yml` で既定の有効プロファイルを宣言する仕組み(必要になってから別 issue で扱う) -- `devbase scale` が直接呼ぶ `compose up -d --no-recreate` の `COMPOSE_PROFILES` の扱い(プロファイルの入口ではない。必要になってから別 issue で扱う) -- carmo-system-console 側の `compose.yml` / フックの書き換え(volareinc/devbase-ext#31) -- 新しい型・クラスの追加。実装は既存のモジュール関数の並びに足す(そのためクラス図を作らない) -- 永続データの追加・変更(そのため ER 図・テーブル定義・CRUD 図を作らない) -- 画面の追加・変更(そのため画面一覧・遷移図を作らない) - -## 用語 - -| 用語 | 意味 | -| --- | --- | -| プロファイル | Compose の `profiles:` に書いた名前。付随サービス群をまとめる単位 | -| 既定のサービス | `profiles:` を持たないサービス。`devbase up` で起動する | -| プロファイルのサービス | そのプロファイルに属し、既定のサービスに含まれないサービス | -| 打ち消し用のプロファイル名 | `__devbase_none__`。どのプロジェクトも定義しない名前で、devbase が `COMPOSE_PROFILES` へ入れて利用者の指定を無効にする(設計の決定 7) | - -## 受け入れ条件 - -以下で `compose.yml` と書くのは、利用者がプロファイルを宣言する場所のことである。devbase が実際に読むのは、その宣言を引き継いだ生成物 `.docker-compose.scale.yml` である(設計の決定 1)。 - -起動と停止: - -- [ ] `profiles: [X]` を持つサービスは `devbase up` で起動せず、`docker ps` に現れない -- [ ] 前提: `devbase up` が済み、既定のサービスだけが動いている - 操作: `devbase project profile up X` を実行する - 結果: プロファイル X のサービスだけが起動し、既定のサービスの Container ID と `StartedAt` は変わらない -- [ ] 前提: プロファイル X のサービスが動いている - 操作: `devbase project profile down X` を実行する - 結果: プロファイル X のサービスのコンテナだけが削除され、既定のサービスの Container ID と `StartedAt` は変わらない -- [ ] `devbase project profile up X` は、プロファイル X に属するサービスをすべて Compose へ渡し、`--no-deps` を付ける -- [ ] 前提: プロファイル X のサービスが `depends_on: {dev: {condition: service_started, required: false}}` を持つ - 操作: `devbase project profile up X` を実行する - 結果: 既定のサービスの Container ID と `StartedAt` が変わらない -- [ ] `depends_on: [dev]`(`required` を書かない形)を持つサービスでも、`devbase project profile up X` で既定のサービスの Container ID と `StartedAt` は変わらない -- [ ] dev の環境変数の値を変えた後でも、`devbase project profile up X` は dev を再作成しない -- [ ] 前提: dev が `depends_on: {db: {condition: service_started, required: false}}` を持ち、db はプロファイル X に属する - 操作: `devbase project profile down X` を実行する - 結果: dev の Container ID と `StartedAt` が前後で変わらない -- [ ] `devbase project profile down X` の後も、そのサービスが使う名前付きボリュームは残る -- [ ] `devbase project profile list` は、`compose.yml` に書かれたプロファイルの名前と、そのサービスが稼働しているかを出す -- [ ] `.docker-compose.scale.yml` が無い状態では、`devbase project profile up X` / `down X` / `list` のいずれも終了コード 1 で止まる。`devbase up` を促すメッセージを出し、コンテナは作らない -- [ ] `compose.yml` に無いプロファイル名を `up` / `down` へ渡すと、存在する名前の一覧を出して終了コード 1 で止まる -- [ ] Docker のデーモンへ接続できない状態では、`devbase project profile up X` / `down X` は Compose の終了コード 1 をそのまま返す -- [ ] 同じ状態で `devbase project profile list` は、プロファイル名と対応を出し、稼働状況の列を `不明` にして終了コード 0 で終わる -- [ ] `devbase project profile up <プロジェクト> X` の結果は、そのプロジェクトのディレクトリで `devbase project profile up X` を実行した場合と同じになる。`down` と `list` も同じである -- [ ] `devbase container profile ...` と `devbase ct profile ...` は `devbase project profile ...` と同じ結果になる。非推奨の警告を 1 行出す - -停止の網羅: - -- [ ] プロファイル X のサービスが起動している状態で `devbase down` を実行すると、既定のサービスとプロファイル X のサービスの両方が削除され、終了コード 0 で終わる -- [ ] 同じ状態で `devbase up` を実行すると、冒頭の停止でプロファイル X のサービスも止まり、起動後は既定のサービスだけが動いている -- [ ] `COMPOSE_PROFILES` が端末の環境変数に設定された状態でも、`devbase up` は既定のサービスだけを起動する -- [ ] プロジェクトの `.env` に `COMPOSE_PROFILES` が書かれた状態でも、`devbase up` は既定のサービスだけを起動する(設計の決定 7) -- [ ] 前提: 既定のサービスがプロファイル X のサービスへ `depends_on` を持ち、`.env` に `COMPOSE_PROFILES=X` が書かれている - 操作: `devbase up` を実行する - 結果: プロファイル X のサービスは起動しない -- [ ] `devbase up` の起動は、生成物の `profiles` を持たないサービスをすべてサービス名として Compose へ渡す - -TUI: - -- [ ] `devbase list` で起動中のプロジェクトを選ぶと、操作のメニューに「テスト用サーバ起動 (profile up)」と「テスト用サーバ停止 (profile down)」が並ぶ -- [ ] `compose.yml` にプロファイルを 1 つも持たないプロジェクトでは、その 2 項目が出ない -- [ ] 項目を選ぶとプロファイル名の選択が出る。名前が 1 つだけのときもその 1 件の選択として出す -- [ ] TUI から起動・停止した後は一覧へ戻り、STATUS のコンテナ数が実際の数に変わる -- [ ] TUI から起動・停止しても、dev-1..N の Container ID と `StartedAt` は変わらない -- [ ] questionary が無い端末の代替経路(番号入力して `up`)の挙動は変わらない - -フック: - -- [ ] `./pre-up` と `./deploy` は `DEVBASE_ACTIVE_PROFILES` を受け取る。`devbase up` から呼ばれるときは、どちらも空である -- [ ] `devbase project profile up X` の後の `./deploy` は `DEVBASE_ACTIVE_PROFILES=X` を受け取る。値はプロファイル名 1 つである -- [ ] 前提: `devbase up` を scale 2 で通した後、`project.yml` の `scale` を 1 へ書き換える - 操作: `devbase project profile up X` を実行する - 結果: 稼働中の 2 つの開発コンテナの両方で `./deploy` が実行される -- [ ] `DEV_SERVICE_NAME` を既定以外(例: `workspace`)にしたプロジェクトでも、scale 2 の状態で `devbase project profile up X` の後に 2 インスタンスとも `./deploy` が実行される -- [ ] 同時に 2 つ以上のプロファイルを起動する操作は作らない。よって複数の値が渡る経路は無い。カンマ区切りは将来の拡張のための予約であり、この変更では受け入れ条件にしない -- [ ] `devbase project profile up X` は `./pre-up` を呼ばない -- [ ] `devbase project profile up X` は、サービスの起動が終わった後にプロジェクトのフックを呼ぶ。フックが終了コード 0 以外を返したら、コマンドも 0 以外で終わる - -退行しないこと: - -- [ ] `profiles:` を 1 つも持たないプロジェクトで、`devbase up` / `down` / `scale` が起動するコンテナの集合と順序が現在と変わらない -- [ ] 生成される `.docker-compose.scale.yml` は、プロファイル付きのサービスについても `profiles:` を保ったまま出力する - -## 非機能の条件 - -| 大項目 | 条件 | -| --- | --- | -| 運用・保守性 | プロファイルのサービスを起動・停止した記録が、既存のログと同じ体裁(`logger.info`)で残る | -| セキュリティ | プロファイルのサービスへ渡す機密は、そのサービスが元々 `env_file` で参照していた由来のキーだけに限る(`_services_receiving_secrets` の現在の規則を変えない)。素の `docker compose` を使わず devbase を通すのは、機密の注入と対象サービスの限定をこの規則の中で行うためである | -| システム環境 | Docker Compose 2.20.0 以上で動く。devbase が使うのは `--profile '*'`、`--no-deps`、サービスを明示した `up` / `stop` / `rm -f` である。案内する `depends_on.required` が 2.20.0 以上を要するため、2.20.0 未満は対象外とする | -| 再現性 | 有効なプロファイルは devbase が決める。子プロセスの `COMPOSE_PROFILES` へ打ち消し用のプロファイル名を入れ、`devbase up` の起動では対象のサービスも明示する。利用者の設定に結果が左右されない。端末の環境変数でも `.env` でも同じである(設計の決定 7) | - -最低対応版を 2.20.0 とする根拠は次のとおりである。 - -| 使う機能 | 使える版 | 根拠 | -| --- | --- | --- | -| `--no-deps` | 2 系全般 | `docker compose up` の古くからある選択肢。版の下限を作らない | -| `up <サービス...>` | 2 系全般 | `docker compose up` は古くからサービス名の位置引数を受ける。版の下限を作らない | -| `stop <サービス...>` / `rm -f <サービス...>` | 2 系全般 | どちらもサービス指定が古くから安定している。版の下限を作らない | -| `depends_on.required` | 2.20.0 以上 | [公式仕様](https://docs.docker.com/reference/compose-file/services/#depends_on)に「Introduced in Docker Compose version 2.20.0」とある | -| `--profile '*'` | v5.1.4 で確認済み。2.20.0 以上 5.x 未満は未検証 | 公式ドキュメントに版の記載が無い(設計の「未確認のまま残ること」) | - -`down` の `[SERVICES]` 位置引数は使わない。プロファイルの停止は `stop` と `rm -f` の 2 段で行う(設計の決定 5)。この位置引数は比較的新しい追加で、対応版を調べないと下限を決められない。加えて、指定したサービスへ `depends_on` を持つ側まで対象に含める。使わないことで、どちらの問題も起きない。 - -版の下限を作る機能は `depends_on.required` だけである。`--no-deps` も、サービスを明示した `up` / `stop` / `rm -f` も 2 系全般で使える。`--profile '*'` は停止の対象を広げる向きの指定であり、解釈しない版でも対象が現在と同じになる想定のため、下限を引き上げない(未検証。設計の「未確認のまま残ること」)。よって 2.20.0 という下限は `depends_on.required` だけで閉じる。2.20.0 未満の v2 では、`required: false` を書いた構成の検証に失敗する。受け入れ条件と文書の例はこの属性を使う。だから「機能は落ちるが壊れない」とは言わず、対象外と定める。動作を確かめたのは Docker Compose v5.1.4 である。 - -## 影響 - -| 対象 | 影響 | -| --- | --- | -| 公開インタフェース | `devbase container profile` と `devbase project profile` を追加する。`devbase list` の TUI の操作メニューに 2 項目を足す。既存のコマンドの引数は変えない。`devbase down` は内部で `--profile '*'` を付ける | -| データ | なし(スキーマも名前付きボリュームの構成も変えない) | -| 既存の振る舞い | `down` が全プロファイルを対象にする。devbase 経由の Compose へ渡る `COMPOSE_PROFILES` が打ち消し用のプロファイル名になる。`up` の起動は既定のサービスを明示して渡す(対象は現在と同じ)。フックへ渡す環境変数が 1 つ増える。`profiles:` を使っていないプロジェクトでは、どちらも対象が変わらない。`--profile '*'` を確認済みなのは v5.1.4 で、2.20.0 以上 5.x 未満は未検証である | - -## 検証手段 - -| 項目 | 手段 | -| --- | --- | -| テスト(TUI) | `uv run pytest tests/cli/tui -q`(メニューの項目と、委譲へ渡す属性の検査) | -| テスト | `uv run pytest tests/ -q`(コマンドの組み立てと生成物の検証。実 docker には触れない) | -| 静的解析 | `uv run ruff check lib/ tests/`(設定がある場合。無ければ省く) | -| 手動確認 | `profiles` を付けた最小の compose(dev / app、`alpine:3`)で `devbase up` → `devbase project profile up X` → `devbase project profile down X` → `devbase down` を通し、各段で `docker ps` の Container ID と `StartedAt` を記録する。app へ `depends_on: {dev: {condition: service_started, required: false}}` を付けた版と、`depends_on: [dev]` を付けた版でも同じ手順を通す。さらに dev の環境変数の値を変えてから `profile up X` を実行し、dev が再作成されないことを確かめる | -| 手動確認(依存の向きが逆の構成) | dev へ `depends_on: {db: {condition: service_started, required: false}}` を書き、db をプロファイル X に入れた構成で `profile up X` → `profile down X` を通す。停止の前後で dev の Container ID と `StartedAt` が変わらないことを見る。`stop` / `rm -f` が依存元を対象に含めないことの確認である(設計の決定 5) | -| 手動確認(`COMPOSE_PROFILES` が設定された端末) | `COMPOSE_PROFILES=X` を環境変数に設定した状態と、プロジェクトの `.env` に書いた状態の両方で `devbase up` を通す。`docker ps` に既定のサービスだけが並ぶことを見る。`.env` の側は、起動のコマンド列に既定のサービス名が並ぶことも見る(設計の決定 7)。続けて `profile up X` → `profile down X` が従来どおり効くことも確かめる | -| 手動確認(TUI) | `devbase list` を開き、起動中のプロジェクトで「テスト用サーバ起動」→ 一覧の STATUS のコンテナ数が増えることを見る。続けて「テスト用サーバ停止」で戻ることも見る。前後で dev-1..N の Container ID と `StartedAt` を比べる | -| 手動確認(退行) | 確認済みの Docker Compose v5.1.4 で実施する。`profiles:` を持たないプロジェクトで `devbase up` と `devbase down` を通し、従来どおり動くことを確かめる。起動するコンテナの集合と順序、`down` 後に何も残らないことを見る。`--profile '*'` がこの経路に入るためである | - -自動テストで dev の Container ID の不変を確かめることはできない(実コンテナが要る)。この条件は手動確認で判定する。 - -## 前提とする取り決め - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | CLI の実装は `lib/devbase/` 配下(CONTRIBUTING.md)。コマンドの追加は `lib/devbase/cli.py` と `lib/devbase/commands/container.py`、compose の生成物に関わる部分は `lib/devbase/volume/compose.py` | -| コーディング規約 | Python は PEP 8(CONTRIBUTING.md)。文書は `docs/` 配下、図は Mermaid | -| テスト戦略 | `tests/commands/` と `tests/volume/` の既存の書き方に合わせ、`subprocess` を差し替えて **組み立てたコマンド列**を検証する。実 docker と実 DEVBASE_ROOT には触れない(テストは実環境の `DEVBASE_ROOT` を継承するため、backend を隔離する) | - -## 境界 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 既存テストの実行、変更範囲内の命名統一、日本語のコメントと文書 | -| 確認してから行う | 既存コマンドの引数の変更、`project.yml` のスキーマ追加、フック名の新設 | -| 行わない | 依頼範囲外のリファクタリング、`.docker-compose.scale.yml` の生成規則のうちプロファイルに関係しない部分の変更 | - -## 未決 - -| 項目 | 誰が決めるか | 期限 | -| --- | --- | --- | -| プロファイル起動の後に呼ぶフックを `./deploy` の再利用にするか、新しい名前にするか | 設計工程(`design`)で決める | 実装計画の前 |