Skip to content

test(e2e): bind-mount /tmp on disk and surface failed tests in CI summary#9570

Merged
jdx merged 1 commit intomainfrom
test-e2e-disk-and-failure-summary
May 3, 2026
Merged

test(e2e): bind-mount /tmp on disk and surface failed tests in CI summary#9570
jdx merged 1 commit intomainfrom
test-e2e-disk-and-failure-summary

Conversation

@jdx
Copy link
Copy Markdown
Owner

@jdx jdx commented May 3, 2026

Summary

  • Fixes Swift e2e ENOSPC failure. e2e/run_test's Docker invocation mounted the container's /tmp and /root via --tmpfs, which is RAM-backed and capped at ~half of RAM. Multi-GB tool extractions (e.g. swift) blew past that limit (No space left on device (os error 28)). Replaced with disk-backed bind mounts under RUNNER_TEMP, mirroring the pattern already used in .github/workflows/registry.yml.
  • Surfaces failed e2e tests in the GitHub Actions UI. With parallel e2e (#9563), per-test ::group:: blocks are still replayed in order but the runner emitted no aggregate failure list, making it tedious to find which test failed in the GitHub Actions log. e2e/run_all_tests now tracks failed tests during the in-order replay and emits:
    • a stderr block at the bottom listing every failed test
    • an ::error title=E2E failures (N)::test1,test2,... annotation that surfaces in the workflow's annotations panel
    • a ### :x: Failed e2e tests (N) section in the GHA step summary

Test plan

  • bash -n and shellcheck -x clean on e2e/run_test and e2e/run_all_tests
  • mise run lint-fix clean
  • Smoke test of the failure-summary tail (verified stderr, ::error:: line, and step-summary content shape)
  • Confirm the e2e tranche that previously OOMed on swift now passes
  • Confirm a deliberately-failing test surfaces in the annotations panel and step summary

🤖 Generated with Claude Code


Note

Medium Risk
Medium risk because it changes how e2e tests run in Docker (/tmp and /root mounts) and alters CI reporting; failures could show up as new CI flakiness or permission/cleanup issues rather than product behavior.

Overview
Improves e2e CI ergonomics and reliability.

e2e/run_test replaces Docker --tmpfs usage for /tmp and /root with disk-backed bind mounts (created under ${MISE_E2E_HOST_TMP:-${RUNNER_TEMP:-${TMPDIR:-/tmp}}}) to avoid ENOSPC on large tool installs, and ensures those temp dirs are cleaned up while preserving the container exit code.

e2e/run_all_tests now tracks which parallel tests failed and, at the end of the run, emits an aggregated failure list to stderr and (on GitHub Actions) a ::error annotation plus a “Failed e2e tests” section appended to the step summary.

Reviewed by Cursor Bugbot for commit c5ce6ec. Bugbot is set up for automated code reviews on this repo. Configure here.

…mary

The Docker-wrapped e2e job was failing on Swift install with `No space
left on device`: `run_test` mounted the container's /tmp via
`--tmpfs /tmp:exec`, which is RAM-backed and capped at ~half of RAM, so
multi-GB extractions like swift blew past it. Replace the tmpfs mounts
with disk-backed bind mounts under `RUNNER_TEMP`, mirroring the pattern
already used in `registry.yml`.

Now that e2e tests run in parallel, a single failure was hard to find in
the GitHub UI — the per-test ::group:: blocks are replayed in order but
the runner emitted no aggregate failure list. Track failed tests during
the in-order replay and emit a stderr block, an `::error::` annotation,
and a step-summary section listing each failed test by name.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request improves E2E test reporting by tracking failed tests and displaying a summary, with specific support for GitHub Actions. It also updates the Docker execution logic to use disk-backed host bind-mounts for /tmp and /root, avoiding capacity limits of RAM-backed tmpfs during large tool installations. A review comment recommends using a shell trap to ensure temporary host directories are cleaned up even if the process is interrupted.

Comment thread e2e/run_test
Comment on lines +219 to +222
local rc=0
"${docker_args[@]}" "$image" bash /mise-src/e2e/run_test "$TEST" || rc=$?
rm -rf "$host_tmp" "$host_root"
return "$rc"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The temporary host directories created for Docker bind-mounts are currently only cleaned up if the script reaches the end of the run_in_docker function. If the script is interrupted (e.g., by a SIGINT/Ctrl-C), these directories will remain on the host. Using a trap ensures that cleanup occurs regardless of how the script exits, which is especially useful for local development.

  local rc=0
  trap 'rm -rf "$host_tmp" "$host_root"' EXIT
  "${docker_args[@]}" "$image" bash /mise-src/e2e/run_test "$TEST" || rc=$?
  return "$rc"

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 3, 2026

Greptile Summary

This PR fixes Swift e2e ENOSPC failures by replacing RAM-backed --tmpfs Docker mounts with disk-backed bind mounts under RUNNER_TEMP, and improves CI observability by collecting failed test names and surfacing them via a stderr list, a GHA ::error:: annotation, and a step-summary section.

Confidence Score: 4/5

Safe to merge; only P2 style/hardening concerns remain around temp-dir cleanup on interrupt and the missing exec mount flag.

All findings are P2. The bind-mount approach is correct and mirrors the existing pattern in registry.yml. The failure-reporting logic in run_all_tests is straightforward and handles edge cases well.

e2e/run_test — the missing exec mount option and the lack of an interrupt-safe cleanup trap are worth a second look before the script runs on self-hosted runners.

Important Files Changed

Filename Overview
e2e/run_test Replaces RAM-backed --tmpfs Docker mounts with disk-backed bind mounts under RUNNER_TEMP; adds cleanup of host dirs after container exits. Two P2 concerns: missing exec mount option and no interrupt-safe cleanup trap.
e2e/run_all_tests Adds FAILED_TESTS array populated during in-order log replay, then emits a stderr summary, GHA ::error:: annotation, and step-summary section when any tests fail. Logic is correct and well-scoped.

Sequence Diagram

sequenceDiagram
    participant RA as run_all_tests
    participant RT as run_test (child)
    participant D as Docker container

    RA->>RT: spawn (parallel, up to E2E_JOBS)
    RT->>RT: mktemp host_tmp, host_root
    RT->>RT: chmod 777 host_tmp host_root
    RT->>D: docker run --rm --read-only -v host_root:/root -v host_tmp:/tmp
    D-->>RT: exit (rc)
    RT->>RT: rm -rf host_tmp host_root
    RT-->>RA: write status file + log

    RA->>RA: in-order replay of logs
    alt test_status != 0
        RA->>RA: FAILED_TESTS += test_name
    end

    RA->>RA: print Failed e2e tests to stderr
    opt GITHUB_ACTIONS set
        RA->>RA: echo ::error title=E2E failures::...
        opt GITHUB_STEP_SUMMARY set
            RA->>RA: append Failed e2e tests section to summary
        end
    end
Loading

Fix All in Claude Code

Reviews (1): Last reviewed commit: "test(e2e): bind-mount /tmp on disk and s..." | Re-trigger Greptile

Comment thread e2e/run_test
Comment on lines +219 to +222
local rc=0
"${docker_args[@]}" "$image" bash /mise-src/e2e/run_test "$TEST" || rc=$?
rm -rf "$host_tmp" "$host_root"
return "$rc"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Temp dirs not cleaned up on interrupt

rm -rf "$host_tmp" "$host_root" only runs when the Docker command returns normally (or with a non-zero exit). If the outer run_all_tests process receives SIGINT/SIGTERM and kills its child processes via its cleanup trap, the run_in_docker bash function is terminated before it reaches the rm -rf line, leaving both directories on disk. On ephemeral GHA runners this is harmless since RUNNER_TEMP is wiped post-job, but for local/long-lived runners the dirs accumulate silently.

A local trap inside run_in_docker would ensure cleanup:

  local rc=0
  trap 'rm -rf "$host_tmp" "$host_root"' EXIT
  "${docker_args[@]}" "$image" bash /mise-src/e2e/run_test "$TEST" || rc=$?
  trap - EXIT
  rm -rf "$host_tmp" "$host_root"
  return "$rc"

Fix in Claude Code

Comment thread e2e/run_test
Comment on lines +196 to +198
host_tmp="$(mktemp -d "$host_tmp_root/mise-e2e-tmp.XXXXXX")"
host_root="$(mktemp -d "$host_tmp_root/mise-e2e-root.XXXXXX")"
chmod 777 "$host_tmp" "$host_root"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Bind-mount lacks explicit exec option

The previous --tmpfs /root:exec --tmpfs /tmp:exec explicitly set the exec mount flag, which is required to run binaries installed under those paths inside the container. The replacement -v bind-mounts inherit the host filesystem's mount flags; if RUNNER_TEMP (or TMPDIR) is on a filesystem mounted with noexec (possible in some self-hosted or hardened runner configurations), any tool installed into /root (the container's home dir) will be non-executable and tests will silently fail.

Consider adding :exec to the Docker mount options or documenting that the host path must be on an exec filesystem:

    -v "$host_root:/root:exec"
    -v "$host_tmp:/tmp:exec"

Note: Docker's -v option does support per-volume mount options in the form src:dst:options.

Fix in Claude Code

@jdx
Copy link
Copy Markdown
Owner Author

jdx commented May 3, 2026

Looked at both review suggestions — neither applies to this codebase:

  1. Cleanup-on-interrupt trap: e2e runs only on ephemeral GHA / Namespace runners. RUNNER_TEMP is wiped at job teardown, so leaked dirs from a SIGINT mid-run don't accumulate. Greptile's own comment notes this is "harmless" for ephemeral runners. Skipping.

  2. -v src:dst:exec: Not a valid Docker mount mode. docker run -v /tmp:/mnt:exec alpine true errors with invalid mode: exec. Docker's -v only supports ro/rw/z/Z/propagation flags — exec is a --tmpfs option, not a -v option. The bind mount inherits exec from the host filesystem, which is the case on the namespace runners we target. Skipping.

This comment was generated by an AI coding assistant.

@jdx jdx enabled auto-merge (squash) May 3, 2026 16:33
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 3, 2026

Hyperfine Performance

mise x -- echo

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.4.28 x -- echo 20.6 ± 1.1 18.4 24.7 1.00
mise x -- echo 20.7 ± 1.0 18.5 24.8 1.01 ± 0.07

mise env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.4.28 env 19.7 ± 1.0 17.4 23.9 1.00
mise env 20.2 ± 1.1 18.0 25.1 1.03 ± 0.08

mise hook-env

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.4.28 hook-env 20.5 ± 1.0 18.5 24.3 1.00
mise hook-env 20.8 ± 1.0 18.9 25.7 1.02 ± 0.07

mise ls

Command Mean [ms] Min [ms] Max [ms] Relative
mise-2026.4.28 ls 17.1 ± 1.0 15.1 22.7 1.00
mise ls 17.5 ± 0.9 15.8 21.3 1.03 ± 0.08

xtasks/test/perf

Command mise-2026.4.28 mise Variance
install (cached) 125ms 129ms -3%
ls (cached) 60ms 62ms -3%
bin-paths (cached) 65ms 66ms -1%
task-ls (cached) 546ms 546ms +0%

@jdx jdx merged commit f6fd3de into main May 3, 2026
38 checks passed
@jdx jdx deleted the test-e2e-disk-and-failure-summary branch May 3, 2026 16:38
mise-en-dev added a commit that referenced this pull request May 3, 2026
### 🚀 Features

- **(conda)** graduate conda backend out of experimental by @jdx in
[#9544](#9544)
- **(deps)** Add dart and flutter providers by @tjarvstrand in
[#9505](#9505)
- **(registry)** add neo4j by @mnm364 in
[#9525](#9525)
- **(registry)** add rustfs by @mnm364 in
[#9530](#9530)
- **(task)** support exclusion patterns in task sources by
@jlarmstrongiv in [#9496](#9496)
- **(vfox)** add stat function to lua file module by @esteve in
[#9497](#9497)

### 🐛 Bug Fixes

- **(backend)** flag regex prerelease versions by @jdx in
[#9500](#9500)
- **(backend)** mark -nightly/-canary/-experimental as prereleases by
@jdx in [#9523](#9523)
- **(backend)** suppress no-versions warning for unresolved-latest
backends by @jdx in [#9548](#9548)
- **(backend)** include dotnet prereleases from package flags by @jdx in
[#9551](#9551)
- **(backend)** scope PEP 440 prerelease detection to Python backends by
@jdx in [#9558](#9558)
- **(cargo)** Apply install_env during cargo install by @c22 in
[#9502](#9502)
- **(copr)** drop epel-9 chroots since rust >= 1.91 is unavailable by
@jdx in [#9484](#9484)
- **(github)** skip attestations on non-default api_url by @jdx in
[#9486](#9486)
- **(github)** retry ip allow list errors without auth by @risu729 in
[#9506](#9506)
- **(http)** update versions host tracking endpoint by @jdx in
[#9527](#9527)
- **(install)** don't warn for configured tools when version is passed
via CLI by @jdx in [#9522](#9522)
- **(install)** refresh latest before installing missing tools by @jdx
in [#9545](#9545)
- **(install)** don't cache nonexistent install paths by @jdx in
[#9553](#9553)
- **(lockfile)** don't propagate ad-hoc CLI overrides into the project
lockfile by @jdx in [#9562](#9562)
- **(plugin)** detect plugin types after cloning by @risu729 in
[#9540](#9540)
- **(release)** pass --no-git-checks to aube publish by @jdx in
[#9483](#9483)
- **(task)** convert PATH to MSYS Unix form when spawning POSIX shells
on Windows by @JamBalaya56562 in
[#9547](#9547)

### 📚 Documentation

- **(contributing)** require popularity check for registry PRs by @jdx
in
[7bbeebe](7bbeebe)
- **(watch)** update pitchfork domain to en.dev by @risu729 in
[#9536](#9536)
- document ghtkn GitHub token setup by @jdx in
[#9546](#9546)
- clarify registry backend acceptance policy by @jdx in
[#9543](#9543)
- Change exec command to use bash for variable echo by @kuboon in
[#9567](#9567)

### 🧪 Testing

- **(e2e)** run test-tool targets in parallel by @jdx in
[#9564](#9564)
- **(e2e)** run tests in parallel by @jdx in
[#9563](#9563)
- **(e2e)** bind-mount /tmp on disk and surface failed tests in CI
summary by @jdx in [#9570](#9570)
- **(tasks)** migrate test_task_help atask to usage field by @jdx in
[#9549](#9549)

### 📦️ Dependency Updates

- update fedora:45 docker digest to 8b838b3 by @renovate[bot] in
[#9507](#9507)
- update ghcr.io/jdx/mise:deb docker digest to f02194c by @renovate[bot]
in [#9509](#9509)
- update taiki-e/install-action digest to 7769b73 by @renovate[bot] in
[#9512](#9512)
- update ghcr.io/jdx/mise:alpine docker digest to 581f8a8 by
@renovate[bot] in [#9508](#9508)
- update rust crate ctor to v0.10.1 by @renovate[bot] in
[#9515](#9515)
- update ghcr.io/jdx/mise:rpm docker digest to a5c9655 by @renovate[bot]
in [#9510](#9510)
- update rust docker digest to a9cfb75 by @renovate[bot] in
[#9511](#9511)
- update rust crate age to v0.11.3 by @renovate[bot] in
[#9514](#9514)
- update rust crate jiff to v0.2.24 by @renovate[bot] in
[#9516](#9516)
- update dependency vitepress-plugin-tabs to ^0.9.0 by @renovate[bot] in
[#9518](#9518)
- update autofix-ci/action action to v1.3.4 by @renovate[bot] in
[#9513](#9513)
- update rust crate usage-lib to v3.2.1 by @renovate[bot] in
[#9517](#9517)
- update apple-actions/import-codesign-certs action to v7 by
@renovate[bot] in [#9519](#9519)
- update taiki-e/install-action digest to 51cd0b8 by @renovate[bot] in
[#9531](#9531)
- exclude taiki-e/install-action from renovate by @jdx in
[#9532](#9532)
- update rust crate blake3 to v1.8.5 by @renovate[bot] in
[#9533](#9533)

### 📦 Registry

- enable shellcheck on windows by @zeitlinger in
[#9487](#9487)
- add google-java-format by @zeitlinger in
[#9488](#9488)
- add expert
([aqua:expert-lsp/expert](https://github.com/expert-lsp/expert)) by
@AlternateRT in [#9498](#9498)
- update entry for checkmake by @eread in
[#9504](#9504)
- add systemctl-tui
([aqua:rgwood/systemctl-tui](https://github.com/rgwood/systemctl-tui))
by @2xdevv in [#9521](#9521)
- add codon by @3w36zj6 in
[#9538](#9538)
- add tool yr (backend:github:VirusTotal/yara-x) by @adam-moss in
[#9542](#9542)
- add tool betterleaks (backend:aqua/betterleaks/betterleaks) by
@adam-moss in [#9541](#9541)
- add `git-filter-repo` by @garysassano in
[#9550](#9550)
- add umoci
([aqua:opencontainers/umoci](https://github.com/opencontainers/umoci))
by @2xdevv in [#9555](#9555)
- add aqua backend for elixir-ls by @AlternateRT in
[#9557](#9557)
- deny inline backend options by @risu729 in
[#9565](#9565)

### Chore

- **(ci)** fail registry tests without summary by @jdx in
[#9559](#9559)
- **(ci)** use !cancelled() instead of always() for test-ci aggregator
by @jdx in [#9569](#9569)
- **(ci)** use namespace runners for ci jobs by @jdx in
[#9561](#9561)
- **(config)** deprecate shorthands_file setting by @risu729 in
[#9534](#9534)
- **(docs)** remove shrill.en.dev analytics script by @jdx in
[#9539](#9539)
- **(release)** replace bc with awk in release-plz star formatting by
@jdx in
[d7f177f](d7f177f)
- bump hk to 1.44.3 by @jdx in
[#9493](#9493)
- invert CLAUDE.md/AGENTS.md so AGENTS.md is canonical by @jdx in
[#9560](#9560)
- set dev profile debug to 1 by @jdx in
[#9572](#9572)

### New Contributors

- @kuboon made their first contribution in
[#9567](#9567)
- @AlternateRT made their first contribution in
[#9557](#9557)
- @2xdevv made their first contribution in
[#9555](#9555)
- @adam-moss made their first contribution in
[#9541](#9541)
- @jlarmstrongiv made their first contribution in
[#9496](#9496)
- @tjarvstrand made their first contribution in
[#9505](#9505)
jdx pushed a commit that referenced this pull request May 6, 2026
## Summary
- wait for successful e2e test environment cleanup when the test is
running inside Docker
- keep asynchronous cleanup outside Docker so local/non-Docker runs keep
the existing fast path
- remove the need for a second cleanup container that chmods the Docker
bind mounts

## Context
#9570 changed Docker e2e runs to bind-mount host-backed directories for
`/tmp` and `/root` instead of using tmpfs. Inside the container, `mktemp
-d` creates root-owned `0700` per-test directories under the
bind-mounted `/tmp`.

For successful tests, `e2e/run_test` previously started `rm -rf
"$TEST_ISOLATED_DIR"` in the background. When the Docker container exits
before that background cleanup reliably finishes, root-owned `0700` test
directories can be left behind on the host mount. The outer host-side
cleanup then emits permission noise such as `rm: cannot remove`.

Waiting for `rm -rf` only when `MISE_E2E_INSIDE_DOCKER=1` fixes the
source of that host-side cleanup noise without running a second
container or broadly chmodding both mounts.

Failed tests keep the existing behavior: the isolated test environment
is left in place and reported for inspection, and the returned test
status remains unchanged.

## Tests
- `git diff --check`
- `bash -n e2e/run_test`
- `mise x shellcheck -- shellcheck -x e2e/run_test`
- `mise x shfmt -- shfmt -d e2e/run_test`
- `MISE_E2E_DOCKER=1 E2E_WAIT_FOR_GH_RATE_LIMIT=0 mise run test:e2e
e2e/cli/test_version`
  - verified captured output had no `rm: cannot remove` lines
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