fix: run the module when the docsgpt command is not on PATH

Refusing to write the service units when `docsgpt` is not on PATH was wrong. A
package installed in a virtualenv is runnable whether or not its console script
is on PATH, and CI runs pytest as `python -m pytest`, where argv[0] is a module
file: the refusal failed thirteen native tests there.

The launcher now prefers the command on PATH, resolved to an absolute path
since PATH can hold relative entries, then an argv[0] that can be executed, and
otherwise this interpreter with `-m docsgpt`, which works wherever the package
is importable. `python -m docsgpt` became an entrypoint of its own and has a
test that runs it.
This commit is contained in:
Alex committed 2026-09-16 22:45:54 +01:00
1 parent f9f52e99f0
commit a96734da07
5 files changed
+53 -20

No files matched your search

+18 -7
View File
@@ -218,13 +218,24 @@ def _redis_urls(base: str) -> dict[str, str]:
}
def _own_executable() -> str:
"""The docsgpt command to put in the service files, or a DeployError naming what to install."""
def _native_launcher() -> list[str]:
"""How the service files start DocsGPT: 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
works wherever the package is importable, which it must be to have got here.
"""
found = shutil.which("docsgpt")
if found:
# Absolute: PATH can hold relative entries, and a unit file needs a program it can exec.
return [str(Path(found).resolve())]
candidate = Path(sys.argv[0]).resolve()
if candidate.is_file() and os.access(candidate, os.X_OK):
return str(candidate)
return [str(candidate)]
if sys.executable:
return [sys.executable, "-m", "docsgpt"]
raise DeployError(
"the docsgpt command is not on PATH, so the services would have nothing to run. "
"could not work out how to start docsgpt for the services. "
"Install it with `uv tool install docsgpt` (or `pip install docsgpt`) and run this again."
)
@@ -274,14 +285,14 @@ def _native_up(args, context: Context, directory: Path) -> int:
(directory / "logs").mkdir(exist_ok=True)
envfile.update(env_path, updates)
executable = shutil.which("docsgpt") or _own_executable()
launcher = _native_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)}
if context.run([executable, "migrate"], stack_env) != 0:
if context.run([*launcher, "migrate"], stack_env) != 0:
raise DeployError("`docsgpt migrate` failed; check the database URL and that the server is reachable.")
for unit in native.units_for(directory, executable, port, directory):
for unit in native.units_for(directory, launcher, port, directory):
services.install(unit)
for name in native.SERVICES:
print(f"Starting {name} ...")
+7 -4
View File
@@ -204,20 +204,23 @@ def services_for_platform(platform: str = sys.platform):
)
def units_for(stack_directory: Path, executable: str, port: int, home: Path) -> list[Unit]:
"""The API and worker services for a native install in ``stack_directory``."""
def units_for(stack_directory: Path, launcher: list[str], port: int, home: Path) -> list[Unit]:
"""The API and worker services for a native install in ``stack_directory``.
``launcher`` is how DocsGPT is started: the ``docsgpt`` command, or an interpreter and ``-m``.
"""
environment = {"DOCSGPT_HOME": str(home)}
return [
Unit(
name=API_SERVICE,
arguments=[executable, "api", "--host", "127.0.0.1", "--port", str(port)],
arguments=[*launcher, "api", "--host", "127.0.0.1", "--port", str(port)],
environment=dict(environment),
working_directory=str(stack_directory),
log_file=str(stack_directory / "logs" / "api.log"),
),
Unit(
name=WORKER_SERVICE,
arguments=[executable, "worker"],
arguments=[*launcher, "worker"],
environment=dict(environment),
working_directory=str(stack_directory),
log_file=str(stack_directory / "logs" / "worker.log"),