Skip to content

purge, prune: name the Docker disk neither command frees - #207

Merged
blooop merged 3 commits into
mainfrom
wayfinder/devlaunch-160-dl
Aug 14, 2026
Merged

blooop merged 3 commits into
mainfrom
wayfinder/devlaunch-160-dl

Conversation

@blooop

@blooop blooop commented Aug 14, 2026 •

Copy link
Copy Markdown
Owner

Closes #160

dl --purge and dl --prune both end on one line naming the disk they do not free:

devlaunch does not manage Docker images or volumes: the containers these workspaces used may still hold disk, and `docker system df` shows what Docker is holding.

What was there before

The decision on #155 reads as if --prune already printed this, so the first thing this branch did was check: on main, neither command says anything about Docker. --prune ends at Removed N clone director(ies) -- X. and --purge at Removed: <cache> or the refusal report. So this adds the line to both rather than extending one to the other, and a single shared constant is what keeps them saying the same thing afterwards — pinned by a test that compares the line the two commands actually printed, rather than each against the constant.

Why

The gigabytes a cleanup reports are usually not the ones being looked for. On the host #160 was decided on, --prune had 4.00 GB of stale clones to give back while docker system df read 86.5 GB reclaimable images, 43.18 GB volumes, 13.88 GB build cache — an order of magnitude more, sitting behind a command that had just said "Removed". Saying nothing is what makes a freed figure read as all of it.

The constraints the ticket is about

  • No docker subprocess. It is a sentence, not a measurement — nothing to be slow, nothing to fail where Docker is absent, stopped, or reachable only as another user. Held by a test that records every process both commands start and asserts none of them is docker.
  • No image enumeration and no parsing of devpod's image names. devpod's images carry no devlaunch or devpod label, so any list would be a guess a user then pastes into docker image rm.
  • Points, never offers. No docker image prune -a — unscoped, and it would take images devlaunch never built, the exact footgun PR dl --purge deletes only the workspaces devlaunch created #129 removed from --purge. A test scrubs both commands' output for image prune, docker image rm, docker rmi and system prune.

Where it prints

From a wrapper around each command's body rather than at each outcome, so "a run that finished names the boundary" holds for every way either can finish — including a purge that found nothing to purge and a prune answered n, which are where somebody whose disk is full is most likely to be reading. The one exit that does not print it is --prune's refusal of an unknown option, which never got as far as looking at a directory.

Tests

test/unit/test_docker_boundary.py (new) holds both commands together, with the wording written out as a literal traceable to the ticket rather than imported from the code:

  • TestWhatAPurgeSaysAboutTheDiskItDidNotFree — names the disk it does not manage / says it may still be held / points at the command that measures it
  • TestWhatAPruneSaysAboutTheDiskItDidNotFree — the same three
  • TestTheTwoCommandsCannotDriftApart::test_they_name_the_boundary_in_the_same_words
  • TestWhatTheBoundaryMayNotDo::test_neither_command_offers_to_delete_an_image
  • TestWhatTheBoundaryMayNotDo::test_naming_it_starts_no_docker_process

test/unit/test_prune_orphaned_clones.py gains TestTheDiskThisCommandDoesNotFree::test_a_run_that_removed_clones_still_names_the_docker_disk_it_does_not_manage, because the fixture that removes something real lives there and the removal path is the one a freed figure is read on.

All eight assertions were checked to bite: with the print neutered, eight of the ten new tests fail. (The two constraint tests pass vacuously by design — they are there to fail if someone later adds a docker image rm to the line.)

Docs

README shows the line in both commands' captured output and adds "The disk neither command frees" explaining the sentence-not-a-measurement and points-not-offers properties; the --ls --size note that already drew the same boundary now links there instead of being a second copy. --help says it under --purge, for both commands. CHANGELOG carries the entry with the measurement it answers.

Verification

pixi run ci green: 1610 passed, pylint 10.00/10, ruff and ty clean. Also run for real against a scratch XDG_CACHE_HOME — dl --prune -y (nothing to prune) and dl --purge -y (foreign workspaces named and left standing) both print the line as their last output.

🤖 Generated with Claude Code

Summary by Sourcery

Add a shared Docker disk boundary message printed at the end of both --purge and --prune runs, clarifying the cleanup scope and pointing users to docker system df.

Enhancements:

  • Introduce a reusable helper and constant to ensure --purge and --prune share identical wording about the Docker-managed disk they do not free.
  • Update CLI help text and README to describe the Docker disk boundary, emphasizing that it is informational, does not run Docker, and does not offer to delete images.
  • Document the behavior in CHANGELOG, including the rationale and constraints around not invoking Docker or suggesting image deletion.

Tests:

  • Add a dedicated test module to verify both commands print the same Docker boundary line, never suggest destructive Docker operations, and do not start Docker processes.
  • Extend existing prune tests to ensure runs that actually remove clones still end with the Docker boundary message.

blooop added 2 commits August 14, 2026 23:00
Both commands report freeing disk and neither frees the disk most machines
are actually holding: on the host devlaunch#160 was decided, a prune had
4.00 GB of stale clones to give back while `docker system df` read 86.5 GB
of reclaimable images, 43.18 GB of volumes and 13.88 GB of build cache. A
freed figure printed on its own reads as all of it, and was out by an order
of magnitude.

So both end on one line, from one constant, so the two cannot drift apart.
Printed from a wrapper around each command's body rather than at each of
their outcomes, which is what makes "a run that finished names the boundary"
true of every way either can finish -- including the ones that removed
nothing, where somebody whose disk is full is most likely to be reading.
`--prune`'s refusal of an unknown option is the one exit that does not, never
having looked at a directory.

A sentence, not a measurement: no docker process, no image enumeration, no
parsing of devpod's image names. Nothing to be slow, and nothing to fail
where Docker is absent or stopped. It points at `docker system df` and never
at `docker image prune -a`, which is unscoped and would take images
devlaunch never built -- the footgun PR #129 took out of `--purge` -- nor at
a `docker image rm` for ids `dl` cannot honestly supply, devpod's images
carrying no devlaunch or devpod label.
The line is documentation that ships as output, so the README shows it in
both commands' captured output and says what it is for, the help says it
under `--purge` for both, and the changelog carries the measurement it
answers. The `--ls --size` note already drew the same boundary for its own
reasons; it now links to the section rather than being a second copy of it.

@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 14, 2026

Copy link
Copy Markdown

Reviewer's Guide

This PR introduces a shared "Docker boundary" message that both dl --purge and dl --prune print at the end of successful runs, refactors their implementations to route through shared helpers, and adds tests and documentation to pin the exact wording, behavioral constraints (no docker subprocess, no image deletion offers), and user-facing explanation of the boundary.

Sequence diagram for shared Docker boundary in prune and purge commands

sequenceDiagram
    actor User
    participant dl as dl
    participant purge_all_data
    participant _purge_devlaunch_data
    participant prune_command
    participant _prune_clone_directories
    participant print_docker_boundary

    %% Purge flow
    User->>dl: dl --purge
    dl->>purge_all_data: purge_all_data()
    purge_all_data->>_purge_devlaunch_data: _purge_devlaunch_data()
    _purge_devlaunch_data-->>purge_all_data: status
    purge_all_data->>print_docker_boundary: print_docker_boundary()
    print_docker_boundary-->>purge_all_data: prints Docker boundary line
    purge_all_data-->>dl: status

    %% Prune flow
    User->>dl: dl --prune [flags]
    dl->>prune_command: prune_command(flags)
    prune_command->>_prune_clone_directories: _prune_clone_directories(flags)
    _prune_clone_directories-->>prune_command: status
    prune_command->>print_docker_boundary: print_docker_boundary()
    print_docker_boundary-->>prune_command: prints Docker boundary line
    prune_command-->>dl: status
Loading

File-Level Changes

Change Details Files
Add a shared Docker boundary message and helper, and integrate it into purge and prune command flows so both end with the same line naming the Docker disk they do not free.
  • Introduce a DOCKER_BOUNDARY constant containing the boundary sentence about Docker-managed disk and docker system df.
  • Add a print_docker_boundary() helper with a detailed docstring explaining why this is a sentence-not-a-measurement and points-not-offers.
  • Refactor purge_all_data into a wrapper that calls a new _purge_devlaunch_data helper, prints the boundary line unconditionally after the purge, and returns the original status code.
  • Refactor prune_command to delegate work to a new _prune_clone_directories helper, printing the boundary line after a successful or no-op run, but not when refusing unknown options.
devlaunch/dl.py
Extend user-facing help and README to document the Docker boundary line, its rationale, and how it relates to size reporting.
  • Update print_help output so the --purge help text explicitly states that neither prune nor purge removes Docker images/volumes/build cache and that both end by pointing at docker system df.
  • Show the boundary line in the purge example output in README and link to a new section describing "The disk neither command frees".
  • Add a README section detailing the mismatch between cleanup-reported gigabytes and Docker-held disk, and clarifying that the boundary is a sentence, not a measurement, and only points to docker system df without offering image deletion commands.
  • Adjust the existing --ls --size note to link to the new boundary section instead of duplicating the explanation.
devlaunch/dl.py
README.md
Add tests that pin the Docker boundary behavior for both commands, ensure their wording stays identical, and enforce constraints around not starting docker and not suggesting image-deleting commands.
  • Create test/unit/test_docker_boundary.py with fixtures and helpers to run dl --purge -y and dl --prune -y under patched subprocess.run and cache directory, capturing output and started processes.
  • Add tests that --purge and --prune each print the three key substrings of the boundary line: that devlaunch does not manage Docker images/volumes, that containers may still hold disk, and that docker system df is the tool that measures it.
  • Add a test that extracts the actual boundary line from each command’s output and asserts they are exactly equal, preventing the two commands from drifting apart in wording.
  • Add tests that combined output from both commands never contains image-deletion or prune suggestions like "image prune", "docker image rm", "docker rmi", or "system prune".
  • Add tests that no docker subprocess is started when printing the boundary by asserting "docker" is absent from recorded subprocess program names.
  • Extend test/unit/test_prune_orphaned_clones.py with a test that a run removing real clones still ends by printing the Docker boundary line and references docker system df.
  • Record the feature and its rationale (sentence-not-a-measurement, points-not-offers, no docker subprocess, no image deletion) in CHANGELOG.md under the new entry for issue Name the Docker boundary after --purge and --prune #160.
test/unit/test_docker_boundary.py
test/unit/test_prune_orphaned_clones.py
CHANGELOG.md

Assessment against linked issues

Issue Objective Addressed Explanation
#160 Add a shared boundary line that is printed at the end of both dl --purge and dl --prune, explaining that devlaunch does not manage Docker images or volumes, that containers may still hold disk, and that docker system df shows what Docker is holding. ✅
#160 Ensure the implementation respects the constraints: no Docker subprocess is started, no Docker image enumeration or name parsing is performed, and the output only points to docker system df without offering or mentioning destructive commands like docker image prune -a or docker image rm. ✅
#160 Update documentation (README, --help, CHANGELOG`) and tests to describe, assert, and keep the Docker boundary behavior and its constraints in sync for both commands. ✅

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 14, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.28%. Comparing base (f5f1333) to head (69549af).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #207      +/-   ##
==========================================
+ Coverage   92.25%   92.28%   +0.02%     
==========================================
  Files          24       24              
  Lines        3436     3448      +12     
==========================================
+ Hits         3170     3182      +12     
  Misses        266      266              
Files with missing lines Coverage Δ
devlaunch/dl.py 94.47% <100.00%> (+0.04%) ⬆️

Impacted file tree graph

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

@blooop blooop left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This was generated by AI during review.

Two-axis review of #207 against ticket #160 at merge-base f5f1333. Preflight: all checks green (ci ×5, e2e, prek, gate, codecov patch+project), non-empty diff (5 files, +304/−3), MERGEABLE.

Standards

BLOCKING

  • CHANGELOG entry is filed under a released section. The new bullet lands at CHANGELOG.md:111, i.e. below ## [0.0.26] - 2026-08-14 (line 95), not under ## [Unreleased] (line 8). The last three CHANGELOG commits on main — dfd738b, dc75158, 461f740 — all wrote into [Unreleased] (hunks at lines 66, 50, 19), and pyproject.toml already reads version = "0.0.26", so as written the entry tells readers a feature shipped in a release that does not contain it. One-line move to [Unreleased]'s ### Added.

Non-blocking

  • DOCKER_BOUNDARY (devlaunch/dl.py:887) diverges from its two siblings in both name and home: DEVPOD_MISSING_MESSAGE / SSH_MISSING_MESSAGE are *_MESSAGE, live in the constants block at lines 95–118, and each carries a "why these words" comment above it. DOCKER_BOUNDARY_MESSAGE, or a comment, would close the gap. (nit)
  • Constant + wrapper is two names for one sentence with one reader each — the tests deliberately use literals, so DOCKER_BOUNDARY is read only by print_docker_boundary. Mild Middle Man; inlining loses nothing. The print_* wrapper itself is squarely in-idiom (print_prune_plan, print_workspaces, print_help), and extracting _purge_devlaunch_data / _prune_clone_directories so the line covers every exit is the right shape — the repeated 3-line wrapper body twice is the only structural duplication introduced, and it is cheaper than the alternative. (observation)
  • Help text (dl.py:3401) documents the boundary only on --purge, as "Like --prune…", while --prune's own entry — which the reader meets first — says nothing. Also, README.md:414's command-table row for --purge is unchanged; AGENTS.md asks README to track help output, though the new #### The disk neither command frees section and its links arguably discharge that. (nit)
  • run_purge in test/unit/test_docker_boundary.py patches devlaunch.dl.update_cache_background, but --purge never reaches it (wants_startup_cache_refresh returns True only for _CACHE_READING_COMMANDS, and no purge-path call exists). Inert scaffolding copied from test_purge_ownership.py; run_prune omits it, so the two helpers are asymmetric for no reason. (nit)
  • RecordedProcesses is the fifth subprocess recorder in test/unit/ (four RecordedDevpod classes), despite test/fixtures/devpod_mock.py existing as a shared home. Per-file recorders are the suite's established practice and this one has a genuinely different job (records all argv), so consistent — just noting the trend. (observation)
  • The 86.5/43.18/13.88 GB figures now appear in four places (CHANGELOG, README, dl.py docstring, test module docstring). Host-measured citations in docstrings are repo idiom; four copies of one number is four things to keep true. (observation)

Test conventions otherwise match well: behavioural class/test names, strings written out rather than imported (with the reason stated), and boundary_line's exactly-one assertion plus the purge/prune equality test are the right drift guards. test_docker_boundary.py, test_purge_ownership.py, test_purge_partial_removal.py: 54 passed.

Spec

Verified against ticket #160 (fetched), the exact diff, and by running the code. Nothing blocks on this axis.

Met

  • "Print one line at the end of --purge and --prune: devlaunch does not manage Docker images or volumes, the containers these workspaces used may still hold disk, and docker system df shows what Docker is holding." — one shared constant DOCKER_BOUNDARY (devlaunch/dl.py:887), printed via wrappers purge_all_data (:929) and prune_command (:2404). All three clauses present verbatim.
  • "No docker subprocess — nothing to fail when Docker is absent." — the whole implementation is print(DOCKER_BOUNDARY); no docker invocation anywhere in the diff. Held (vacuously, as the PR declares) by test_naming_it_starts_no_docker_process.
  • "No image enumeration, and no parsing of devpod's <workspace-id>-<5hex>:devpod-<32hex> naming." — no enumeration or parsing added.
  • "Point at docker system df … and never at docker image prune -a" — the printed line names only docker system df; test_neither_command_offers_to_delete_an_image scrubs both commands' output for image prune, docker image rm, docker rmi, system prune.
  • "Tested: the boundary line is asserted in both commands' captured output … and README / --help / CHANGELOG say the same thing." — test/unit/test_docker_boundary.py (9 tests, pass; wording written as literals, drift test compares the two commands' actual lines) plus one in test_prune_orphaned_clones.py. README, --help and CHANGELOG all updated.

Findings

  • Non-blocking (edge path + doc overclaim). "Print one line at the end of --purge and --prune". Both abort paths run: dl --prune answered n prints the line; dl --purge answered n prints Aborted. and stops — the purge confirmation is in main() (dl.py:3651-3655), outside the wrapper. Defensible (an aborted purge reported no freed figure), but it is asymmetric with prune's own n, is not declared in the PR body's "Where it prints" (which lists only the unknown-flag exit as the exception), and two claims are then false as written: the purge_all_data docstring's "holds for every way a purge can end" (:925) and README's "That last line ends every purge" (README.md:489). Either print it on abort or narrow both sentences.
  • Non-blocking. --prune's report_unlocatable failure exit (dl.py:2417-2419, status 1) also prints the line — a failure report rather than a cleanup. Harmless, and consistent with the docstring's stated scope.

PR body claims checked: "on main, neither command says anything about Docker" — confirmed (no system df in dl.py at the merge base). Test inventory and the "8 of 10 bite" split match the diff.

Verdict

Request changes — one blocking finding:

  1. Move the CHANGELOG entry from under ## [0.0.26] to ## [Unreleased] → ### Added (Standards).

Worth folding into the same round because it makes two shipped sentences false: resolve the purge-n asymmetry — either print the boundary line on the aborted-purge path too, or narrow the purge_all_data docstring (":925 holds for every way a purge can end") and README:489 ("That last line ends every purge") to match what the code does (Spec, non-blocking on its own).

All other findings are non-blocking and recorded above. GitHub refuses --request-changes from the PR author's account, so this is posted as a comment; this written verdict governs.

… purge

Blocking (Standards): the boundary-line bullet was filed under `## [0.0.26]`,
a section pyproject.toml already claims as released, so it told readers the
feature shipped in a release that does not contain it. Moved to
`## [Unreleased]` -> `### Added`, where dfd738b, dc75158 and 461f740 filed
theirs.

Spec: `dl --prune` answered `n` ended on the Docker boundary line, `dl --purge`
answered `n` printed `Aborted.` and stopped -- the purge confirmation lives in
main(), outside the wrapper that prints the line. That made two shipped
sentences false: purge_all_data's "holds for every way a purge can end" and
README's "That last line ends every purge".

Resolved by removing the asymmetry rather than documenting it: the aborted
purge now prints the line too, through the existing print_docker_boundary
wrapper so there is still one sentence and one literal. The listing above the
prompt is a report of what a purge would take, which is where the disk it would
not take is worth naming -- somebody who just declined is still looking for the
gigabytes. Docstring, README and CHANGELOG now say so, and
TestAPurgeAnsweredNoEndsThereToo pins the abort path, including that its line
is byte-identical to a completed purge's.
@blooop
blooop merged commit fc800ae into main Aug 14, 2026
12 checks passed
@blooop
blooop deleted the wayfinder/devlaunch-160-dl branch August 14, 2026 22:21
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.

Name the Docker boundary after --purge and --prune

1 participant