Skip to content

[Hackathon] fix(bundle): re-fetching a failure bundle into an existing --out fails when its parent isn't writable or --out is a mount point (EACCES/EXDEV) #640

Closed as not planned

Description

@Satianurag

Verified against (commit SHA or released version)

commit a18d37c (main HEAD)

Discord Username / User ID

sati1325

What does this improvement do?

Fetching a failure bundle a second time into the same --out directory fails whenever the commit needs to rotate the previous artifacts: commitBundle stages old files into an aside directory it creates in dirname(--out) (join(parent, '.<base>.aside.<uuid>'), src/lib/bundle.ts ~line 630). Two real setups break:

  • --out is writable but its parent is not — e.g. --out=/workspace/out under a root-owned /workspace, or a shared top-level dir — so mkdir of the aside fails with EACCES.
  • --out is itself a mount point (Docker/CI volume, bind mount, tmpfs) — so rename() of the old artifacts into the aside crosses devices and fails with EXDEV.

The first fetch succeeds; every later fetch into that dir fails and leaves a .partial marker behind, so consumers treat the bundle as incomplete. The only workaround is deleting --out by hand.

Repro (verified on a18d37c, unit level):

const s = findSample('GET','https://api.testsprite.com/api/cli/v1/tests/test_8f2a4d10/failure');
const ctx = s.body().data;
await writeBundle(ctx, {dir, fetchImpl});   // OK — 12 files
chmodSync(parent(dir), 0o555);
await writeBundle(ctx, {dir, fetchImpl});
// EACCES: mkdir '<parent>/.out.aside.<uuid>'
// dir afterwards: .partial code.py failure.json meta.json result.json steps video.mp4

Mount-point case (tmpfs): second run fails EXDEV: cross-device link not permitted, rename '<dir>/meta.json' -> '<parent>/.mnt.aside.<uuid>/meta.json', same .partial + stale contents.

Expected: any --out the first fetch wrote successfully can be refreshed in place — stage the aside inside --out (same filesystem, known-writable) or fall back to an in-dir aside / copy+unlink on EACCES/EXDEV.

Details / implementation notes

Root cause: commitBundle (src/lib/bundle.ts ~line 630) builds the aside at join(dirname(dir), \..aside.`)andrename()s the managed files into it. That crosses a permission boundary (parent can be read-only while dir itself is writable) and a device boundary (dir` can be a mount point while the parent lives on another filesystem).

Fix: create the aside inside dir (e.g. dir/.aside.<uuid>, excluded from the managed/stale-entry sweep) so every rename stays on one filesystem in a directory known to be writable — the same place the tmp staging already lives. Fallback alternative: keep the current location but retry with an in-dir aside (or copy+unlink) when mkdir/rename returns EACCES/EXDEV. Regression test: write once, chmod 0555 the parent, write again — must succeed and leave a complete bundle with no .partial marker.

A PR with the fix + regression test will follow referencing this issue.

Confirmations

  • I have searched existing issues and this is not a duplicate.
  • I have provided my Discord identity above for reward coordination.

Activity

  1. fyZhang66 commented on Oct 22, 2026

    @fyZhang66
    Contributor

    Thanks @Satianurag. It needs a repeat fetch into the same --out whose parent is read-only or which is itself a mount point, and a fresh or emptied directory works; no one has hit it in practice. Closing as not planned.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    hackathonCLI hackathon submissions

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions