Skip to content

Commit 6fbce36

Browse files
takemi-ohamaclaude
andcommitted
feat(env): devbase env init で HOST_SSH_USER / HOST_SSH_HOST を自動設定
コンテナ→ホスト SSH (ホスト側 GUI アプリ起動) ワークフロー向けに、ホストの ログインユーザー名 / SSH 先ホスト名を収集する host コレクタを追加。 - keys.py: HOST_SSH_USER / HOST_SSH_HOST 定数追加 - collectors/host.py: getpass.getuser() を既定値に提示、safe_input で上書き可 (非対話/CI は既定値設定、getuser 空時は安全スキップ) - cmd_env_sync: _sync_host() で欠落キーのみ既定値補完 (手動上書きは尊重) - docs: environment-variables.md に host 節追記 - tests: コレクタ + sync backfill の単体テスト 9 ケース Closes #48 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 44c0fbd commit 6fbce36

6 files changed

Lines changed: 421 additions & 0 deletions

File tree

docs/user/environment-variables.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,17 @@ devbase はホストマシンの認証情報を自動収集し、コンテナ内
127127
| `SLACK_CHANNEL_ID` | チャンネル ID |
128128
| `SLACK_USER_MENTION` | ユーザーメンション |
129129

130+
#### host -- ホスト接続情報 (SSH)
131+
132+
コンテナからホストへ SSH してホスト側 GUI アプリ(例: Chrome をリモートデバッグモードで起動)を起動するワークフロー向けの設定です。`devbase env init` はホスト上で実行されるため、ホストのログインユーザー名を自動取得して既定値として提示します。
133+
134+
| キー | 説明 |
135+
|------|------|
136+
| `HOST_SSH_USER` | コンテナ→ホスト SSH 時のホストログインユーザー名(既定: `getpass.getuser()` で自動取得) |
137+
| `HOST_SSH_HOST` | SSH 先ホスト名(既定: `host.docker.internal`、WSL2/Windows では上書き可) |
138+
139+
ユーザー名のみで秘密情報ではありません。SSH 鍵やリモートログインの有効化はホスト側でユーザーが別途設定する前提です。`devbase env sync` 実行時には、未設定のキーのみ既定値で補完されます(既存値は上書きしません)。
140+
130141
## ソースファイル変更検出
131142

132143
devbase はソースファイル(`~/.aws/config` 等)のハッシュを `.env.sources.yml` で管理しています。

issues/PLAN48_host-ssh-user-env.md

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# PLAN48: `devbase env init``HOST_SSH_USER` を自動設定する
2+
3+
## 関連リンク
4+
5+
- 元 issue: [#48](https://github.com/devbasex/devbase/issues/48) `feat: devbase env init で HOST_SSH_USER を自動設定する`
6+
- 利用側 (参照のみ・本リポジトリ対象外): ai-plugins `plugins/ndf/skills/playwright-browser-connect/`
7+
`scripts/start-host-chrome.sh` がコンテナ→ホスト SSH のために `HOST_SSH_USER` を要求する)
8+
9+
## 概要
10+
11+
`devbase env init` の対話セットアップに **ホスト接続情報 (SSH)** コレクタを追加し、
12+
ホスト (mac/Linux/WSL) のログインユーザー名を `HOST_SSH_USER` として `.env` に自動設定する。
13+
併せて SSH 先ホスト名 `HOST_SSH_HOST`(既定 `host.docker.internal`)も収集する。
14+
15+
これにより、コンテナからホストへ SSH してホスト側 GUI アプリ(例: Chrome をリモート
16+
デバッグモードで起動)を起動するワークフローを、`HOST_SSH_USER=<名>` の手動指定なしで
17+
利用できるようにする。
18+
19+
`devbase env init`**ホスト上で実行される CLI** であり、ホストのユーザー名を確実に
20+
取得できる立場にある(`getpass.getuser()`)。
21+
22+
## 設計判断(確定事項)
23+
24+
| 論点 | 決定 | 理由 |
25+
|---|---|---|
26+
| 収集キー | `HOST_SSH_USER` + `HOST_SSH_HOST` 両方 | WSL2/Windows ではホストユーザーと SSH 先が一致しないケースがあり、`HOST_SSH_HOST` の上書き余地を残す(issue 補足) |
27+
| `devbase env sync` 対応 | あり(欠落キーの補完) | 既存ユーザーの `.env` への後付け backfill。ただし手動上書きは尊重して上書きしない |
28+
| 進め方 | 単一 PR | 変更 ~4 ファイル・~100 行・依存タスクなし。release ブランチ不要 |
29+
30+
## アーキテクチャ整合
31+
32+
既存のコレクタ機構に倣う:
33+
34+
- `lib/devbase/env/collector.py``CollectorRegistry``env/collectors/*.py` を走査し、
35+
モジュール直下の **`COLLECTOR` 定数**(大文字)を登録する。issue 例の `collector =`
36+
実装規約と異なるため `COLLECTOR =` で定義する。
37+
- 既存 `git.py` / `slack.py` と同じ `Collector(name, display_name, collect_fn)` インターフェース。
38+
- 対話入力は `devbase.env.store.safe_input`(EOF 安全。非対話/CI では default を返す)を使う。
39+
40+
```mermaid
41+
flowchart LR
42+
init["devbase env init"] --> reg["CollectorRegistry.discover()"]
43+
reg --> host["collectors/host.py<br/>COLLECTOR"]
44+
host --> collect["collect_host_info(env_file)"]
45+
collect --> envfile[".env<br/>HOST_SSH_USER / HOST_SSH_HOST"]
46+
sync["devbase env sync"] --> backfill["_sync_host()<br/>欠落キーのみ補完"]
47+
backfill --> envfile
48+
```
49+
50+
## 変更ファイル
51+
52+
| ファイル | 種別 | 内容 |
53+
|---|---|---|
54+
| `lib/devbase/env/keys.py` | 変更 | `HOST_SSH_USER` / `HOST_SSH_HOST` 定数を追加 |
55+
| `lib/devbase/env/collectors/host.py` | 新規 | `host` コレクタ(`collect_host_info` + `COLLECTOR`|
56+
| `lib/devbase/commands/env.py` | 変更 | `cmd_env_sync``_sync_host()` を追加 |
57+
| `docs/user/environment-variables.md` | 変更 | `#### host` 節とコレクタ一覧へ追記 |
58+
| `tests/env/test_collector_host.py` | 新規 | コレクタ + sync の単体テスト |
59+
60+
## 実装詳細
61+
62+
### 1. `keys.py`
63+
64+
```python
65+
# --- Host (コンテナ→ホスト SSH 接続) ---
66+
HOST_SSH_USER = "HOST_SSH_USER"
67+
HOST_SSH_HOST = "HOST_SSH_HOST" # 任意。default: host.docker.internal
68+
```
69+
70+
### 2. `collectors/host.py`(新規)
71+
72+
```python
73+
"""ホスト接続情報 (SSH) コレクター"""
74+
75+
import getpass
76+
77+
from devbase.log import get_logger
78+
from devbase.env import keys
79+
from devbase.env.store import EnvFile, safe_input
80+
from devbase.env.collector import Collector
81+
82+
logger = get_logger(__name__)
83+
84+
DEFAULT_HOST_SSH_HOST = "host.docker.internal"
85+
86+
87+
def _default_host_user() -> str:
88+
"""ホストのログインユーザー名。HOME/USER/LOGNAME 欠落時も例外を出さず "" を返す。"""
89+
try:
90+
return getpass.getuser()
91+
except Exception:
92+
return ""
93+
94+
95+
def collect_host_info(env_file: EnvFile) -> None:
96+
"""ホスト接続情報 (SSH) を対話的に収集する"""
97+
print("\n=== ホスト接続情報 (SSH) ===")
98+
99+
default_user = env_file.get(keys.HOST_SSH_USER) or _default_host_user()
100+
user = safe_input(f"{keys.HOST_SSH_USER} [{default_user}]: ", default_user)
101+
if user:
102+
env_file.set(keys.HOST_SSH_USER, user)
103+
else:
104+
logger.info("%s: 既定値が取得できずスキップ", keys.HOST_SSH_USER)
105+
106+
default_host = env_file.get(keys.HOST_SSH_HOST) or DEFAULT_HOST_SSH_HOST
107+
host = safe_input(f"{keys.HOST_SSH_HOST} [{default_host}]: ", default_host)
108+
if host:
109+
env_file.set(keys.HOST_SSH_HOST, host)
110+
111+
112+
COLLECTOR = Collector(
113+
name="host",
114+
display_name="ホスト接続情報 (SSH)",
115+
collect_fn=collect_host_info,
116+
)
117+
```
118+
119+
非対話 (EOF) 時: `safe_input` が default を返すため `HOST_SSH_USER=getpass.getuser()`
120+
`HOST_SSH_HOST=host.docker.internal` が設定される(受け入れ条件「非対話/CI でも既定値で設定」)。
121+
`getpass.getuser()` が空を返す環境では `HOST_SSH_USER` は安全にスキップされる。
122+
123+
### 3. `cmd_env_sync` への `_sync_host()` 追加
124+
125+
ホスト情報はソースファイルを持たないため hash 比較は使わず、**欠落キーのみ既定値で補完**する
126+
(既存値=WSL2 等での手動上書きは尊重して上書きしない)。既存ユーザーの `.env` への後付け
127+
backfill として機能する。
128+
129+
```python
130+
def _sync_host(env_file):
131+
"""ホスト接続情報の同期。欠落キーを既定値で補完する。更新件数を返す。"""
132+
from devbase.env.collectors.host import _default_host_user, DEFAULT_HOST_SSH_HOST
133+
updated = 0
134+
if not env_file.get(keys.HOST_SSH_USER):
135+
user = _default_host_user()
136+
if user:
137+
env_file.set(keys.HOST_SSH_USER, user)
138+
logger.info("HOST_SSH_USER: %s を設定", user)
139+
updated += 1
140+
if not env_file.get(keys.HOST_SSH_HOST):
141+
env_file.set(keys.HOST_SSH_HOST, DEFAULT_HOST_SSH_HOST)
142+
logger.info("HOST_SSH_HOST: %s を設定", DEFAULT_HOST_SSH_HOST)
143+
updated += 1
144+
return updated
145+
```
146+
147+
`cmd_env_sync` 内で `updated += _sync_host(env_file)` を呼ぶ。`updated > 0` なら既存ロジック
148+
どおり `env_file.save()` まで到達する。
149+
150+
### 4. ドキュメント (`docs/user/environment-variables.md`)
151+
152+
「コレクター一覧」に `host` を追加し、slack 節の後に以下を追記:
153+
154+
```markdown
155+
#### host -- ホスト接続情報 (SSH)
156+
157+
| キー | 説明 |
158+
|------|------|
159+
| `HOST_SSH_USER` | コンテナ→ホスト SSH 時のホストログインユーザー名(既定: `getpass.getuser()`|
160+
| `HOST_SSH_HOST` | SSH 先ホスト名(既定: `host.docker.internal`、WSL2/Windows では上書き可) |
161+
162+
ユーザー名のみで秘密情報ではない。SSH 鍵・リモートログイン有効化はホスト側で別途設定する前提。
163+
```
164+
165+
## テスト計画 (`tests/env/test_collector_host.py`)
166+
167+
`input` / `getpass.getuser` を monkeypatch した単体テスト(本コレクタは questionary を
168+
使わない素の `input`/`safe_input` 経路のため、real TTY は不要):
169+
170+
1. **既定値設定**: `getuser`→"alice"、入力 EOF → `HOST_SSH_USER=alice` / `HOST_SSH_HOST=host.docker.internal`
171+
2. **上書き**: 入力で "bob" / "192.168.1.10" → その値で設定される
172+
3. **getuser 例外**: `getuser` が例外 → `HOST_SSH_USER` 未設定・`HOST_SSH_HOST` は既定で設定
173+
4. **既存値優先**: env に既存 `HOST_SSH_USER=carol` → default 提示が carol
174+
5. **`_sync_host` backfill**: 欠落時に補完・既存値は上書きしない・更新件数が正しい
175+
6. **レジストリ登録**: `CollectorRegistry.discover()` 後に name=="host" が含まれる
176+
177+
既存テスト一式が回帰しないこと(`pytest tests/`)も確認する。
178+
179+
## 受け入れ条件(issue より)
180+
181+
- [ ] `devbase env init` 実行時に `HOST_SSH_USER` の既定値(ホストのユーザー名)が提示され、確認・変更できる
182+
- [ ] `.env``HOST_SSH_USER` が書き出され、コンテナ内の環境変数として参照できる
183+
- [ ] 非対話/CI でも既定値(`getpass.getuser()`)で設定される、もしくは安全にスキップされる
184+
- [ ] `HOST_SSH_HOST`(既定 `host.docker.internal`)を収集し、上書きできる
185+
- [ ] `devbase env sync` で欠落キーを既定値で補完する(既存値は尊重)
186+
- [ ] ドキュメント(`docs/user/environment-variables.md`)に `HOST_SSH_USER` / `HOST_SSH_HOST` を追記
187+
188+
## PR 計画
189+
190+
| PR | branch | base | 概要 |
191+
|---|---|---|---|
192+
| 単一 | `feature/PLAN48-host-ssh-user` | `main` | 上記 4 ファイル変更 + テスト。`/ndf:cross-review` でセルフレビュー後 merge |
193+
194+
release ブランチは作成しない(単一 PR・低結合)。`/ndf:implementation-plan` 確認 →
195+
実装 → `/ndf:pr``/ndf:cross-review` の通常フローで進める。

lib/devbase/commands/env.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ def _encode_git():
116116
# GCP(プロファイル管理があるため個別処理)
117117
updated += _sync_gcp(sources, env_file)
118118

119+
# Host 接続情報(ソースファイルを持たないため hash 比較せず欠落キーを補完)
120+
updated += _sync_host(env_file)
121+
119122
if updated > 0:
120123
env_file.save()
121124
_update_source_metadata(devbase_root, env_file)
@@ -129,6 +132,29 @@ def _encode_git():
129132
return 0
130133

131134

135+
def _sync_host(env_file):
136+
"""ホスト接続情報の同期。更新件数を返す。
137+
138+
ホスト情報はソースファイルを持たないため hash 比較は使わず、**欠落キーのみ既定値で
139+
補完**する。既存値 (WSL2 等での手動上書き) は尊重して上書きしない。これにより本機能
140+
導入前の ``.env`` への後付け backfill として機能する。
141+
"""
142+
from devbase.env.collectors.host import _default_host_user, DEFAULT_HOST_SSH_HOST
143+
144+
updated = 0
145+
if not env_file.get(keys.HOST_SSH_USER):
146+
user = _default_host_user()
147+
if user:
148+
env_file.set(keys.HOST_SSH_USER, user)
149+
logger.info("%s: %s を設定", keys.HOST_SSH_USER, user)
150+
updated += 1
151+
if not env_file.get(keys.HOST_SSH_HOST):
152+
env_file.set(keys.HOST_SSH_HOST, DEFAULT_HOST_SSH_HOST)
153+
logger.info("%s: %s を設定", keys.HOST_SSH_HOST, DEFAULT_HOST_SSH_HOST)
154+
updated += 1
155+
return updated
156+
157+
132158
def _sync_source(sources, env_file, name, label, encode_fn):
133159
"""AWS/Gitなどの単一ソース同期の共通処理。更新件数(0 or 1)を返す。"""
134160
source = sources.get_source(name)

lib/devbase/env/collectors/host.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""ホスト接続情報 (SSH) コレクター
2+
3+
コンテナからホストへ SSH してホスト側 GUI アプリ (例: Chrome をリモートデバッグ
4+
モードで起動) を起動するワークフロー向けに、ホストのログインユーザー名 / SSH 先
5+
ホスト名を収集する。``devbase env init`` はホスト上で実行されるため、ホストの
6+
ユーザー名を ``getpass.getuser()`` で確実に取得できる。
7+
"""
8+
9+
import getpass
10+
11+
from devbase.log import get_logger
12+
from devbase.env import keys
13+
from devbase.env.store import EnvFile, safe_input
14+
from devbase.env.collector import Collector
15+
16+
logger = get_logger(__name__)
17+
18+
DEFAULT_HOST_SSH_HOST = "host.docker.internal"
19+
20+
21+
def _default_host_user() -> str:
22+
"""ホストのログインユーザー名を返す。
23+
24+
``getpass.getuser()`` は HOME/USER/LOGNAME 等が全て無い環境で例外を投げうるため、
25+
その場合は空文字を返して呼び出し側で安全にスキップできるようにする。
26+
"""
27+
try:
28+
return getpass.getuser()
29+
except Exception:
30+
return ""
31+
32+
33+
def collect_host_info(env_file: EnvFile) -> None:
34+
"""ホスト接続情報 (SSH) を対話的に収集する"""
35+
print("\n=== ホスト接続情報 (SSH) ===")
36+
37+
# HOST_SSH_USER: 既存値 > getpass.getuser() を既定として提示し、上書き可能にする
38+
default_user = env_file.get(keys.HOST_SSH_USER) or _default_host_user()
39+
user = safe_input(f"{keys.HOST_SSH_USER} [{default_user}]: ", default_user)
40+
if user:
41+
env_file.set(keys.HOST_SSH_USER, user)
42+
else:
43+
logger.info("%s: 既定値が取得できずスキップ", keys.HOST_SSH_USER)
44+
45+
# HOST_SSH_HOST: 任意。既定 host.docker.internal (WSL2/Windows では上書き可)
46+
default_host = env_file.get(keys.HOST_SSH_HOST) or DEFAULT_HOST_SSH_HOST
47+
host = safe_input(f"{keys.HOST_SSH_HOST} [{default_host}]: ", default_host)
48+
if host:
49+
env_file.set(keys.HOST_SSH_HOST, host)
50+
51+
52+
COLLECTOR = Collector(
53+
name="host",
54+
display_name="ホスト接続情報 (SSH)",
55+
collect_fn=collect_host_info,
56+
)

lib/devbase/env/keys.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,7 @@ def gcp_credentials_key(profile: str) -> str:
4646
# --- Slack ---
4747
SLACK_KEYS = ("SLACK_BOT_TOKEN", "SLACK_TEAM_ID",
4848
"SLACK_CHANNEL_ID", "SLACK_USER_MENTION")
49+
50+
# --- Host (コンテナ→ホスト SSH 接続) ---
51+
HOST_SSH_USER = "HOST_SSH_USER"
52+
HOST_SSH_HOST = "HOST_SSH_HOST" # 任意。default: host.docker.internal

0 commit comments

Comments
 (0)