Skip to content

refactor(ci): drop the vestigial /tmp workspace from e2e (both workflows) - #3454

Merged
myasnikovdaniil merged 1 commit into
mainfrom
fix/e2e-guard-workspace-cleanup
Jul 29, 2026
Merged

refactor(ci): drop the vestigial /tmp workspace from e2e (both workflows)#3454
myasnikovdaniil merged 1 commit into
mainfrom
fix/e2e-guard-workspace-cleanup

Conversation

@kvaps

@kvaps Andrei Kvapil (kvaps) commented Jul 27, 2026

Copy link
Copy Markdown
Member

What this PR does

The E2E job's if: always() cleanup ran rm -rf /tmp/$SANDBOX_NAME with no check that SANDBOX_NAME was ever set. Set sandbox ID carries no condition, so it inherits the implicit success() every unconditional step gets: any failure ahead of it — a conflicting pr.patch, a download-artifact miss, Checkout code, the release-path curl — or a cancellation that reaches the job after it has started (the job condition is !cancelled()) skips it, and SANDBOX_NAME is never written to $GITHUB_ENV. The if: always() steps then run anyway and interpolate the empty value; Remove workspace is the destructive one, expanding to rm -rf /tmp/ and failing the cleanup step against root-owned entries in the runner's /tmp. This is not a "re-run skips the setup" bug: a re-run restarts the job at step one and re-runs the unconditional Set sandbox ID — the trigger is incomplete initialization, by any of the routes above.

The /tmp/$SANDBOX_NAME workspace is a vestige of the retired multi-job self-hosted-runner design, where several runs-on: [self-hosted] jobs shared no state and rendezvoused through a sha256(REPO:WORKFLOW:REF) key, with a per-run cp -r of the checkout into /tmp. Since CI moved onto the ephemeral pool (runs-on: oracle-vm-32cpu-128gb-x86-64) it is a single job: there is no cross-job rendezvous and the checkout is never reused across runs, so copying it into /tmp and removing it afterwards is dead weight. (An earlier review justified the change by "wiping the entire /tmp of the self-hosted runner" — that describes the topology we retired; the hazard is real, but not for that reason.)

So this drops the workspace rather than guarding the rm. Both the PR (pull-requests.yaml) and nightly (nightly.yaml) E2E jobs now run in $GITHUB_WORKSPACE: the Prepare workspace and Remove workspace steps are gone, the cd /tmp/$SANDBOX_NAME prefixes are dropped, and the upload paths point at _out/… instead of /tmp/${SANDBOX_NAME}/_out/…. That takes the empty-value expansion out of the cds, the stray mkdir, the upload paths and the destructive rm in one move, across both workflows rather than one step of many. Set sandbox ID stays: the job still reads $SANDBOX_NAME directly (failure diagnostics, the chainsaw docker cp, teardown, and every make SANDBOX_NAME=… argument) — the container needs a name, just not a directory.

It also removes the dead code the single-runner retirement left in packages/core/testing/Makefile: the world-writable mkdir -p /tmp/${SANDBOX_NAME} + chmod 777 in the apply target (they backed a -v /tmp/${SANDBOX_NAME}:/workspace/hosttmp bind mount that was later removed — nothing under images/e2e-sandbox/ references /workspace/hosttmp), and the -e SANDBOX_NAME=${SANDBOX_NAME} passed into the container (nothing under images/e2e-sandbox/ reads it). Every other use of SANDBOX_NAME — the container name and the make arguments — is kept.

Note this cannot make a job with an empty SANDBOX_NAME go green: such a job already failed initialization or was cancelled, so its conclusion is fixed before cleanup runs. It removes a second, destructive failure the old cleanup piled on top, and it does not change whether re-runs work in general — they do.

The diagnosis of the real trigger, the corrected blast radius, and the scope across both workflows plus the Makefile are credited to the maintainer review on this PR.

Evidence

Run 30067779342 attempt 3 exhibits the mechanism: Download Talos image (regular PR) failed on GetSignedArtifactURL with a 404, which skipped Set sandbox ID, and the always() steps then ran on an empty value — Collect chainsaw report created /tmp/_out, the uploads resolved to /tmp//_out/…, and Remove workspace did:

rm: cannot remove '/tmp/systemd-private-…-systemd-timedated.service-1oADZP': Operation not permitted
rm: cannot remove '/tmp/.X11-unix': Operation not permitted
##[error]Process completed with exit code 1.

The step ran unprivileged in /home/ubuntu/_work/…, so all it could hit was Operation not permitted on root-owned entries in the sticky /tmp — it deleted nothing. That run is a later, independent occurrence (it started well after this PR was opened), not this change's motivating case. The 404 that triggered it is a separate, non-deterministic problem — the artifact listing succeeded and only the signed-URL call failed ~190 ms in, download-artifact treats 404 as non-retryable, and attempt 2 of the same run fetched the artifact fine — and it deserves its own issue rather than being folded in here.

Screenshots

Not applicable — CI workflow change only.

Downstream repositories

The change is confined to this repo's E2E CI (.github/workflows/pull-requests.yaml, .github/workflows/nightly.yaml) and its testing Makefile (packages/core/testing/Makefile). No downstream repository consumes it.

  • No downstream repository is affected by this change

Release note

refactor(ci): drop the vestigial /tmp/$SANDBOX_NAME workspace from the E2E PR and nightly jobs — they now run in $GITHUB_WORKSPACE, removing an unguarded `rm -rf /tmp/$SANDBOX_NAME` cleanup that failed the job on cleanup when SANDBOX_NAME was empty

Summary by CodeRabbit

  • Bug Fixes

    • Improved end-to-end test environment setup and sandbox cleanup.
    • Updated test reporting and artifact collection for more reliable results.
    • Ensured OpenAPI and full-suite tests run from the correct prepared environment.
  • Chores

    • Simplified container and workflow configuration for sandbox-based testing.
    • Clarified checkout token handling in nightly workflows.

@github-actions github-actions Bot added area/ci Issues or PRs related to CI workflows, GitHub Actions, automation kind/bug Categorizes issue or PR as related to a bug labels Jul 27, 2026
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 255a359d-106d-44b0-943d-71a54205e6f8

📥 Commits

Reviewing files that changed from the base of the PR and between c34b24e and 78fc3f9.

📒 Files selected for processing (3)
  • .github/workflows/nightly.yaml
  • .github/workflows/pull-requests.yaml
  • packages/core/testing/Makefile
💤 Files with no reviewable changes (1)
  • packages/core/testing/Makefile
🚧 Files skipped from review as they are similar to previous changes (2)
  • .github/workflows/pull-requests.yaml
  • .github/workflows/nightly.yaml

📝 Walkthrough

Walkthrough

The E2E workflows centralize sandbox preparation, remove repeated directory changes, collect artifacts from relative _out paths, and delegate sandbox cleanup to the testing Makefile. The sandbox container no longer receives SANDBOX_NAME.

Changes

E2E sandbox workflow

Layer / File(s) Summary
Centralize sandbox preparation and test execution
.github/workflows/nightly.yaml, .github/workflows/pull-requests.yaml, packages/core/testing/Makefile
Sandbox setup is consolidated into environment preparation, subsequent installation and test commands run from the prepared context, and SANDBOX_NAME is removed from the container environment.
Update report and artifact collection
.github/workflows/nightly.yaml, .github/workflows/pull-requests.yaml
Reports and debug artifacts use relative _out paths with updated missing-file handling and report collection.
Delegate sandbox teardown
.github/workflows/pull-requests.yaml, .github/workflows/nightly.yaml
Cleanup uses the testing Makefile’s sandbox delete target instead of directly removing the workspace.
Update checkout authentication comment
.github/workflows/nightly.yaml
The mirror workflow comment now describes direct registry authentication and checkout token handling.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: androndo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: removing the /tmp workspace from E2E jobs across both workflows.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/e2e-guard-workspace-cleanup

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the size/XS This PR changes 0-9 lines, ignoring generated files label Jul 27, 2026
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

IvanHunters
IvanHunters previously approved these changes Jul 27, 2026

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM. Correct root-cause safety fix.

Without the guard, an empty SANDBOX_NAME (e.g. when the setup step never exported it before this if: always() cleanup) turns rm -rf /tmp/$SANDBOX_NAME into rm -rf /tmp/, wiping the entire /tmp of the self-hosted runner (the e2e job runs on a persistent oracle-vm runner, so this affects other/subsequent runs).

The guard is correct in every branch under the default bash -e -o pipefail shell:

  • name set, rm ok -> exit 0
  • name set, rm fails -> || true swallows it, cleanup step stays green
  • name empty -> [ -n ] short-circuits, rm never runs -> exit 0

Bonus: quoting "/tmp/$SANDBOX_NAME" handles names with special characters.

@kvaps Andrei Kvapil (kvaps) added the do-not-merge/hold Indicates that a PR should not merge because someone has issued /hold label Jul 27, 2026

@myasnikovdaniil myasnikovdaniil left a comment

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.

NOT LGTM — the guard is correct as code, but the description's diagnosis is wrong, its impact claim is wrong, and the scope is one step out of fourteen across two workflows. I'd rather see this land as the cleanup it points at than as a patch on one symptom.

What the workflow actually does

Set sandbox ID (590) carries no condition, so it inherits the implicit success() that every unconditional step gets. Any failure ahead of it skips it — Apply patch on a conflicting pr.patch, either download-artifact, Checkout code, the release-path curl — as does a cancellation that reaches the job after it has started. In all of those cases SANDBOX_NAME is never written to $GITHUB_ENV and stays empty for the remainder of the job.

Eight steps then run anyway, because they carry if: always(), and every one of them interpolates that empty value. Remove workspace is the destructive one: rm -rf /tmp/$SANDBOX_NAME expands to rm -rf /tmp/.

So the hazard is real and worth removing. What the description says causes it is not what causes it.

Corrections to the description

"On GitHub's re-run failed jobs, the earlier steps that populate SANDBOX_NAME (Set sandbox ID / Prepare workspace) are skipped." There is no step-level re-run in GitHub Actions — a new job attempt starts at the first step and re-evaluates every if: from scratch. Set sandbox ID is unconditional and lives in the same job, so a re-run runs it. Steps get skipped here for the ordinary reason: something ahead of them failed. The re-run is not the trigger; incomplete initialization is, by any of the routes above. Worth noting that 792951b9c recently moved the job condition to !cancelled(), which makes the cancellation route reachable.

"so 're-run failed jobs' could never go green for E2E and every failure forced a full re-run." An empty SANDBOX_NAME implies initialization already failed or the job was cancelled, so that job's conclusion is already fixed before cleanup runs. The guard turns two red steps into one red step. It cannot make such a job green, and it does not change whether re-runs work in general — they do.

Blast radius. The approving review justifies the change as "wiping the entire /tmp of the self-hosted runner (the e2e job runs on a persistent oracle-vm runner, so this affects other/subsequent runs)". #3268 moved all CI off the persistent self-hosted runner onto the ephemeral pool, and this job runs there (runs-on: oracle-vm-32cpu-128gb-x86-64, 529). The reasoning describes a topology we retired. Still worth fixing — just not for that reason.

Scope

Eight if: always() steps in this job interpolate SANDBOX_NAME with no check that it was ever set:

  • Collect chainsaw report (709) — cd /tmp/$SANDBOX_NAME + mkdir -p _out, which on an empty value creates /tmp/_out on the runner
  • Collect report (726) and Collect images list (739) — make -C packages/core/testing … invoked from /tmp
  • the three Upload … steps (717, 732, 745) — path: /tmp/${{ env.SANDBOX_NAME }}/_out/…/tmp//_out/…
  • Tear down sandbox (802) — docker rm -f ""container name cannot be empty (harmless, as the description says)
  • Remove workspace (806) — the one this PR fixes

.github/workflows/nightly.yaml has the same shape and is untouched by this PR: Set sandbox ID (273), Prepare workspace (277), six if: always() steps (330–364), and the same unguarded removal at 366. It quotes its paths, which does nothing for an empty value, and it has no images collect/upload pair. Eight plus six is fourteen; this PR changes one.

What I think the actual fix is

SANDBOX_NAME started life as a rendezvous key and only later became a workspace directory name too, and the reason for the rendezvous is gone.

Before 505b693c3 ("[ci] Run e2e tests on shared runners") E2E was five state-sharing runs-on: [self-hosted] jobs — prepare_env, install_cozystack, test_apps, collect_debug_information, cleanup — each carrying its own copy of the same Set sandbox ID step, all relying on shared /tmp and dockerd state on the self-hosted runner. That is what sha256(GITHUB_REPOSITORY:GITHUB_WORKFLOW:GITHUB_REF) buys: jobs that cannot pass state to each other can each re-derive the same value, keyed so concurrent branches don't collide and a re-push replaces its predecessor. The /tmp workspace came in alongside it — b2a697f98 introduced mv cozystack /tmp/$SANDBOX_NAME, and b3380d836 changed the mv to cp -r so the move would stop confusing the checkout action.

Today it is one job on the ephemeral pool. No cross-job rendezvous, no sibling jobs sharing /tmp, and the copy is protecting a checkout directory that is no longer reused across runs.

Two ways to go, and I'd take the first:

  1. Drop the /tmp workspace. Remove Prepare workspace and Remove workspace, run in $GITHUB_WORKSPACE, drop the cd /tmp/$SANDBOX_NAME prefixes, and change the three upload paths from /tmp/${{ env.SANDBOX_NAME }}/_out/… to _out/…. That deletes the rm rather than guarding it, and it takes the empty-value expansion out of the upload paths and the cds at the same time. Note this does not let us drop Set sandbox ID: the job reads $SANDBOX_NAME directly in the failure diagnostics (619–629, 689–699), the chainsaw docker cp (714), teardown (804), and every make SANDBOX_NAME=… argument. The container still needs a name — it just doesn't need a directory.
  2. If you'd rather keep the /tmp copy, gate the block instead of the command: if: always() && env.SANDBOX_NAME != '' on all eight steps here and all six in nightly.yaml. That subsumes the rm guard and covers the stray mkdir and the /tmp//_out upload paths too.

Either way, one detail is worth knowing: SANDBOX_NAME := in packages/core/testing/Makefile is a plain assignment, and a command-line make SANDBOX_NAME= beats it. Passing the variable explicitly does not fall back to the Makefile default when it is empty — it actively blanks it, which is why teardown reaches docker rm -f "" rather than removing a default-named container. The one place a sane default existed, the workflow overrides it with nothing.

While you're in there — other things the single-runner retirement left behind

Small, none urgent, all in packages/core/testing/Makefile:

  • apply still does mkdir -p /tmp/${SANDBOX_NAME} and chmod 777 /tmp/${SANDBOX_NAME} (71–72). Those backed a bind mount added in fa6442998 as -v /tmp/${SANDBOX_NAME}:/workspace/hosttmp, widened to -v /tmp:/workspace/hosttmp in 433bfe7b6, and removed entirely in f891d0bee. No e2e-sandbox code references /workspace/hosttmp any more, but the world-writable directory is still created on every apply.
  • The same target passes -e SANDBOX_NAME=${SANDBOX_NAME} into the container (77), and nothing under packages/core/testing/images/e2e-sandbox/ reads it.

A process note, and the one occurrence I can find

Everything above is read off the workflow, the Makefile and the history, because the PR gives nothing to read it against: the description asserts the mechanism in prose with no run link, no job link, no log excerpt and no issue reference, and the commit message has none either. I think that is why the wrong cause went unchallenged and why the approval reasoned from a runner topology we no longer have. For a change whose whole argument is "this happens on re-run", a link to a job where it happened would have settled it in one click.

I did go looking, and I want to be careful about how I present what I found, because it is not this PR's motivating case and it should not be read as one. Run 30067779342 attempt 3 exhibits the failure, but it started at 2026-07-27T14:34:02Z — about three and a half hours after this PR was opened at 11:00:03Z. Attempts 1 and 2 of that same run both completed Remove workspace successfully. So it is a later, independent occurrence that happens to demonstrate the mechanism; nobody could have been looking at it when the description was written.

What it shows is that the trigger is not the one described. Download Talos image (regular PR) failed on GetSignedArtifactURL with a 404, which skipped Set sandbox ID along with everything else unconditional, and the eight always() steps then ran on an empty value — Collect chainsaw report created /tmp/_out, the uploads resolved to /tmp//_out/…, teardown ran docker rm -f "", and Remove workspace did this:

rm: cannot remove '/tmp/systemd-private-…-systemd-timedated.service-1oADZP': Operation not permitted
rm: cannot remove '/tmp/.X11-unix': Operation not permitted
##[error]Process completed with exit code 1.

Note the shape of that: the failures are all Operation not permitted on root-owned entries in a sticky /tmp, and the job's workdir is /home/ubuntu/_work/…, so the step ran unprivileged and could not reach anything it did not own. Also worth knowing for anyone chasing the download 404 separately: the artifact listing succeeded and only the signed-URL call failed, ~190 ms in, because download-artifact treats 404 as non-retryable. It is not deterministic — attempt 2 of the same run fetched the same artifact fine — but one 404 establishes nondeterminism, not a root cause. That deserves its own issue rather than being folded in here.

Requesting changes on framing and scope, not on the line itself. Happy to take the cleanup myself if you'd rather not carry it.

Comment thread .github/workflows/pull-requests.yaml Outdated
- name: Remove workspace
if: always()
run: rm -rf /tmp/$SANDBOX_NAME
run: '[ -n "$SANDBOX_NAME" ] && rm -rf "/tmp/$SANDBOX_NAME" || true'

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.

This guards one of the eight if: always() steps that interpolate SANDBOX_NAME with no check that it was ever set. The six collect/upload steps above it (709, 717, 726, 732, 739, 745) have the same problem — Collect chainsaw report does cd /tmp/$SANDBOX_NAME + mkdir -p _out, which creates /tmp/_out on the runner when the value is empty, and the three uploads resolve to path: /tmp//_out/…. Gating the block is simpler than guarding each command:

      - name: Remove workspace
        if: always() && env.SANDBOX_NAME != ''
        run: rm -rf "/tmp/$SANDBOX_NAME"

Applied to all eight, that covers the rm, the stray mkdir and the upload paths in one go, and needs no shell guard. actionlint accepts the expression, and a value written to $GITHUB_ENV in an earlier step is readable in a later step's if:.

Two notes on the guard as written, if it stays. || true also swallows a genuine rm failure on a non-empty name, where the old behaviour surfaced it; rm -rf "/tmp/${SANDBOX_NAME:?}" refuses to run on an empty or unset value while still failing loudly on a real error. And the same unguarded line exists at .github/workflows/nightly.yaml:366, so this fix as scoped leaves the nightly E2E job able to do exactly the same thing.

@kvaps
Andrei Kvapil (kvaps) force-pushed the fix/e2e-guard-workspace-cleanup branch from 432bcc0 to c34b24e Compare July 27, 2026 16:27
@github-actions github-actions Bot added size/M This PR changes 30-99 lines, ignoring generated files and removed size/XS This PR changes 0-9 lines, ignoring generated files labels Jul 27, 2026
@kvaps

Copy link
Copy Markdown
Member Author

You're right on all three counts, thanks for the thorough read.

  • Cause: the "re-run skips setup" framing was wrong — a job re-run re-runs the unconditional Set sandbox ID; the real trigger is an upstream failure (or a cancel under !cancelled()) skipping it, leaving SANDBOX_NAME empty for the always() steps.
  • Impact: agreed — an empty SANDBOX_NAME means the job's conclusion is already red, so the guard only turns two red steps into one; it doesn't make anything green or change whether re-runs work.
  • Blast radius: the persistent-/tmp reasoning predates ci: move remaining workflows off the persistent self-hosted runner #3268 moving this to the ephemeral pool. And one-of-fourteen is one-of-fourteen.

Going with your Option 1: dropping the /tmp workspace entirely — removing Prepare/Remove workspace, running in $GITHUB_WORKSPACE, dropping the cd /tmp/$SANDBOX_NAME prefixes and fixing the upload paths — across both pull-requests.yaml and nightly.yaml, plus the dead mkdir/chmod 777 and the unused -e SANDBOX_NAME in packages/core/testing/Makefile. Set sandbox ID stays (container name / make args / diagnostics). We'll carry it rather than hand it back.

Rewriting the description with the correct cause and the run 30067779342 attempt 3 evidence (and noting the download-artifact 404 that triggered it is a separate non-deterministic issue for its own ticket). Will re-request your review once it's force-pushed. The missing run link is on us.

@kvaps Andrei Kvapil (kvaps) changed the title fix(ci): guard e2e workspace cleanup against empty SANDBOX_NAME refactor(ci): drop the vestigial /tmp workspace from e2e (both workflows) Jul 27, 2026
…ows)

The E2E job once ran as several state-sharing self-hosted-runner jobs
that rendezvoused through a sha256(REPO:WORKFLOW:REF) key and a per-run
/tmp/$SANDBOX_NAME copy of the checkout. Since the move to the ephemeral
pool it is a single job: there is no cross-job rendezvous and the
checkout is never reused, so copying it into /tmp and removing it
afterwards is dead weight.

Run every step in $GITHUB_WORKSPACE instead: drop the Prepare/Remove
workspace steps and the `cd /tmp/$SANDBOX_NAME` prefixes, and point the
upload paths at _out/. This deletes the unguarded `rm -rf
/tmp/$SANDBOX_NAME` rather than guarding it: when an upstream step fails
or the job is cancelled, the unconditional `Set sandbox ID` is skipped,
SANDBOX_NAME stays empty, and the `if: always()` steps interpolate it —
the removal expanding to `rm -rf /tmp/` and failing the cleanup step.
`Set sandbox ID` stays, since the container still needs a name.

Apply the same treatment to nightly.yaml, and drop the Makefile dead
code the single-runner retirement left behind: the world-writable
mkdir/chmod of /tmp/$SANDBOX_NAME (backed a bind mount removed long ago)
and the `-e SANDBOX_NAME` container env (nothing in the e2e-sandbox
image reads it).

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Andrei Kvapil <andrei.kvapil@aenix.io>
@kvaps
Andrei Kvapil (kvaps) force-pushed the fix/e2e-guard-workspace-cleanup branch from c34b24e to 78fc3f9 Compare July 28, 2026 15:49
@github-actions github-actions Bot added the kind/cleanup Categorizes issue or PR as related to cleanup of code, process, or technical debt label Jul 28, 2026

@IvanHunters IvanHunters left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM. The rework matches the recommended approach (drop the /tmp workspace entirely rather than guard it), and both workflows (pull-requests.yaml and nightly.yaml) are covered. The corrected description now accurately attributes the empty SANDBOX_NAME to incomplete initialization, and the Makefile dead code (world-writable mkdir + chmod 777, -e SANDBOX_NAME) is removed. Traced path/CWD resolution end-to-end (make targets, docker cp, upload-artifact paths, ROOT_DIR) — all consistent; repo-wide sweep finds no remaining /tmp/$SANDBOX_NAME references.

Two housekeeping notes: the do-not-merge/hold label is still set, and the now-outdated inline thread on pull-requests.yaml should be marked resolved.

@myasnikovdaniil myasnikovdaniil left a comment

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.

LGTM — the rework implements Option 1 exactly as scoped, across both workflows plus the Makefile, and every point I raised is closed with no new failure mode introduced.

Going through my earlier review point by point.

Cause, impact and blast radius — closed. The rewritten description states the trigger as incomplete initialization rather than a step-level re-run, states plainly that the change cannot turn a red job green, and retracts the persistent-/tmp blast-radius reasoning as describing a topology we retired. It also now carries the run link and log excerpt the earlier version lacked, and correctly frames run 30067779342 attempt 3 as a later independent occurrence rather than the motivating case.

The /tmp workspace is gone, not guarded — closed. Prepare workspace and Remove workspace are removed from both jobs; the job now runs in $GITHUB_WORKSPACE. .github/workflows/pull-requests.yaml:589 and .github/workflows/nightly.yaml:273 keep Set sandbox ID, which is right — the container still needs a name for the diagnostics docker execs, the chainsaw docker cp, teardown and every make SANDBOX_NAME=… argument.

Scope — closed, both workflows. The always() step count drops from 8 to 7 in pull-requests.yaml (700, 707, 716, 721, 728, 733, 790) and from 6 to 5 in nightly.yaml (323, 330, 338, 343, 351) — exactly the one destructive step removed from each. The remaining steps still interpolate a possibly-empty SANDBOX_NAME, and that is fine under Option 1 rather than a leftover: with the cds gone every one of them is now inert on an empty value. Collect chainsaw report (pull-requests.yaml:699, nightly.yaml:322) does mkdir -p _out in the workspace instead of /tmp/_out on the runner, and its docker cp is || true; the collect steps are || true; the uploads carry if-no-files-found: ignore/warn (pull-requests.yaml:725 and :737 take the warn default); teardown reaches docker rm -f "" quoted and || true. Nothing left in either workflow interpolates a possibly-empty variable into a destructive command.

Upload paths — closed, and the artifact layout is unchanged. The three PR paths (pull-requests.yaml:711, :725, :737) and the two nightly paths (nightly.yaml:334, :347) are now relative. Neither workflow sets a defaults.run.working-directory, so upload-artifact resolves them against $GITHUB_WORKSPACE, which is where mkdir -p _out and the Makefile's ../../../_out both land. Each path names a single literal file, so its parent directory is the artifact root and the artifact still contains the bare chainsaw-report.xml / cozyreport.tgz / images.txt — same as under the old absolute form.

Path and CWD resolution — verified end to end. ROOT_DIR at packages/core/testing/Makefile:7 derives the repo root from the Makefile's own location, not from the caller's cwd, so dropping the cd prefixes leaves docker cp "${ROOT_DIR}/." …:/workspace (:77) copying the same tree it copied before — the previous /tmp/$SANDBOX_NAME was itself just a cp -r of that workspace. Root prepare-env (Makefile:180) enters packages/core/testing, from which copy-nocloud-image (:34), collect-report (:56-57) and collect-images (:61-62) resolve ../../../_out to the repo-root _out in both the old and the new layout.

Makefile dead code — closed. apply (packages/core/testing/Makefile:70) goes straight from delete to docker run: the world-writable mkdir -p /tmp/${SANDBOX_NAME} + chmod 777 and the -e SANDBOX_NAME=${SANDBOX_NAME} are both gone. A tree-wide sweep confirms nothing reads SANDBOX_NAME inside the container — every remaining occurrence is a host-side workflow line or a make argument — and no /tmp/$SANDBOX_NAME reference survives anywhere. The only remaining hosttmp hits are in the vendored HAMi device-plugin chart, unrelated to the retired bind mount.

In-place workspace mutation does not change behaviour. git apply _out/assets/pr.patch (pull-requests.yaml:573) leaves uncommitted working-tree changes, and Select E2E tests (:637) diffs the commit range origin/${BASE_REF}...HEAD, which those changes cannot affect — and the old /tmp copy carried a byte-identical .git, so the selection is the same either way. Nightly's Stage GHCR install tree (nightly.yaml:251) deliberately overlays packages/ before the sandbox is created, and every later consumer wants that staged tree; no step downstream depends on a pristine checkout.

The nightly comment rewrite is accurate. The old wording justified persist-credentials: false by the /tmp copy, which was never true for the mirror job in the first place. The replacement at nightly.yaml:123-125 holds: registry auth is established before checkout (:62), and the three steps after it only pull, mirror and push registry artifacts (:130, :140, :145) with no git operation — hack/nightly-mirror.sh contains none.

And it is empirically green on the reworked commit. Run 30375236044 on 78fc3f917 completed the E2E job with every step in the reworked path successful — Prepare environment, Install Cozystack, Select E2E tests, both test steps, all three collect/upload pairs and Tear down sandbox — and all three artifacts uploaded non-empty (chainsaw-report 1165 B, cozyreport 384824 B, image-list 18128 B). Since the change touches packages/core/ and the E2E workflow, TIA escalated to the full suite, so the relative upload paths and the no-cd make invocations are exercised rather than argued.

Non-blocking follow-ups

  1. The download-artifact 404 on GetSignedArtifactURL that the description cites as the observed trigger still has no issue of its own — a search turns up nothing filed. Worth opening so the non-deterministic 404 is tracked separately, as the description says it should be.
  2. nightly.yaml's E2E job is not exercised by PR CI, so its half of this change is verified by inspection only; the first live run is the next nightly. The two jobs are now structurally identical in this area, so the residual risk is low, but it is worth a glance at the next nightly.
  3. Optional, pre-existing: pull-requests.yaml:725 and :737 rely on upload-artifact's default if-no-files-found: warn while nightly states it explicitly at :348. Harmless, but stating it in both places would make the intent local.
  4. Optional, pre-existing and out of scope here: SANDBOX_NAME := at packages/core/testing/Makefile:5 is still beaten by a command-line make SANDBOX_NAME=, so passing the variable explicitly blanks it rather than falling back to the default. Nothing depends on that default today; noting it only so it is not rediscovered as a bug.

@myasnikovdaniil
myasnikovdaniil merged commit 5d42047 into main Jul 29, 2026
204 of 209 checks passed
@myasnikovdaniil
myasnikovdaniil deleted the fix/e2e-guard-workspace-cleanup branch July 29, 2026 10:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/ci Issues or PRs related to CI workflows, GitHub Actions, automation do-not-merge/hold Indicates that a PR should not merge because someone has issued /hold kind/bug Categorizes issue or PR as related to a bug kind/cleanup Categorizes issue or PR as related to cleanup of code, process, or technical debt size/M This PR changes 30-99 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants