From dbbed28f88e17d6eefdb77bcac1335aad099afe9 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 16 Sep 2026 22:55:09 +0100 Subject: [PATCH] fix: upgrade re-execs a docsgpt it can actually find `upgrade` exec'd the bare name `docsgpt`, so after `python -m docsgpt upgrade` in a virtualenv without the console script on PATH, os.execv failed with a traceback. It now uses the same launcher the service units get, which is why that helper is no longer named for native mode. --- docsgpt/deploy/commands.py | 9 +++++---- tests/deploy/test_commands.py | 13 ++++++++++++- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/docsgpt/deploy/commands.py b/docsgpt/deploy/commands.py index 0988044d..f718e2e0 100644 --- a/docsgpt/deploy/commands.py +++ b/docsgpt/deploy/commands.py @@ -218,8 +218,8 @@ def _redis_urls(base: str) -> dict[str, str]: } -def _native_launcher() -> list[str]: - """How the service files start DocsGPT: the command on PATH, or this interpreter and the module. +def _docsgpt_launcher() -> list[str]: + """How to start DocsGPT from another process: the command on PATH, or this interpreter and the module. A unit has to name something that can be executed, and ``sys.argv[0]`` often cannot be: under ``python -m docsgpt``, or pytest, it is a module file. Falling back to the running interpreter @@ -285,7 +285,7 @@ def _native_up(args, context: Context, directory: Path) -> int: (directory / "logs").mkdir(exist_ok=True) envfile.update(env_path, updates) - launcher = _native_launcher() + launcher = _docsgpt_launcher() print("Applying database migrations ...") # The child reads the stack's settings, not a .env in whatever directory this was run from. stack_env = {"DOCSGPT_HOME": str(directory), "DOCSGPT_ENV_FILE": str(env_path)} @@ -692,7 +692,8 @@ def upgrade(args, context: Optional[Context] = None) -> int: if installer == "uv": if context.run(["uv", "tool", "install", "--force", spec]) != 0: raise DeployError(f"uv could not install {spec}") - return context.exec_up(["docsgpt", "up", "--dir", str(stack.stack_dir(args.dir))]) + # The same launcher the service units get: a bare name is not always on PATH to exec. + return context.exec_up([*_docsgpt_launcher(), "up", "--dir", str(stack.stack_dir(args.dir))]) command = f"pipx install --force {spec}" if installer == "pipx" else f"pip install -U {spec}" print(f"Upgrade the package with `{command}`, then run `docsgpt up` to move the stack to it.", file=sys.stderr) return 1 diff --git a/tests/deploy/test_commands.py b/tests/deploy/test_commands.py index b73fedb2..9e9e684b 100644 --- a/tests/deploy/test_commands.py +++ b/tests/deploy/test_commands.py @@ -334,7 +334,8 @@ class TestUpgrade: ) assert _run(["upgrade", "--dir", str(tmp_path), "--version", "0.22.0"], context) == 0 assert self_calls[0] == ["uv", "tool", "install", "--force", "docsgpt==0.22.0"] - assert self_calls[1] == ["exec", "docsgpt", "up", "--dir", str(tmp_path)] + assert self_calls[1][0] == "exec" + assert self_calls[1][-3:] == ["up", "--dir", str(tmp_path)], "the launcher can be an interpreter and -m" def test_latest_when_no_version_is_given(self, tmp_path): self_calls = [] @@ -343,6 +344,16 @@ class TestUpgrade: assert _run(["upgrade", "--dir", str(tmp_path)], context) == 0 assert self_calls[0] == ["uv", "tool", "install", "--force", "docsgpt"] + def test_without_the_command_on_path_it_re_execs_the_module(self, tmp_path, monkeypatch): + """After `python -m docsgpt upgrade` there may be no docsgpt on PATH for execv to find.""" + monkeypatch.setattr("docsgpt.deploy.commands.shutil.which", lambda name: None) + monkeypatch.setattr(sys, "argv", [str(tmp_path / "not-a-program")]) + self_calls = [] + context = _context(installer=lambda: "uv", run=lambda args: 0, + exec_up=lambda argv: self_calls.append(argv) or 0) + assert _run(["upgrade", "--dir", str(tmp_path)], context) == 0 + assert self_calls[0][:3] == [sys.executable, "-m", "docsgpt"] + def test_a_pip_install_is_told_what_to_run(self, tmp_path, capsys): context = _context(installer=lambda: "pip", run=lambda args: pytest.fail("must not run")) assert _run(["upgrade", "--dir", str(tmp_path)], context) == 1