diff --git a/docs/specifications/base-image-chain-build.md b/docs/specifications/base-image-chain-build.md index a792d0ff..0e080ee5 100644 --- a/docs/specifications/base-image-chain-build.md +++ b/docs/specifications/base-image-chain-build.md @@ -12,7 +12,7 @@ Dockerfile から直の親イメージを読む規則(直の親の読み方) (`bin/devbase`)と `--expires` の判定(`lib/devbase`)が同じ Dockerfile から同じ直の親を読む。 読む Dockerfile(プロジェクトの Dockerfile)は、`docker compose build` が建てる開発サービス -(`DEV_SERVICE_NAME`、既定 `dev`)の `build` から決める。compose の構成は `docker compose config` で読み、 +(`DEV_SERVICE_NAME`。未設定か空なら `dev`)の `build` から決める。compose の構成は `docker compose config` で読み、 通常のビルドと `--expires` の判定が同じ決め方(Dockerfile の場所の決め方)を使う。 利用者向けの説明は @@ -431,10 +431,11 @@ compose の理由(`required variable MISSING is missing a value: need it` な ### 決定 6: `docker compose build` が建てるサービスと同じサービスを見るため、開発サービス名は `bin/devbase` が `--service` で渡す -`bin/devbase` は `${DEV_SERVICE_NAME:-dev}`(空なら `dev`)を `docker compose build` に渡すが、Python の -`get_dev_service_name()` は空の値をそのまま返す。入口が自分で名前を決めると、`DEV_SERVICE_NAME=` と空で書いた -プロジェクトで、建てるサービスと場所を決めるサービスが分かれる。`--expires` の判定が空の値で開発サービスを -見失う件は #425 にある。 +`bin/devbase` は `${DEV_SERVICE_NAME:-dev}`(未設定か空なら `dev`)を `docker compose build` に渡し、同じ値を +`--service` で入口へ渡す。建てるサービスと場所を決めるサービスが、1 つの値から決まる。入口が自分で名前を決めると、 +2 つが揃うかは Bash と Python の 2 つの読み方が同じ規則であることに頼る。Python の `get_dev_service_name()` も +未設定か空なら `dev` と読み、今は 2 つの読み方は揃っている([開発サービス名の読み方](dev-service-name.md)、#425)が、 +読み方が将来分かれても建てるサービスと場所を決めるサービスが分かれないよう、`--service` で渡す形を保つ。 ### 決定 7: 実行時用の `.env` がまだ無いプロジェクトでも建てられるよう、入口は `--no-env-resolution` で構成を読む @@ -447,6 +448,7 @@ compose の理由(`required variable MISSING is missing a value: need it` な - [base イメージの中身の絞り込み](base-image-contents.md)(派生イメージの一覧) - [base イメージの文字の描画と、文書を扱う道具](base-image-rendering.md)(ブラウザの派生イメージ) - [位置引数の解決(プロジェクト名・イメージ名)](cli-argument-resolution.md)(名前の形 `is_single_segment_name`) +- [開発サービス名の読み方(`DEV_SERVICE_NAME`)](dev-service-name.md)(`--service` で渡す名前の決め方) - [用語集: イメージの継承(`image-lineage`)](../glossary.md#イメージの継承image-lineage) - [コンテナ操作ガイド](../user/container-operations.md) - 実装 PR: devbasex/devbase#427(#415)・devbasex/devbase#432(`--no-env-resolution`) diff --git a/docs/specifications/dev-service-name.md b/docs/specifications/dev-service-name.md new file mode 100644 index 00000000..c9e1ddb0 --- /dev/null +++ b/docs/specifications/dev-service-name.md @@ -0,0 +1,135 @@ +# 開発サービス名の読み方(`DEV_SERVICE_NAME`) + +## 概要 + +プロジェクトの compose の構成で、どのサービスを開発サービスとして扱うかは、プロジェクトの `env` に書く +環境変数 `DEV_SERVICE_NAME` で決める。**未設定か空(長さ 0 の文字列)なら `dev`、それ以外は値をそのまま使う。** + +この名前は 2 つの実行の単位がそれぞれ決める。`bin/devbase`(Bash)は `${DEV_SERVICE_NAME:-dev}` で、 +`lib/devbase`(Python)は `lib/devbase/volume/compose.py` の `get_dev_service_name()` で決め、2 つは同じ値から +同じ名前を得る。`DEV_SERVICE_NAME=` と空で書いたプロジェクトでも、`build` が建てるサービスと、`up`・`login`・ +`open`・`post-start`・`scale`・`--expires` の判定・token の配布が扱うサービスは同じ `dev` になる。 + +## 用語 + +開発サービス名の定義は [用語集: Compose の構成(`compose`)](../glossary.md#compose-の構成compose) にある。 + +## コンテキスト + +変更は Compose の構成(`compose`)のコンテキストに閉じる。開発サービス名を使う側(コマンドの入口 `cli`・ +イメージの継承 `image-lineage`)は名前を受け取るだけで、読み方を自分では持たない。 + +## 背景 + +`bin/devbase` は以前から空の値を `dev` と読んでいたが、Python の `get_dev_service_name()` は空の値をそのまま +返していた。`DEV_SERVICE_NAME=` と書いたプロジェクトでは、`build` は `dev` を建てるのに、Python の経路は空の名前の +サービスを探して開発サービスを見失った(#425)。Python の側を `bin/devbase` の読み方へ揃え、`bin/devbase` は +変えていない。 + +## 対象範囲 + +- `lib/devbase/volume/compose.py` の `get_dev_service_name()` が、`DEV_SERVICE_NAME` から開発サービス名を決める規則 +- `bin/devbase` の `${DEV_SERVICE_NAME:-dev}` と同じ値を決めること + +含まない: + +- 空白だけの値・サービス名に使えない文字を含む値の検証(名前の形の検証) +- `DEV_SERVICE_NAME` 以外の環境変数の空の値の扱い +- 空の値を書いたプロジェクトへの警告 + +## 構成要素 + +| 要素 | 置き場所 | 責務 | +| --- | --- | --- | +| Python の読み方 | `lib/devbase/volume/compose.py` の `get_dev_service_name()` | 環境変数から開発サービス名を決める。Python で `DEV_SERVICE_NAME` を読む唯一の場所 | +| Bash の読み方 | `bin/devbase` の `cmd_build` の `${DEV_SERVICE_NAME:-dev}` | 名前を決め、`docker compose build` と `project_dockerfile --service` へ渡す([派生イメージの継承の連なりのビルド](base-image-chain-build.md)の決定 6) | +| 呼び出し側 | `lib/devbase/commands/container.py` の `cmd_up`・`cmd_login`・`cmd_open`・`cmd_post_start`・`cmd_scale`・`_resolve_dev_service`・`_ensure_images`・`_dev_instance_indices`・`_maybe_open_editor`・`_open_editor_at`、`lib/devbase/commands/env.py` の `_push_token_to_running`、`lib/devbase/volume/compose.py` の `generate_scaled_compose` | 名前を受け取り、compose の構成から開発サービスを引く。空の値を自分では確かめない | + +`DEV_SERVICE_NAME` は `bin/devbase` が `env` を `set -a` で読んで環境へ載せるほか、Python の `_load_project_env` +(`lib/devbase/commands/container.py`)も `env` を読み直して載せる。どちらの経路でも空の値は環境変数として +Python へ渡り、`get_dev_service_name()` が `dev` と読む。 + +```mermaid +graph LR + ENV["プロジェクトの env
DEV_SERVICE_NAME="] + B["bin/devbase の cmd_build
${DEV_SERVICE_NAME:-dev}"] + G["get_dev_service_name()"] + C[呼び出し側] + PD[project_dockerfile] + DC[docker compose] + ENV -->|set -a で読む| B + ENV -->|set -a / _load_project_env| G + B -->|--service dev| PD + B -->|build dev| DC + G -->|dev| C + C -->|config / up / exec| DC +``` + +## 仕様 + +### 読み方 + +| `DEV_SERVICE_NAME` の値 | 開発サービス名 | +| --- | --- | +| 未設定 | `dev` | +| 空(`''`) | `dev` | +| 空白だけ(`' '` など) | 値のまま(`' '`)。前後の空白を削らない | +| それ以外(`app` など) | 値のまま | + +失敗の形は無く、名前は必ず決まる。構成にその名前のサービスが無いときの扱いは呼び出し側が持つ。 +呼び出しのたびに環境変数から決め直し、保存しない。 + +### 常に成り立つ条件 + +| # | 条件 | 破れたときの扱い | +| --- | --- | --- | +| I1 | `DEV_SERVICE_NAME` が未設定か空(長さ 0)なら、名前は `dev` である | 単体のテストが落ちる | +| I2 | `DEV_SERVICE_NAME` が空でなければ、名前はその値を変えずに使う(空白だけの値も前後の空白を削らない) | 単体のテストが落ちる | +| I3 | 同じ `DEV_SERVICE_NAME` の値から、`bin/devbase` の `${DEV_SERVICE_NAME:-dev}` と `get_dev_service_name()` は同じ名前を決める | I1・I2 の単体のテストが、Bash の `${VAR:-word}` の規則(未設定か空のときだけ置き換え、空白だけの値は置き換えない)と同じ表で縛る | +| I4 | `lib/devbase/` で `DEV_SERVICE_NAME` を読むのは `volume/compose.py` の 1 か所だけである | 環境変数の読み取りを集める検査のテストが落ちる | + +### 設計の判断 + +- **空の値を `dev` と読む規則は `get_dev_service_name()` の中だけに置く。** 呼び出し側は 12 か所あり、どれもこの関数から + 名前を受け取るため、関数の中に置けば呼び出し側を変えずにすべての経路が揃う。`bin/devbase` で空の値を消してから + Python へ渡す形では、`_load_project_env` が `env` を読み直す経路で空の値が戻る +- **空の判定は長さ 0 だけにし、前後の空白を削らない。** `${DEV_SERVICE_NAME:-dev}` は空白だけの値を置き換えない。 + Python だけが空白を削ると、空白だけの値で `build` が建てるサービスと Python が探すサービスが分かれる(I3 が破れる) +- **読み取りは `os.environ.get('DEV_SERVICE_NAME') or 'dev'` の形で関数の中に置く。** 環境変数の読み取りを集める検査 + ([テストの環境の隔離](test-environment-isolation.md#読み取りの集合の集め方))は `os.environ.get(N, ...)` の形を + `ast` で拾う。この形を保てば、検査が読み取りを拾う場所は `volume/compose.py` のままになる + +## データ・設定 + +| 名前 | 向き | 意味 | +| --- | --- | --- | +| `DEV_SERVICE_NAME` | 入力(プロジェクトの `env`、任意) | 開発サービス名。未設定か空なら `dev` | + +## 運用 + +- `DEV_SERVICE_NAME=` と空で書いたプロジェクトは、書かないプロジェクトと同じく `dev` を開発サービスとして扱う +- 空白だけの値は空として扱わない。サービス名として使えない値を書いたときの扱いは呼び出し側(構成にそのサービスが無い)に従う + +## テスト観点 + +`tests/volume/test_compose_dev_service_name.py`: + +- `get_dev_service_name()` が、未設定で `'dev'`、空で `'dev'`、`app` で `'app'`、空白 1 文字で `' '` を返すこと(I1・I2。 + 関数を `os.environ.get('DEV_SERVICE_NAME', 'dev')` に戻すと空の値の場合が落ちる) +- 構成に `dev` と `redis` があり `DEV_SERVICE_NAME` が空のとき、`generate_scaled_compose(scale=2)` が `dev` を + `dev-1`・`dev-2` へ複製し、`dev` を残さないこと(呼び出し側の経路が `get_dev_service_name()` を差し替えずに通る) + +`tests/test_env_isolation.py`: + +- `test_collector_finds_known_reads` が `DEV_SERVICE_NAME` の読み取りを `volume/compose.py` に見つけること(I4) +- `test_test_side_setenv_wins` が `monkeypatch.setenv('DEV_SERVICE_NAME', 'x')` の後に `get_dev_service_name()` から + `'x'` を得ること + +手動の確認: 作業用のプロジェクトの `env` に `DEV_SERVICE_NAME=` と書き、`devbase login` が `dev` のコンテナへ入ること。 + +## 関連リンク + +- [付随サービス群の後からの起動・停止(Compose の profiles)](compose-profiles.md)(開発サービス名を生成物で `<開発サービス名>-1`..`-N` へ複製する) +- [派生イメージの継承の連なりのビルド](base-image-chain-build.md)(`build` が開発サービス名を `--service` で渡す、決定 6) +- [テストの環境の隔離(環境変数と HOME)](test-environment-isolation.md)(環境変数の読み取りを集める検査) +- 実装 PR: devbasex/devbase#438(#425) diff --git a/docs/specifications/snapshot-series.md b/docs/specifications/snapshot-series.md index 37547550..8b76ef02 100644 --- a/docs/specifications/snapshot-series.md +++ b/docs/specifications/snapshot-series.md @@ -82,7 +82,7 @@ classDiagram +series_latest(volumes) dict|None +auto_snapshot_target(max_incrementals) str|None +last_snapshot_time(volumes) datetime|None - +rotate(keep, max_total) int + +rotate(keep, max_total) RotateResult -_rotation_plan(snapshots, keep, max_total) list -_safe_snap_dir(name) Path -_series_entries(volumes) list diff --git a/docs/specifications/test-environment-isolation.md b/docs/specifications/test-environment-isolation.md index 4e935e14..df0a67f4 100644 --- a/docs/specifications/test-environment-isolation.md +++ b/docs/specifications/test-environment-isolation.md @@ -290,7 +290,7 @@ I4 が保証するのは session の段が終わった時点までである。 ```text 隔離の一覧に無い環境変数が 1 個ある -DEV_SERVICE_NAME: volume/compose.py:37 +DEV_SERVICE_NAME: volume/compose.py:41 tests/conftest.py の ISOLATED_ENV か NOT_ISOLATED_ENV(理由つき)に足す ``` diff --git a/issues/issue-425-design.md b/issues/issue-425-design.md deleted file mode 100644 index 4db48e9e..00000000 --- a/issues/issue-425-design.md +++ /dev/null @@ -1,221 +0,0 @@ -# devbase: `DEV_SERVICE_NAME=` と空で書いたプロジェクトで、`build` は `dev` を建てるのに `up`・`login`・`--expires` の判定などは空の名前のサービスを探して開発サービスを見失う → 空の値は未設定と同じく `dev` と読み、どの経路も同じ開発サービスを使う(#425) - -## 目的 - -- **何が壊れているか**: `bin/devbase` は `${DEV_SERVICE_NAME:-dev}` で空の値を `dev` と読むが、Python の `get_dev_service_name()` は空の値をそのまま返す。同じ `env` から 2 つの開発サービス名が生まれる -- **誰が困るか**: プロジェクトの `env` に `DEV_SERVICE_NAME=` と空で書いた利用者。`build` は通るのに、`up` のイメージの確かめ・`login`・`open`・`post-start`・`scale`・`--expires` の判定・トークンの配布が開発サービスを見つけられない -- **直すと何が成り立つか**: 空の値は未設定と同じく `dev` になり、`bin/devbase` と Python が同じ `env` から同じ開発サービス名を得る - -## 適用範囲 - -- **働く範囲**: このリポジトリの `lib/devbase/`。配布先は `install.sh` で `main` を取る利用者の端末。`bin/devbase` とプラグインのリポジトリ(`projects/*`)は変えない -- **プロジェクトごとに違うもの**: 開発サービス名はプロジェクトの `env` の `DEV_SERVICE_NAME` から受ける(今と同じ)。新しい設定と引数は足さない -- **当たるモード**: `standard` - -## あるべき姿の根拠 - -| 根拠 | 種類 | 何を示すか | -| --- | --- | --- | -| `bin/devbase` の `cmd_build` は `docker compose build "${DEV_SERVICE_NAME:-dev}"` と `--service "${DEV_SERVICE_NAME:-dev}"` で名前を決める(`bin/devbase` の 245・328・333・337 行) | 既存の形 | 空の値を `dev` と読む規則がすでに配布物にあり、Python をこちらへ揃えれば読み方が 1 つになる | -| Bash の `${VAR:-word}` は、変数が未設定か空のときに `word` へ置き換え、空白だけの値は置き換えない(`bash -c 'X=" "; printf "[%s]" "${X:-dev}"'` は `[ ]`、`X=` では `[dev]`。macOS の bash 5.3、2026-10-07) | 実測 | 「空」は長さ 0 の文字列だけを指す。前提 2(空白だけの値は空に含めない)と一致する | -| 課題の本文の前提 1「`bin/devbase` の `${DEV_SERVICE_NAME:-dev}` の読み方に Python を揃える(`bin/devbase` の側は変えない)」 | 利用者の指示の原文 | 直す側を Python に決める | - -要求と受け入れ条件は #425 の本文にある(コピーは `issues/issue-425-requirements.md`)。この文書は「どう作るか」だけを扱う。 - -## ドメインモデル - -### コンテキスト - -| コンテキスト | 何の語が 1 つの意味に決まるか | -| --- | --- | -| Compose の構成(`compose`) | 開発サービス名(どのサービスを開発サービスとして扱うか) | - -変更はこの 1 つのコンテキストに閉じる。開発サービス名を使う側(コマンドの入口 `cli`・イメージの継承 `image-lineage`)は -名前を受け取るだけで、読み方を自分では持たない。その関係は変えない。 - -### 集約 - -| 集約 | 持ち主(書き換えてよいもの) | 根 | エンティティ | 値オブジェクト | -| --- | --- | --- | --- | --- | -| 開発サービス名 | Python 側は `lib/devbase/volume/compose.py` の `get_dev_service_name()`、Bash 側は `bin/devbase` の `${DEV_SERVICE_NAME:-dev}`(この変更では変えない) | 環境変数 `DEV_SERVICE_NAME` の値 | — | 決めた名前(文字列) | - -呼び出しのたびに環境変数から決め直し、保存しない。持ち主が 2 つあるのは、Bash と Python という別の実行の単位が -それぞれ名前を必要とするためである。2 つが同じ規則で読むことを I3 で縛る。 - -### 不変条件 - -| # | 集約 | 条件 | 破れたときの扱い | -| --- | --- | --- | --- | -| I1 | 開発サービス名 | `DEV_SERVICE_NAME` が未設定か空(長さ 0)なら、名前は `dev` である | 単体のテストが落ちる | -| I2 | 開発サービス名 | `DEV_SERVICE_NAME` が空でなければ、名前はその値を変えずに使う(空白だけの値も前後の空白を削らない) | 単体のテストが落ちる | -| I3 | 開発サービス名 | 同じ `DEV_SERVICE_NAME` の値(未設定・空・`app`・空白 1 文字)から、`bin/devbase` の `${DEV_SERVICE_NAME:-dev}` と `get_dev_service_name()` は同じ名前を決める | 2 つを並べて比べるテストが落ちる | -| I4 | 開発サービス名 | `lib/devbase/` で `DEV_SERVICE_NAME` を読むのは `volume/compose.py` の 1 か所だけである | 環境変数の読み取りを集める検査のテストが落ちる | - -### ドメインイベント - -| # | イベント | 発生元 | 受け手 | -| --- | --- | --- | --- | -| E1 | 利用者がプロジェクトの `env` に `DEV_SERVICE_NAME=` と書いた | 利用者 | `bin/devbase`(E2)・`_load_project_env`(E2 の Python 側の読み込み) | -| E2 | 空の `DEV_SERVICE_NAME` が環境変数として Python へ渡った | `bin/devbase` の `set -a` での読み込み、または `_load_project_env` | `get_dev_service_name()`(E3) | -| E3 | Python が開発サービス名を求め、`dev` を得た | `get_dev_service_name()` | 呼び出し側(E4) | -| E4 | 呼び出し側が compose の構成から開発サービス `dev` を引いた | `_resolve_dev_service`・`generate_scaled_compose` など(変えない) | `up`・`login`・`open`・`post-start`・`scale`・`--expires` の判定・トークンの配布 | -| E5 | `build` が `dev` を建て、同じ `dev` を `--service` で Dockerfile の場所を決める入口へ渡した | `bin/devbase` の `cmd_build`(変えない) | `docker compose build`・`project_dockerfile` | - -E3 と E5 が同じ `dev` になることが、この変更で揃える点である(I3)。 - -### 用語 - -| 用語 | 意味 | 用語集への反映 | -| --- | --- | --- | -| 開発サービス名 | `get_dev_service_name()` が返す名前(`DEV_SERVICE_NAME`。未設定か空なら `dev`)。生成物では `<開発サービス名>-1`..`-N` へ複製される | 意味の変更(`compose`。この設計の変更で反映済み) | - -## 機能一覧 - -| # | 機能 | 誰が使うか | -| --- | --- | --- | -| F1 | `DEV_SERVICE_NAME=` と空で書いたプロジェクトで、`up`・`login`・`open`・`post-start`・`scale`・`--expires` の判定・トークンの配布が `dev` を開発サービスとして使う | プロジェクトの利用者 | -| F2 | `bin/devbase` と Python が、同じ `env` から同じ開発サービス名を決める | devbase の開発者 | - -## 構成要素 - -| 要素 | 責務 | この変更 | -| --- | --- | --- | -| `get_dev_service_name()`(`lib/devbase/volume/compose.py`) | 環境変数から開発サービス名を決める。Python で `DEV_SERVICE_NAME` を読む唯一の場所 | 変える。空の値を `dev` と読む | -| `cmd_build`(`bin/devbase`) | `${DEV_SERVICE_NAME:-dev}` で名前を決め、`docker compose build` と `project_dockerfile --service` へ渡す | 変えない | -| 呼び出し側(`container.py` の `cmd_up`・`cmd_login`・`cmd_open`・`cmd_post_start`・`cmd_scale`・`_resolve_dev_service`・`_ensure_images`・`_dev_instance_indices`・`_maybe_open_editor`・`_open_editor_at`、`env.py` の `_push_token_to_running`、`compose.py` の `generate_scaled_compose`) | 名前を受け取り、compose の構成から開発サービスを引く | 変えない | -| 用語集の「開発サービス名」(`docs/glossary/glossary.json` と、作り直す `docs/glossary.md`) | 語の意味を定める | 変える(この設計の変更で反映) | -| 用語集の意味を写した確定仕様の文(`docs/specifications/editor-open.md` と `docs/specifications/compose-profiles.md` の用語の表) | 仕様の中で語の意味を示す | 変える。用語集と同じ文にする | -| `CHANGELOG.md` の `[Unreleased]` | 利用者に見える振る舞いの変更を記す | 足す | - -型・クラスは足さず変えない。`get_dev_service_name()` の引数と戻り値の型(`str`)も変えないため、クラス図と構造の節は置かない。 - -既存の規則のうち、`DEV_SERVICE_NAME` の値を前提にしたものを、名前の検索(`DEV_SERVICE_NAME`・`get_dev_service_name`・`既定 dev`)と、 -空の値が通る経路(E1〜E5)の読みで集めた。空の値を `dev` と読んだ後に当てはまらなくなる規則は、上の表の用語集と 2 つの確定仕様の文だけである。 -`generate_scaled_compose` の `dev_service_name is None` のときの既定・`_dev_instance_indices` の `<名前>-<番号>` の照合・ -`tests/conftest.py` の隔離の一覧は、受け取る名前が `dev` になるだけで、そのまま当てはまる。 - -### 文脈と配置 - -```mermaid -graph LR - U[利用者] -->|書く| ENV["プロジェクトの env
DEV_SERVICE_NAME="] - subgraph HOST[利用者の端末] - subgraph SH["bin/devbase(Bash)"] - B["cmd_build
${DEV_SERVICE_NAME:-dev}"] - end - subgraph PY["lib/devbase(Python)"] - G["get_dev_service_name()"] - C[呼び出し側] - PD[project_dockerfile] - end - end - ENV -->|set -a で読む| B - ENV -->|set -a / _load_project_env| G - B -->|--service dev| PD - B -->|build dev| DC[docker compose] - G -->|dev| C - C -->|config / up / exec| DC -``` - -変更は `G` の 1 か所に閉じる。`B` と `PD` と `C` は変えない。 - -## 入出力の契約 - -変えるのは、プロジェクトの `env` に書く `DEV_SERVICE_NAME` の読み方だけである。 - -| 項目 | 書くこと | -| --- | --- | -| 名前 | 環境変数 `DEV_SERVICE_NAME`(Python の入口は `get_dev_service_name()`) | -| 入力 | 文字列。任意 | -| 出力 | 未設定 → `dev`、空(`''`)→ `dev`(**変わる**。今は `''`)、それ以外 → 値をそのまま(空白だけの値も含む) | -| 失敗の形 | 無い。名前は必ず決まる。構成にその名前のサービスが無いときの扱いは呼び出し側が持ち、変えない | -| 互換性 | 空で書いたプロジェクトだけが変わる。今の振る舞いは開発サービスを見失うだけで、頼りにする利用者は無いと置く(要求の前提 4)。未設定と空でない値のプロジェクトは変わらない | - -## 処理の流れ - -```mermaid -sequenceDiagram - participant W as bin/devbase - participant G as get_dev_service_name() - participant C as 呼び出し側 - participant D as docker compose - W->>W: env を set -a で読む(DEV_SERVICE_NAME='') - alt build - W->>D: build ${DEV_SERVICE_NAME:-dev} → dev - else up・login・open・post-start・scale・--expires の判定・トークンの配布 - W->>C: Python の入口を起動 - C->>G: 開発サービス名を求める - G->>G: 値を読む。未設定か空なら dev - G-->>C: dev - C->>D: config / up / exec(サービス dev) - end -``` - -構成要素の表のうち、用語集・確定仕様の文・`CHANGELOG.md` は実行の流れに現れないため、この図に描かない。 - -## 決定の記録 - -### 決定 1: どの呼び出し側も同じ名前を得るため、空の値を `dev` と読む規則を `get_dev_service_name()` の中だけに置く - -呼び出し側は 12 か所あり、どれも `get_dev_service_name()` から名前を受け取る(要求の前提 3)。規則を関数の中に置けば、 -呼び出し側を変えずにすべての経路が揃う。Python の読み込み(`_load_project_env`)も空の値を環境へ載せるため、 -`bin/devbase` を通らない経路も同じ関数で揃う。 - -`bin/devbase` で空の `DEV_SERVICE_NAME` を消してから Python へ渡す形は採らない。`bin/devbase` を変えないという要求の -前提 1 に反し、`_load_project_env` が `env` を読み直す経路(トークンの配布など)では空の値が戻る。呼び出し側ごとに -空の値を確かめる形も採らない。12 か所に同じ規則が散り、1 か所の書き漏れで経路が分かれる。 - -根拠: Value 1 / Mission(MVV 版 1) - -### 決定 2: `bin/devbase` と同じ値を空と読むため、空の判定は長さ 0 だけにし、前後の空白を削らない - -`${DEV_SERVICE_NAME:-dev}` は空白だけの値を置き換えない(あるべき姿の根拠の実測)。Python が空白を削って `dev` と読むと、 -空白だけの値で `build` が建てるサービスと Python が探すサービスがまた分かれる。空白だけの値やサービス名に使えない文字の -検証は、名前の形の検証として別の課題で扱う(要求の「含まない」)。 - -前後の空白を削ってから空かを見る形は採らない。上の理由で I3 が破れる。 - -根拠: Value 1(MVV 版 1) - -### 決定 3: 環境変数の読み取りを集める検査が今と同じ場所を拾うよう、読み取りは `os.environ.get('DEV_SERVICE_NAME')` の形のまま関数の中に置く - -`test_collector_finds_known_reads` と `test_every_env_read_is_listed` は、`os.environ.get(N, ...)` の形を `ast` で拾う -(`docs/specifications/test-environment-isolation.md` の「読み取りの集合の集め方」)。`os.environ.get('DEV_SERVICE_NAME') or 'dev'` -はこの形を保ち、読む場所は `volume/compose.py` のままになる。確定仕様のその節の例(`os.environ.get('DEV_SERVICE_NAME', 'dev')`)は -拾う形の例示で、実際のコードの写しではないため、書き換えない。 - -空の値を別の関数(環境変数を一般に読む helper)へ寄せる形は採らない。`DEV_SERVICE_NAME` 以外の変数の空の値は範囲の外で -(要求の「含まない」)、helper の中で読むと検査が拾う場所も移る。 - -根拠: Value 1(MVV 版 1) - -## テスト設計 - -| 受け入れ条件・不変条件 | どの振る舞いで縛るか | どう壊したら落ちるべきか | -| --- | --- | --- | -| 受け入れ条件 1・I1 | `DEV_SERVICE_NAME=''` で `get_dev_service_name()` が `'dev'` を返す | 関数を元の `os.environ.get('DEV_SERVICE_NAME', 'dev')` に戻すと落ちる | -| 受け入れ条件 2・I1 | `DEV_SERVICE_NAME` を消した状態で `'dev'` を返す | 既定を `dev` 以外にすると落ちる | -| 受け入れ条件 3・I2 | `DEV_SERVICE_NAME=app` で `'app'` を返す | 値を捨てて常に `dev` を返すと落ちる | -| 受け入れ条件 4・I2 | `DEV_SERVICE_NAME=' '` で `' '` を返す | 空白を削ってから空かを見る形にすると落ちる | -| I3 | 未設定・空・`app`・空白 1 文字のそれぞれで、Bash が `${DEV_SERVICE_NAME:-dev}` を展開した値と `get_dev_service_name()` が一致する | Python 側だけ空白を削る、または Python 側だけ空を `dev` と読まない形にすると落ちる | -| 受け入れ条件 5 | 構成に `dev` があり `DEV_SERVICE_NAME=''` のとき、`_resolve_dev_service` が `dev` のサービスの定義を返す。`get_dev_service_name` を差し替えずに通す | 関数を元に戻すと、空の名前で引いて空の定義を返し落ちる | -| 受け入れ条件 6・I4 | 読み取りを集める処理が `DEV_SERVICE_NAME` の読み取りを `volume/compose.py` に見つけ、ほかのファイルには見つけない | 呼び出し側に `os.environ.get('DEV_SERVICE_NAME')` を足すと落ちる。関数の読み取りを拾えない形へ書き換えると落ちる | -| 受け入れ条件 7 | `glossary.py check` が設計文書と用語集の食い違いを出さず、`docs/glossary.md` が `glossary.json` から作り直してある | 用語集の意味を古い文に戻すと、`render` の差分が出る | -| 受け入れ条件 8 | `CHANGELOG.md` の `[Unreleased]` を読んで確かめる(レビュー) | — | -| 受け入れ条件 9 | 全体のテスト・lint・固有の語の検査(要求の「検証手段」のコマンド) | — | - -既存のテストの多くは `container.get_dev_service_name` を `lambda: 'dev'` に差し替えている。受け入れ条件 5 のテストは差し替えると -関数を通らず、壊しても落ちない。 - -## 設計の結果 - -| 課題 | 扱い | 取り込み先 | 触るファイル | -| --- | --- | --- | --- | -| #425 | 実装する | — | `lib/devbase/volume/compose.py`、`tests/volume/`、`tests/commands/`、`tests/test_env_isolation.py`、`docs/specifications/editor-open.md`、`docs/specifications/compose-profiles.md`、`CHANGELOG.md` | - -## 未確認のまま残ること - -| 項目 | 内容 | -| --- | --- | -| 確定仕様 `base-image-chain-build.md` の決定 6 の文 | 「`get_dev_service_name()` は空の値をそのまま返す」は、直した後に事実でなくなる。決定の理由として残すか、#425 で揃ったと書き足すかは、確定仕様化(`plan-to-spec`)の担当が決める(要求の「未決」) | -| 実機での確かめ | 空の値で `devbase login` が `dev` のコンテナへ入ることは、リリース後テストで作業用のプロジェクトを使って確かめる。実際の利用者の環境での `devbase up` は P3 のため人の承認を得てから行う | diff --git a/issues/issue-425-requirements.md b/issues/issue-425-requirements.md deleted file mode 100644 index 159420b7..00000000 --- a/issues/issue-425-requirements.md +++ /dev/null @@ -1,134 +0,0 @@ -# #425: DEV_SERVICE_NAME を空で書くと、bin/devbase は dev を使い Python は空の名前で開発サービスを探す - -正は課題の本文(#425)で、この文書はその写しである。`spec-copy.py write` で作り直す。手で直さない。 - -## 依頼(原文) - -> ## 何を見つけたか -> -> プロジェクトの `env` に `DEV_SERVICE_NAME=`(空の値)と書いたとき、2 つの読み方が別のサービス名になる(コードを読んだ結果。実機では未再現)。 -> -> - `bin/devbase` の `cmd_build` は `${DEV_SERVICE_NAME:-dev}` で、空なら `dev` を `docker compose build` に渡す。Dockerfile の場所を決める `python -m devbase.commands.project_dockerfile` にも同じ値を `--service` で渡す(#415)ため、通常のビルドの中では建てるサービスと場所を決めるサービスが揃う -> - Python の `get_dev_service_name()`(`lib/devbase/volume/compose.py:35`)は `os.environ.get('DEV_SERVICE_NAME', 'dev')` で、空の値をそのまま返す。`--expires` の判定(`lib/devbase/commands/container.py` の `_get_base_image_ref`)・`up`・`login`・`open`・`post-start`・`scale` はこちらを使う -> -> `bin/devbase` は `env` を `set -a` で読むため、空の値も環境変数として Python へ渡る。 -> -> ## どこで見つけたか -> -> - `lib/devbase/volume/compose.py` の `get_dev_service_name` -> - `bin/devbase` の `cmd_build`(`docker compose build "${DEV_SERVICE_NAME:-dev}"` と `project_dockerfile` の `--service "${DEV_SERVICE_NAME:-dev}"`) -> -> ## なぜこの変更の範囲外なのか -> -> issue #415 の受け入れ条件は、通常のビルドと `--expires` の判定が**同じ開発サービス名から**同じ Dockerfile を決めることを求め、開発サービス名の読み方そのものは前提 1(2 つが同じ値を使う)に置いている。`get_dev_service_name()` は `up`・`login`・`open`・`post-start` などでも使われ、直すと #415 の範囲(Dockerfile の場所の決め方)の外へ広がる。#415 は、通常のビルドが `${DEV_SERVICE_NAME:-dev}` の値を引数で渡し、建てるサービスと場所を決めるサービスを揃えた(PR #427)。根拠: Value 1(MVV 版 1) -> -> ## 直さないと何が起きるか -> -> `DEV_SERVICE_NAME=` と空で書いたプロジェクトで、`--expires` の判定・`devbase up` のイメージの確かめ・`login` などが、サービス名 `''` を探して開発サービスを見失う。今の手元のプラグインのプロジェクトで `DEV_SERVICE_NAME` を `env` に書いたものは 0 件で、すぐには表に出ない。 -> -> ## 由来 -> -> issue #415 の設計中に見つけた。 - -## 目的 - -- プロジェクトの `env` に `DEV_SERVICE_NAME=`(空の値)と書いても、`devbase` のどの経路も同じ開発サービス名(`dev`)を使う状態にする。`bin/devbase` の `build` と、Python の `up`・`login`・`open`・`post-start`・`scale`・`--expires` の判定とで、開発サービスを見失う経路を無くす - -## 前提 - -- 前提 1: 空の値(長さ 0 の文字列)は「`DEV_SERVICE_NAME` を書いていない」と同じに扱い、開発サービス名は `dev` になる。`bin/devbase` の `${DEV_SERVICE_NAME:-dev}` の読み方に Python を揃える(`bin/devbase` の側は変えない) -- 前提 2: 空白だけの値(`DEV_SERVICE_NAME=" "` など)は空の値に含めない。`bin/devbase` の `${DEV_SERVICE_NAME:-dev}` も空白だけの値をそのまま使うため、今も 2 つの読み方は揃っている -- 前提 3: Python で `DEV_SERVICE_NAME` を読む箇所は `lib/devbase/volume/compose.py` の `get_dev_service_name()` 1 か所だけである(`lib/` を `DEV_SERVICE_NAME` で検索した結果。ほかの当たりは文字列の説明か `get_dev_service_name()` の呼び出し)。呼び出し側(`lib/devbase/commands/container.py` の `cmd_up`・`cmd_login`・`cmd_open`・`cmd_post_start`・`cmd_scale`・`_resolve_dev_service`・`_ensure_images`・`_dev_instance_indices`・`_maybe_open_editor`・`_open_editor_at`、`lib/devbase/commands/env.py` の `_push_token_to_running`、`lib/devbase/volume/compose.py` の `generate_scaled_compose`)は名前を受け取るだけで、直す場所は `get_dev_service_name()` に閉じる -- 前提 4: 手元のプラグインのプロジェクト(`repos/`・`projects/*`)で `DEV_SERVICE_NAME` を書いたものは 0 件である(issue の起票時の調べ)。空の値を書いた利用者の環境があっても、変更後は `dev` を探すようになるだけで、`dev` 以外の名前を書いた利用者の振る舞いは変わらない - -## 対象範囲 - -含む: -- `get_dev_service_name()` が、`DEV_SERVICE_NAME` が空の値のとき `dev` を返すようにする -- 空の値の扱いを確かめるテスト(関数の単体と、呼び出し側の経路 1 つ以上) -- 開発サービス名の既定の書き方(用語集の「開発サービス名」と、それを写した確定仕様の文)を「未設定か空なら `dev`」へ揃える -- `CHANGELOG.md` の `[Unreleased]` への記入(利用者に見える振る舞いの変更のため) - -含まない: -- `bin/devbase` の `${DEV_SERVICE_NAME:-dev}` の読み方の変更(すでに空なら `dev`) -- `build` が `--service` で開発サービス名を渡す形(#415 の決定 6)をやめること。Python 側が揃った後も、建てるサービスと場所を決めるサービスを 1 つの値で揃える役目は残る -- 空白だけの値・サービス名として使えない文字を含む値の検証(前提 2。名前の形の検証は別の課題で扱う) -- `DEV_SERVICE_NAME` 以外の環境変数の空の値の扱い -- 空の値を書いたプロジェクトへの警告の表示 - -## ドメインイベント - -| # | イベント | 引き金 | 失敗したとき | 順序の前提 | -| --- | --- | --- | --- | --- | -| E1 | 利用者がプロジェクトの `env` に `DEV_SERVICE_NAME=` と書いた | 利用者の編集 | — | — | -| E2 | `bin/devbase` が `env` を `set -a` で読み、空の `DEV_SERVICE_NAME` を環境変数として Python へ渡した | `devbase` の各コマンドの起動 | `env` を読めなければ今の扱いのまま(この変更では変えない) | E1 | -| E3 | Python が開発サービス名を求め、`dev` を得た | `up`・`login`・`open`・`post-start`・`scale`・`--expires` の判定・トークンの配布 | — (空の値でも未設定でも名前は必ず決まる) | E2 | -| E4 | 呼び出し側が compose の構成から開発サービス `dev` を引いた | E3 の名前 | 構成に `dev` が無ければ、今の「開発サービスが見つからない」扱いのまま(この変更では変えない) | E3 | -| E5 | `build` が `${DEV_SERVICE_NAME:-dev}` の `dev` を建て、同じ `dev` を `--service` で Dockerfile の場所を決める処理へ渡した | `devbase build` | 今の扱いのまま | E2 | - -E3 と E5 が同じ `dev` になることが、この変更で揃える点である。 - -## 用語 - -| 用語 | 意味 | -| --- | --- | -| 開発サービス名 | `get_dev_service_name()` が返す名前。`DEV_SERVICE_NAME` が未設定か空なら `dev` | - -## 受け入れ条件 - -- [ ] `DEV_SERVICE_NAME` が空の値(`''`)のとき、`get_dev_service_name()` は `'dev'` を返す -- [ ] `DEV_SERVICE_NAME` が未設定のとき、`get_dev_service_name()` は `'dev'` を返す(退行しない) -- [ ] `DEV_SERVICE_NAME=app` のとき、`get_dev_service_name()` は `'app'` を返す(退行しない) -- [ ] `DEV_SERVICE_NAME=" "`(空白 1 文字)のとき、`get_dev_service_name()` は `' '` を返す(前提 2。`bin/devbase` の `${DEV_SERVICE_NAME:-dev}` と同じ値) -- [ ] 前提: compose の構成に `dev` サービスがあり、`DEV_SERVICE_NAME` が空の値である - 操作: 呼び出し側の経路(`_resolve_dev_service` または `generate_scaled_compose`)を通す - 結果: `dev` サービスを開発サービスとして扱う(空の名前のサービスを探して見失わない) -- [ ] 環境変数の読み取りを集める検査(`tests/test_env_isolation.py` の `test_collector_finds_known_reads`)が、変更後も `DEV_SERVICE_NAME` の読み取りを `volume/compose.py` に見つける -- [ ] 用語集の「開発サービス名」の意味が「未設定か空なら `dev`」と読め、`docs/glossary.md` は `glossary.json` から作り直してある -- [ ] `CHANGELOG.md` の `[Unreleased]` に、空の `DEV_SERVICE_NAME` を `dev` として扱うようになったことが書いてある -- [ ] 全体のテスト・lint・固有の語の検査が通る(下の「検証手段」) - -## 影響 - -| 対象 | 影響 | -| --- | --- | -| 公開インタフェース | 変わる(互換あり)。`DEV_SERVICE_NAME=` と空で書いたときの開発サービス名が `''` から `dev` になる。今の振る舞いは開発サービスを見失うだけで、これを頼りにする利用者は無いと置く(前提 4) | -| データ | 無し | -| 既存の振る舞い | 空の `DEV_SERVICE_NAME` を書いたプロジェクトの `up`・`login`・`open`・`post-start`・`scale`・`--expires` の判定・トークンの配布が `dev` を開発サービスとして使う。未設定・空でない値のプロジェクトは変わらない | - -## 検証手段 - -| 項目 | 手段 | -| --- | --- | -| テスト | `env -u DEVBASE_ROOT uv run --locked pytest tests/ -q` | -| 静的解析 | `uvx ruff check --select=E9,F63,F7,F82 lib` | -| 固有の語の検査 | `python3 .github/scripts/proper_term_check.py`(`語の一覧が無いため飛ばした` と出たら検査していないと記録する) | -| 手動確認 | リリース後テストで、作業用のプロジェクトの `env` に `DEV_SERVICE_NAME=` と書き、`devbase login` が `dev` のコンテナへ入ることを確かめる。実際の利用者の環境での `devbase up` は P3 のため人の承認を得てから行う | - -## 前提とする取り決め - -| 項目 | 参照先 / 決めたこと | -| --- | --- | -| プロジェクト構造 | `AGENTS.md` の「このリポジトリ」。直すのは `lib/devbase/` の Python。`projects/*`・`repos/` は管理外のため触らない | -| コーディング規約 | `AGENTS.md` の「書き方」と周りのコードに合わせる。lint は上の静的解析のコマンド | -| テスト戦略 | 単体: `get_dev_service_name()` の 4 つの値(未設定・空・`app`・空白)。結合: 空の値で呼び出し側の経路が `dev` を引くこと。テストは `monkeypatch` で `DEV_SERVICE_NAME` を置き、実の docker に触らない(`docs/specifications/test-environment-isolation.md`) | - -## 境界 - -| 区分 | 内容 | -| --- | --- | -| 常に行う | 上の 3 つの検査を push の前に手元で打つ。用語集を直したら `glossary.py render` で `docs/glossary.md` を作り直す | -| 確認してから行う | `get_dev_service_name()` の戻り値の型や呼び出し方を変えること。名前の形の検証を足すこと | -| 行わない | `bin/devbase` の読み方の変更。base イメージの変更(P1)。実際の利用者の環境での `devbase up`(P3) | - -## 未決 - -| 項目 | 誰が決めるか | 期限 | -| --- | --- | --- | -| 確定仕様(`docs/specifications/base-image-chain-build.md` の決定 6)の「`get_dev_service_name()` は空の値をそのまま返す」の文を、直した後の事実へどう書き換えるか(決定の理由として残すか、#425 で揃ったと書き足すか) | 確定仕様化の担当 | 確定仕様化の工程 | - -## 根拠 - -- Value 1(入った時点で使える): `env` の書き方の違いで開発サービスを見失わないようにする -- Mission: どのプロジェクトでも同じ読み方で開発環境へ入れるようにする -- P1・P3: 範囲に含めない(境界の「行わない」)