Skip to content

Commit 4da65f4

Browse files
takemi-ohamaclaude
andcommitted
fix(up): plain SSH コマンド提示を which 非依存化 + 実名問い合わせに compose file (-f) を渡す
cross-review round2 codex 指摘 C/D 対応。 C[正確性]: plain SSH の degrade (手元コマンド提示) は decide_action が editor_cmd None で先頭 skip するため、リモートに code が無いと print_command へ到達せず skip していた。提示コマンドは手元(ローカル)で実行する前提なので リモートの code 実在に依存させない。 - resolve_editor_display() を追加 (which 非依存・必ず非None) - decide_action(ctx, editor_available: bool) へシグネチャ変更。SSH の print_command は editor_available に依存せず到達、launch 系経路のみ editor 不在で skip。 - open_editor は launch=editor / print=display を使い分け。 D[正確性]: _query_container_name が docker compose ps に override compose を 渡さず base compose.yml に無い {dev}-{index} を見てほぼ常にフォールバック していた。 - _query_container_name / resolve_container_name / open_editor / _maybe_open_editor に compose_file を追加し起動時と同じ -f を伝播。 - cmd_up は [6/6] で override_file を渡す。 テスト追加・更新 (766 passed)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent abad035 commit 4da65f4

4 files changed

Lines changed: 216 additions & 27 deletions

File tree

lib/devbase/commands/container.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,8 @@ def _auto_snapshot() -> None:
356356

357357

358358
def _maybe_open_editor(project_name: str, open_flag: Optional[bool],
359-
open_index: Optional[int], scale: int) -> None:
359+
open_index: Optional[int], scale: int,
360+
compose_file=None) -> None:
360361
"""`up` 完了後に dev コンテナへ接続したエディタを開く ([6/6])。
361362
362363
有効判定は ``open_flag`` (CLI ``--open``/``--no-open``) が優先、None なら env
@@ -365,6 +366,10 @@ def _maybe_open_editor(project_name: str, open_flag: Optional[bool],
365366
``open_index`` は起動済みインスタンス範囲 ``1..scale`` 内である必要がある。
366367
0・負数・``scale`` 超過は存在しないコンテナ URI になり原因不明な起動失敗を招くため、
367368
警告を出して既定 (1) へフォールバックする。
369+
370+
``compose_file`` は実コンテナ名問い合わせ用の override compose。``up`` 起動時と
371+
同じファイルを渡さないと ``{dev}-{index}`` サービスが見えず実名取得に失敗する。
372+
未指定なら ``.docker-compose.scale.yml`` が存在すればそれ、無ければ None。
368373
"""
369374
from devbase.editor import opener
370375

@@ -387,6 +392,11 @@ def _maybe_open_editor(project_name: str, open_flag: Optional[bool],
387392
)
388393
open_index = 1
389394

395+
# 実コンテナ名問い合わせ用の compose file: 明示指定がなければ override が
396+
# 存在すればそれを使う (起動時と同じ file を docker compose ps へ渡す)。
397+
if compose_file is None and _SCALE_COMPOSE_FILE.exists():
398+
compose_file = _SCALE_COMPOSE_FILE
399+
390400
dev_service_name = get_dev_service_name()
391401
workdir = opener.resolve_workdir(os.environ, project_name)
392402
logger.info("[6/6] Opening editor attached to the dev container...")
@@ -396,6 +406,7 @@ def _maybe_open_editor(project_name: str, open_flag: Optional[bool],
396406
dev_service_name=dev_service_name,
397407
workdir=workdir,
398408
index=open_index,
409+
compose_file=compose_file,
399410
)
400411
except Exception as e: # noqa: BLE001 - エディタ起動で up を倒さない
401412
logger.warning("エディタの自動オープンに失敗しましたがデプロイは成功しています: %s", e)
@@ -470,7 +481,8 @@ def cmd_up(project_name: str = None, scale: int = None,
470481
if deploy_script.exists() and deploy_script.is_file():
471482
_run_deploy_script_for_instances(deploy_script, range(1, scale + 1))
472483

473-
_maybe_open_editor(project_name, open_editor, open_index, scale)
484+
_maybe_open_editor(project_name, open_editor, open_index, scale,
485+
compose_file=override_file)
474486

475487
logger.info("=== Deploy completed successfully ===")
476488
return 0

lib/devbase/editor/opener.py

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,26 @@ def resolve_editor_cmd(environ=None) -> Optional[list]:
123123
return None
124124

125125

126+
def resolve_editor_display(environ=None) -> list:
127+
"""コマンド提示 (print_command) 用のエディタ argv を解決する。
128+
129+
:func:`resolve_editor_cmd` と異なり ``shutil.which`` による実在チェックは
130+
行わない。plain SSH では提示コマンドを実行するのは「ユーザの手元 (ローカル)」
131+
であり、コマンドを実行している側 (リモート) に ``code`` が存在する必要は無い
132+
ため、リモートの実在に依存せず必ず非 None を返す。
133+
134+
``DEVBASE_EDITOR`` があればそれを (シェル風に分割して) 用い、無ければ既定の
135+
``["code"]`` を返す。
136+
"""
137+
env = os.environ if environ is None else environ
138+
explicit = env.get("DEVBASE_EDITOR")
139+
if explicit:
140+
parts = shlex.split(explicit)
141+
if parts:
142+
return parts
143+
return ["code"]
144+
145+
126146
def build_attach_uri(container_name: str, workdir: str) -> str:
127147
"""``vscode-remote://attached-container+<hex>/<workdir>`` を組む。
128148
@@ -175,19 +195,28 @@ def _parse_compose_ps_name(stdout: str) -> Optional[str]:
175195

176196

177197
def _query_container_name(dev_service_name: str, index: int,
198+
compose_file=None,
178199
runner: Optional[Callable] = None) -> Optional[str]:
179200
"""実 docker へ問い合わせて dev インスタンスの実コンテナ名を取得する (保険)。
180201
181202
scale 生成 compose ではサービス名が ``{dev}-{index}`` (例 ``dev-1``) になるため
182203
その service token を指定して ``docker compose ps --format json`` を実行する。
204+
``{dev}-{index}`` サービスは override compose (``.docker-compose.scale.yml``)
205+
側にしか存在しないため、``compose_file`` が与えられた場合は起動時と同じ
206+
``-f <compose_file>`` を付与しないと base ``compose.yml`` には無いサービスを
207+
見に行きほぼ常にフォールバックになる。
183208
取得できなければ None。docker 不在・非0・例外・空はすべて None に握り潰し、
184209
呼び出し側が決定的名へフォールバックできるようにする。
185210
"""
186211
run = runner or subprocess.run
187212
service_token = f"{dev_service_name}-{index}"
213+
cmd = ["docker", "compose"]
214+
if compose_file is not None:
215+
cmd += ["-f", str(compose_file)]
216+
cmd += ["ps", "--format", "json", service_token]
188217
try:
189218
proc = run(
190-
["docker", "compose", "ps", "--format", "json", service_token],
219+
cmd,
191220
capture_output=True, text=True, timeout=10,
192221
)
193222
except Exception: # noqa: BLE001 - docker 不在等は保険なので握り潰す
@@ -201,6 +230,7 @@ def _query_container_name(dev_service_name: str, index: int,
201230

202231

203232
def resolve_container_name(dev_service_name: str, project_name: str, index: int = 1,
233+
compose_file=None,
204234
runner: Optional[Callable] = None) -> str:
205235
"""dev コンテナの実コンテナ名を返す。
206236
@@ -213,7 +243,8 @@ def resolve_container_name(dev_service_name: str, project_name: str, index: int
213243
を全インスタンスへ設定する (volume/compose.py)。COMPOSE_PROJECT_NAME は
214244
project_name と一致するため、docker 問い合わせに失敗しても決定的に組み立てられる。
215245
"""
216-
queried = _query_container_name(dev_service_name, index, runner=runner)
246+
queried = _query_container_name(dev_service_name, index,
247+
compose_file=compose_file, runner=runner)
217248
if queried:
218249
return queried
219250
return f"{project_name}-{dev_service_name}-{index}"
@@ -229,24 +260,37 @@ def resolve_workdir(environ=None, project_name: Optional[str] = None) -> str:
229260
return f"/work/{repo}" if repo else "/work"
230261

231262

232-
def decide_action(ctx: EditorContext, editor_cmd: Optional[list]) -> OpenPlan:
233-
"""コンテキストとエディタ可用性から起動方針を決める (§2.4 マトリクス)。"""
234-
if not editor_cmd:
235-
return OpenPlan(
236-
"skip",
237-
"エディタ (code) が見つかりません。VS Code の `code` コマンドを PATH に "
238-
"通すか DEVBASE_EDITOR を設定してください",
239-
)
263+
_NO_EDITOR_REASON = (
264+
"エディタ (code) が見つかりません。VS Code の `code` コマンドを PATH に "
265+
"通すか DEVBASE_EDITOR を設定してください"
266+
)
267+
268+
269+
def decide_action(ctx: EditorContext, editor_available: bool) -> OpenPlan:
270+
"""コンテキストとエディタ可用性から起動方針を決める (§2.4 マトリクス)。
271+
272+
``editor_available`` はローカルに launch 可能な ``code`` 系コマンドが実在するか
273+
(``resolve_editor_cmd`` が非 None か) を表す。plain SSH の print_command 経路は
274+
「ユーザの手元 (ローカル) でコマンドを実行する」前提のため、コマンドを実行して
275+
いる側 (リモート) の editor 実在には依存させない (``editor_available`` を見ない)。
276+
"""
240277
if not ctx.is_tty:
241278
return OpenPlan("skip", "非対話 (非TTY/CI) 環境のため")
242279
if ctx.in_vscode:
243280
# VS Code 統合ターミナル (ローカル / WSL / Remote-SSH シム)。code が
244-
# クライアント側へ委譲するため直接起動でよい。
281+
# クライアント側へ委譲するため直接起動でよい。code シムが無いと委譲
282+
# できないため editor が無ければ skip。
283+
if not editor_available:
284+
return OpenPlan("skip", _NO_EDITOR_REASON)
245285
return OpenPlan("launch", "VS Code 統合ターミナル経由")
246286
if ctx.is_ssh:
247287
# plain SSH (VS Code 外)。クライアントへ push する公式手段が無いため
248-
# コマンドを提示する degrade。
288+
# 手元で叩くコマンドを提示する degrade。提示先はローカルなのでリモートの
289+
# editor 実在には依存しない。
249290
return OpenPlan("print_command", "SSH セッション (VS Code 外) のため")
291+
# ローカル/WSL 端末。直接 launch するため editor が無ければ skip。
292+
if not editor_available:
293+
return OpenPlan("skip", _NO_EDITOR_REASON)
250294
return OpenPlan("launch", "ローカル/WSL 端末")
251295

252296

@@ -259,29 +303,34 @@ def _launch(cmd: list, env: dict) -> None:
259303

260304

261305
def open_editor(*, project_name: str, dev_service_name: str, workdir: str,
262-
index: int = 1, environ=None,
306+
index: int = 1, compose_file=None, environ=None,
263307
isatty: Optional[bool] = None, system: Optional[str] = None,
264308
launcher: Optional[Callable[[list, dict], None]] = None) -> str:
265309
"""dev コンテナへ接続した VS Code を開く / コマンド提示 / スキップする。
266310
267311
戻り値は実行された action ('launch' | 'print_command' | 'skip')。例外は
268312
握り潰して warning にし、``up`` 本体を絶対に失敗させない。``isatty`` /
269-
``system`` は :func:`detect_context` への差し替え口 (テスト用)。
313+
``system`` は :func:`detect_context` への差し替え口 (テスト用)。``compose_file``
314+
は実コンテナ名問い合わせ時に起動と同じ override compose を ``-f`` で渡すため。
270315
"""
271316
env = os.environ if environ is None else environ
272317
ctx = detect_context(env, isatty=isatty, system=system)
273-
editor = resolve_editor_cmd(env)
274-
plan = decide_action(ctx, editor)
318+
editor = resolve_editor_cmd(env) # launch 用 (which 込み・None あり得る)
319+
display = resolve_editor_display(env) # print 用 (必ず非 None)
320+
plan = decide_action(ctx, editor_available=bool(editor))
275321

276-
container = resolve_container_name(dev_service_name, project_name, index)
322+
container = resolve_container_name(dev_service_name, project_name, index,
323+
compose_file=compose_file)
277324
uri = build_attach_uri(container, workdir)
278325

279326
if plan.action == "skip":
280327
logger.info("エディタの自動オープンをスキップ: %s", plan.reason)
281328
return "skip"
282329

283-
quoted = " ".join(shlex.quote(c) for c in editor)
284330
if plan.action == "print_command":
331+
# 提示コマンドは手元 (ローカル) で実行する前提。ローカルに code が無くても
332+
# 提示できるよう display (which 非依存) を用いる。
333+
quoted = " ".join(shlex.quote(c) for c in display)
285334
logger.info("SSH セッションを検出しました (%s)。", plan.reason)
286335
logger.info(
287336
"手元の VS Code で次を実行するか、VS Code の Remote-SSH 統合ターミナルから "
@@ -290,6 +339,7 @@ def open_editor(*, project_name: str, dev_service_name: str, workdir: str,
290339
logger.info(" %s --folder-uri '%s'", quoted, uri)
291340
return "print_command"
292341

342+
quoted = " ".join(shlex.quote(c) for c in editor)
293343
logger.info("[editor] %s を起動します (%s)", quoted, plan.reason)
294344
cmd = [*editor, "--folder-uri", uri]
295345
try:

tests/cli/test_project_dispatch.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,3 +417,17 @@ def test_maybe_open_editor_valid_index_within_scale(monkeypatch):
417417
lambda **kw: called.append(kw) or 'launch')
418418
container._maybe_open_editor('carmo', True, 2, 3)
419419
assert called[0]['index'] == 2
420+
421+
422+
def test_maybe_open_editor_forwards_compose_file(monkeypatch):
423+
"""compose_file 引数が open_editor まで伝播する (実コンテナ名問い合わせ用)。"""
424+
from devbase.commands import container
425+
from devbase.editor import opener
426+
monkeypatch.setattr(opener, 'is_open_enabled', lambda environ=None: True)
427+
monkeypatch.setattr(container, 'get_dev_service_name', lambda: 'dev')
428+
called = []
429+
monkeypatch.setattr(opener, 'open_editor',
430+
lambda **kw: called.append(kw) or 'launch')
431+
container._maybe_open_editor('carmo', True, 1, 1,
432+
compose_file='override.yml')
433+
assert called[0]['compose_file'] == 'override.yml'

0 commit comments

Comments
 (0)