Skip to content

Give a clear error when devpod is missing - #71

Merged
blooop merged 2 commits into
mainfrom
fix/57-missing-devpod-binary
Aug 7, 2026
Merged

blooop merged 2 commits into
mainfrom
fix/57-missing-devpod-binary

Conversation

@blooop

@blooop blooop commented Aug 7, 2026 •

Copy link
Copy Markdown
Owner

Resolves #57 on map #51.

run_devpod is the only place the package spawns devpod, so the missing binary is translated there, caught once in main(), and reported as:

$ dl --ls                                    # with no devpod on PATH
devpod not found on PATH: dl cannot manage workspaces without it. Install devpod from
https://devpod.sh/docs/getting-started/install (pixi/conda installs of devlaunch include
it; pip installs do not).
$ echo $?
127

Two modelling choices, both deliberate

Not an OSError. FileNotFoundError is one, and dl.py has ~12 broad except OSError / except (RuntimeError, OSError) handlers. Letting the condition travel as an OSError meant the attach path reported Failed to create workspace: [Errno 2]... instead of naming the real cause. Verified by experiment: rebasing the exception on OSError makes the survival test fail with assert 1 == 127.

Not a returncode. A synthetic 127 in the CompletedProcess would send callers — which all branch on returncode — down paths that assume devpod ran. An absent binary is a different kind of outcome from a command that ran and failed, so it stays out of that value domain.

Exit code 127 is the shell's own "command not found" convention, and dl otherwise propagates devpod's returncodes verbatim.

Verified against the real CLI, not just tests

env PATH=/nonexistent python -m devlaunch.dl <arg>, capturing the three streams separately:

rc stdout stderr traceback
--help 0 full help empty no
--version 0 version empty no
--ls, --repos, --completion-data, --update-cache 127 empty 1 line no

stdout staying empty matters: the completion machinery parses it, so a half-written JSON document or a stray traceback would land in the user's shell. --help/--version are also test-asserted never to call subprocess.run at all.

--update-cache now exits rather than overwriting a good completion cache with an empty one. Nothing leaks into an interactive shell either — update_cache_background spawns it with stdout/stderr set to DEVNULL.

Checked by hand

  • No except Exception can swallow it: the five in the package are in worktree/ (git and clone work) plus dl.py:920, which sits after its run_devpod call and wraps only clone cleanup.
  • run_devpod passes no cwd, so FileNotFoundError there can only mean the executable is absent — the message cannot lie about the cause.

git — assessed and left alone

The ticket allowed covering git "if it falls out naturally". It does not: git is spawned from ~20 sites across four modules, and there is no traceback to fix — with git absent, dl owner/repo@main already prints one line (Repository 'owner/repo': [Errno 2] No such file or directory: 'git') and exits 1. Improving that means a new exception threaded through three modules, or a shutil.which probe covering only the clone path. Separate ticket.

Verification

pixi run ci-no-cover green: 480 passed, 8 deselected (+13). pylint 10.00/10, ruff, ty clean. pixi.lock untouched. README documents the hint and the exit code.

Summary by Sourcery

Handle missing devpod binary as a dedicated error with a clear message and consistent exit code across the CLI.

Bug Fixes:

  • Ensure all devpod-dependent commands fail with a single-line install hint and exit code 127 when devpod is not on PATH instead of emitting tracebacks or misleading workspace errors.

Enhancements:

  • Introduce a DevpodNotInstalled signal in run_devpod and centralize its handling in main() so callers distinguish a missing binary from a devpod command that ran and failed.
  • Guarantee that completion-related commands keep stdout empty on devpod-missing errors to avoid corrupting shell completion output.

Documentation:

  • Document the devpod-missing behavior, including the stderr hint and exit code 127, in the README.

Tests:

  • Add comprehensive tests covering missing-devpod behavior across CLI entrypoints, completion flags, cache updates, and the end-to-end exit code and output contract.

On a box without devpod, every workspace command printed a raw
FileNotFoundError traceback. run_devpod is the only place the package
spawns devpod, so the missing binary is translated there into a
DevpodNotInstalled exception, caught once in main, reported as one line on
stderr, and exited 127 -- the shell's own "command not found" code.

DevpodNotInstalled inherits from Exception, deliberately not from OSError.
FileNotFoundError *is* an OSError, and dl has a dozen broad OSError
handlers; letting it travel as one meant the attach path reported "Failed
to create workspace" instead of naming the real cause.

It is also not folded into a returncode. Callers branch on returncode, so a
synthetic 127 would send them down paths that assume devpod actually ran.
A binary that is absent is a different kind of outcome from a command that
ran and failed, and it stays out of that value domain.

--help and --version never touch devpod and still work. The completion
paths (--repos, --completion-data, --update-cache) keep stdout empty, since
that is what the completion machinery parses, and --update-cache now leaves
a good cache in place rather than overwriting it with an empty one.

Closes #57

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @blooop, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@sourcery-ai

sourcery-ai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Reviewer's Guide

Introduce a dedicated DevpodNotInstalled error and centralized handling to provide a clear, single-line message and exit code 127 when the devpod binary is missing, while preserving existing behavior when devpod runs and fails, and adding comprehensive tests and README documentation around this behavior.

Sequence diagram for handling missing devpod in the dl CLI

sequenceDiagram
    actor User
    participant dl as main
    participant cli as _run_cli
    participant devpod as run_devpod
    participant sp as subprocess_run

    User->>dl: main()
    dl->>cli: _run_cli()
    cli->>devpod: run_devpod(args, env, capture)
    devpod->>sp: subprocess.run(cmd, ...)
    sp-->>devpod: FileNotFoundError
    devpod->>dl: DevpodNotInstalled(DEVPOD_MISSING_MESSAGE)
    dl-->>User: print(DEVPOD_MISSING_MESSAGE) to stderr
    dl-->>User: exit DEVPOD_MISSING_EXIT_CODE (127)
Loading

File-Level Changes

Change Details Files
Introduce a dedicated DevpodNotInstalled exception and related constants to represent a missing devpod binary distinctly from normal subprocess failures.
  • Add DevpodNotInstalled exception class in dl module, deliberately not subclassing OSError or RuntimeError to avoid broad handlers swallowing it
  • Define DEVPOD_MISSING_MESSAGE as a single-line, user-facing install hint mentioning PATH, install URL, and packaging differences
  • Define DEVPOD_MISSING_EXIT_CODE = 127 to align with the shell's "command not found" convention
devlaunch/dl.py
Translate FileNotFoundError from subprocess.run in run_devpod into DevpodNotInstalled while keeping normal devpod failures represented via return codes.
  • Wrap subprocess.run call in run_devpod in a try/except FileNotFoundError block
  • On FileNotFoundError, raise DevpodNotInstalled with DEVPOD_MISSING_MESSAGE, preserving the original exception as the cause
  • Retain existing branching on the capture argument and security notes around subprocess.run usage
devlaunch/dl.py
Centralize CLI handling of a missing devpod in main(), ensuring a clean stderr message, correct exit code, and untouched stdout for completion-related commands.
  • Refactor main() into a thin wrapper that calls a new _run_cli() function
  • In main(), catch DevpodNotInstalled, print its message to stderr, and return DEVPOD_MISSING_EXIT_CODE
  • Document in main()’s docstring that stdout must stay clean for completion consumers
devlaunch/dl.py
Add comprehensive tests to verify behavior when devpod is missing across CLI entry points, including exit codes, output streams, and interaction with broad exception handlers.
  • Introduce helper _devpod_missing() to simulate FileNotFoundError for a missing devpod binary
  • Add TestMissingDevpodBinary test class covering exception hierarchy, run_devpod translation behavior, and preservation of non-zero return codes when devpod runs
  • Add tests for dl --ls, workspace up, delete, --repos, --completion-data, and --update-cache to ensure they emit one stderr line, keep stdout empty when required, and exit 127 without being swallowed by broad handlers
  • Add tests asserting that --help and --version do not call devpod at all and behave normally without it
  • Add an end-to-end test that runs python -m devlaunch.dl with a PATH lacking devpod, asserting rc=127, empty stdout, one-line stderr without traceback, and presence of the install URL
test/test_dl.py
Document the missing-devpod behavior and exit code in the README’s pip installation section.
  • Extend pip installation note to mention that commands needing devpod emit a single-line install hint to stderr and exit with code 127 when devpod is not on PATH
  • Clarify that dl --help and dl --version continue to work even when devpod is not installed
README.md

Assessment against linked issues

Issue Objective Addressed Explanation
#57 Detect when the devpod binary is missing at the subprocess shell-out seam (run_devpod), convert the FileNotFoundError into a specific, intentional signal, and have dl print a single actionable error line (with an install hint) to stderr and exit with a non-zero status instead of a traceback. ✅
#57 Ensure CLI commands that require devpod (e.g., dl --ls and similar) fail gracefully with that clear message and exit code, while commands that should not require devpod (--help, --version) continue to work without invoking devpod; document this behavior in the README. ✅

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov

codecov Bot commented Aug 7, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.40%. Comparing base (f2a4a6d) to head (60c51e4).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #71      +/-   ##
==========================================
+ Coverage   87.15%   87.40%   +0.24%     
==========================================
  Files          10       10              
  Lines        1425     1437      +12     
==========================================
+ Hits         1242     1256      +14     
+ Misses        183      181       -2     
Files with missing lines Coverage Δ
devlaunch/dl.py 88.87% <100.00%> (+0.51%) ⬆️
devlaunch/worktree/storage.py 97.19% <ø> (ø)

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The metadata-hardening merge landed with codespell red on main, which
blocks every branch cut from it regardless of what that branch changed.
@blooop
blooop merged commit f87d8e2 into main Aug 7, 2026
9 checks passed
@blooop
blooop deleted the fix/57-missing-devpod-binary branch August 7, 2026 21:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build: clear error when devpod binary is missing

1 participant