Skip to content

bench: recover the container-written clone before each cold reset - #204

Merged
blooop merged 1 commit into
mainfrom
wayfinder/devlaunch-bench-perms
Aug 14, 2026
Merged

blooop merged 1 commit into
mainfrom
wayfinder/devlaunch-bench-perms

Conversation

@blooop

@blooop blooop commented Aug 14, 2026 •

Copy link
Copy Markdown
Owner

The first real run of the launch-benchmark workflow never reached a trend point:
the cold-recreate shape's per-run reset could not remove the clone, and run 1's
launch then died on a .git it could not write.
Run 31838698495
(the run of the workflow #203 added for issue #198 — follow-up, not a reopen):

Failed to remove local clone: [Errno 13] Permission denied: PosixPath('/tmp/dl-bench/cache/devlaunch/repos/blooop/mcp-devtasks/mcp-devtasks-main-savarigi')
Failed to checkout branch 'main': fatal: Unable to create '.../.git/index.lock': Permission denied
run 1/5 exited 1; no median over a failing command

Why it is CI-only, and why the fix is not in dl

A launch leaves its clone owned by the container's user — uid 1000 (vscode) in
the standard devcontainer base image. On a developer's own uid-1000 host nothing
comes up, which is why #203's local rehearsal of the same two shapes was green;
a GitHub runner's user is not that uid. README.md already names this case ("when
part of the cache will not go"): dl cannot unlink what it does not own, so it
deletes the workspace, warns about the clone, and exits 0. The next launch in the
shape then inherits a permission-broken clone and fails in git checkout — and
--before failing to establish the cold state is exactly the thing that makes
the shape report a failed run instead of a time.

So nothing about dl is wrong here, and nothing in dl changes. The mismatch
belongs to the environment that creates it, and GitHub-hosted runners have
passwordless sudo.

The fix

The cold shape's --before becomes sudo chown -R of this job's own clone cache
and then the remove, as one command:

--before "bash -c 'test -d $XDG_CACHE_HOME/devlaunch/repos && sudo chown -R $(id -u):$(id -g) $XDG_CACHE_HOME/devlaunch/repos; dl $TARGET rm --force'"
  • Inside --before, not a step of its own. --before runs before EVERY run;
    runs 2..5 recreate the same container-owned files run 1 did, so a one-time step
    would recover run 1 and hand runs 2..5 the clone that broke it.
  • Scoped to $XDG_CACHE_HOME/devlaunch/repos — the cache the job already
    scoped into /tmp — rather than anything wider. test -d because a reset also
    runs before the first launch, when there may be nothing there.
  • Sudo is assumed in this one step, and the step says so.

Guards

test/test_bench_workflow.py gains one class, in the same text-assertion style
as the rest of the charter (three tests): the reset chowns before it removes, it
reclaims only this job's clone cache, and chown appears exactly once in the
workflow's settings — a second one would be a one-time step doing per-run work.
The alert-don't-gate guards and #195's job-fails-trend-untouched discipline are
untouched; the workflow is still not in ci.yml's needs and still
fail-on-alert: false.

pixi run ci is green locally (12 passed in test_bench_workflow.py, full suite
and lint clean). The --before plumbing — that the composed command splits into
bash -c …, that the chown and the remove both run on every run, and that the
test -d guard leaves the exit status the remove's — was rehearsed against the
real scripts/bench_launch.py with a stub dl and sudo.

Validation is the merge push itself. The defect only reproduces on a runner,
and the workflow triggers on push to main, so merging this PR runs the next
bench — which either publishes the first cold-recreate point or shows the same
step failing with the chown in place.

Summary by Sourcery

Tests:

  • Add tests asserting the cold reset command chowns the cache before removal, is scoped to the job’s cache directory, and that chown is only used in the per-run reset.

…eset

A launch leaves its clone owned by the container's user (uid 1000 in the
standard devcontainer base image), which is not a GitHub runner's own uid.
`dl rm` deleted the workspace, warned that the clone would not go and exited 0,
and the next cold run died writing .git/index.lock into a clone it did not own
-- so the cold-recreate shape reported a failed run instead of a time
(run 31838698495, the first real run of the workflow).

The mismatch is the environment's, not dl's, so the recovery is a sudo chown -R
of this job's own clone cache composed into the shape's --before command, where
it runs before EVERY run: runs 2..5 recreate the same files run 1 did.
@sourcery-ai

sourcery-ai Bot commented Aug 14, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Adjusts the bench workflow cold-recreate reset command to chown container-owned git clones back to the runner user before forced removal, and adds tests ensuring the reset command’s behavior, scope, and per-run cardinality.

File-Level Changes

Change Details Files
Make the cold-recreate bench reset chown the cached clones back to the runner user before removing them, implemented as a single --before shell command.
  • Replace the simple dl rm --force reset command with a bash -c wrapper that conditionally chowns the devlaunch clone cache and then runs dl rm --force.
  • Guard the chown with a test -d on the devlaunch/repos cache path to avoid failing when the cache directory does not yet exist.
  • Document in-place in the workflow why the chown is needed, that it assumes passwordless sudo on GitHub runners, and why it must live inside --before.
.github/workflows/bench.yml
Add tests that characterize and lock down the cold reset command’s structure, scope, and per-run execution semantics.
  • Introduce a cold_reset() helper that extracts the single --before line from bench_settings for inspection in tests.
  • Add a test class that asserts the reset command chowns before removing, is scoped to this job’s clone cache, and that chown appears exactly once to ensure the recovery is per-run via --before rather than a separate step.
  • Keep existing CI-discipline tests unchanged while extending the bench workflow charter with the new behavior.
test/test_bench_workflow.py

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

@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.

Hey - I've found 1 issue, and left some high level feedback:

  • The --before command currently lets a failing sudo chown short‑circuit the subsequent dl $TARGET rm --force and return a non‑zero status, which conflicts with the intent that the guard leave the exit status to the remove; consider restructuring the shell (e.g., if test -d ...; then sudo chown ... || true; fi; dl ...) so that dl rm always runs and controls the overall exit code where appropriate.
  • The cold_reset() helper picks the --before line using a substring search for "--before", which could be tripped by future comments or unrelated lines; tightening this to match only actual setting lines (e.g., via a prefix or regex) would make the tests more robust against incidental text changes in bench_settings().
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `--before` command currently lets a failing `sudo chown` short‑circuit the subsequent `dl $TARGET rm --force` and return a non‑zero status, which conflicts with the intent that the guard leave the exit status to the remove; consider restructuring the shell (e.g., `if test -d ...; then sudo chown ... || true; fi; dl ...`) so that `dl rm` always runs and controls the overall exit code where appropriate.
- The `cold_reset()` helper picks the `--before` line using a substring search for `"--before"`, which could be tripped by future comments or unrelated lines; tightening this to match only actual setting lines (e.g., via a prefix or regex) would make the tests more robust against incidental text changes in `bench_settings()`.

## Individual Comments

### Comment 1
<location path="test/test_bench_workflow.py" line_range="144-150" />
<code_context>
+        things this job did not create."""
+        assert "XDG_CACHE_HOME" in cold_reset()
+
+    def test_the_recovery_runs_per_run_rather_than_once_before_the_bench(self):
+        """Every run recreates those files, so a one-time step before the bench
+        recovers run 1 and leaves runs 2..5 with exactly the clone that broke
+        it. `--before` is the only place with the right cardinality."""
+        assert bench_settings().count("chown") == 1, (
+            "a second chown is a one-time step doing per-run work"
+        )
</code_context>
<issue_to_address>
**suggestion (testing):** Use a more targeted way to count `chown` occurrences to avoid false positives/negatives.

Counting plain "chown" in `bench_settings()` can match comments or unrelated text, and may miss future variations of the command. Consider instead counting only in the `--before` section (e.g. `cold_reset().count("chown") == 1`), or matching the exact `chown` command you expect, so the test stays focused on the intended call and is less brittle to wording changes.

```suggestion
    def test_the_recovery_runs_per_run_rather_than_once_before_the_bench(self):
        """Every run recreates those files, so a one-time step before the bench
        recovers run 1 and leaves runs 2..5 with exactly the clone that broke
        it. `--before` is the only place with the right cardinality."""
        reset = cold_reset()
        assert reset.count("sudo chown") == 1, (
            "a second chown in the cold-reset command would incorrectly do per-run work once"
        )
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +144 to +150
def test_the_recovery_runs_per_run_rather_than_once_before_the_bench(self):
"""Every run recreates those files, so a one-time step before the bench
recovers run 1 and leaves runs 2..5 with exactly the clone that broke
it. `--before` is the only place with the right cardinality."""
assert bench_settings().count("chown") == 1, (
"a second chown is a one-time step doing per-run work"
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Use a more targeted way to count chown occurrences to avoid false positives/negatives.

Counting plain "chown" in bench_settings() can match comments or unrelated text, and may miss future variations of the command. Consider instead counting only in the --before section (e.g. cold_reset().count("chown") == 1), or matching the exact chown command you expect, so the test stays focused on the intended call and is less brittle to wording changes.

Suggested change
def test_the_recovery_runs_per_run_rather_than_once_before_the_bench(self):
"""Every run recreates those files, so a one-time step before the bench
recovers run 1 and leaves runs 2..5 with exactly the clone that broke
it. `--before` is the only place with the right cardinality."""
assert bench_settings().count("chown") == 1, (
"a second chown is a one-time step doing per-run work"
)
def test_the_recovery_runs_per_run_rather_than_once_before_the_bench(self):
"""Every run recreates those files, so a one-time step before the bench
recovers run 1 and leaves runs 2..5 with exactly the clone that broke
it. `--before` is the only place with the right cardinality."""
reset = cold_reset()
assert reset.count("sudo chown") == 1, (
"a second chown in the cold-reset command would incorrectly do per-run work once"
)

@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.48%. Comparing base (67b5495) to head (10ff13c).

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             main     #204   +/-   ##
=======================================
  Coverage   92.48%   92.48%           
=======================================
  Files          24       24           
  Lines        3380     3380           
=======================================
  Hits         3126     3126           
  Misses        254      254           

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.

Inline review (small follow-up fix; reviewer did not author the diff): Approve.

Mechanics verified from the diff: the step shell expands $XDG_CACHE_HOME/$(id -u):$(id -g)/$TARGET once at step time, leaving shlex.split an exact ['bash','-c','test -d … && sudo chown -R …; dl … rm --force'] argv — per-run recovery inside --before, which is the only placement with the right cardinality (runs 2..5 recreate the same container-owned files). ; before the remove keeps the command's exit the remove's; test -d covers the nothing-there-yet first reset. Scope deliberately stops at the job's own repos tree. Root-cause correction accepted and better than the dispatch hypothesis: uid 1001 runner vs uid 1000 container user — the exact class README's 'when part of the cache will not go' documents, which is also why PR #203's uid-1000 local rehearsal couldn't reproduce it. Guards extend the existing text-assertion style and are mutation-checked (chown-after-rm and hoisted-one-time-step both fail). All pre-existing charter guards untouched. Validation is the merge push: this merge triggers the next bench run on a real runner.

Verdict: Approve.

@blooop
blooop merged commit 1fe94ce into main Aug 14, 2026
12 checks passed
@blooop
blooop deleted the wayfinder/devlaunch-bench-perms branch August 14, 2026 21:04
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.

1 participant