mirror of
https://github.com/max-sixty/worktrunk.git
synced 2026-09-14 20:00:38 +08:00
ci(tend): reach the pre-merge gate's tools from inside the sandbox (#3897)
The agent tend runs as, `tend-sandbox`, could not reach `cargo-insta`, `cargo-nextest`, or `pre-commit` — all three of the tools `cargo run -- hook pre-merge --yes` invokes. That command is what `CLAUDE.md` and `.claude/skills/running-tend/SKILL.md` tell the bot to verify its work with, and in the sandbox it died at `✗ pre-merge command failed: pre-commit: exit status: 127`. Tend derives the sandbox's PATH from the runner's, rewriting a leading `/home/runner` to `/home/tend-sandbox` and keeping an entry only where the rewritten directory exists. `useradd -m` seeds the sandbox home from `/etc/skel`, so a toolchain baked into the runner image has a sibling there and survives; anything `tend-setup` installs at runtime into the runner's home has none and is dropped, with nothing logged. `baptiste0928/cargo-install` forces `--root $HOME/.cargo-install/<crate>` and `uv tool install` writes to the runner's `~/.local/bin`, so all three went the second way. `uv` itself stayed reachable only because `astral-sh/setup-uv` puts it under `/opt/hostedtoolcache`, a system path the rewrite leaves alone. `sandbox_setup:` runs as the sandbox user once its PATH is built, with `~/.local/bin` already first on it. `pre-commit` installs into the sandbox's own home so uv's shim shebang resolves there; the cargo binaries are copied from where `tend-setup` already built them, at the versions pinned there. The closing probe is the assertion — `sandbox-setup.sh` runs the block under `bash -eo pipefail`, so a tool that goes missing again fails the job and files a `tend-outage` issue naming it, rather than degrading quietly. A fourth tool needed the same treatment by a different route. The gate's `insta` step runs `--all-features`, which turns on `shell-integration-tests`; that needs `nu` as much as zsh and fish, and two tests `.expect()` it outright. `tend-setup` apt-installed only zsh and fish, so the probe would have cleared the agent to run a gate that then died on a missing shell. `hustcer/setup-nu` installs under `/opt/hostedtoolcache`, a system path the derivation carries across verbatim, so `nu` needs no `sandbox_setup:` line — it just needed installing. Pinned at 0.115.0, level with `test-setup` and `dev/codex.sh`; `running-tend`'s weekly pin-bump recipe enumerates the `setup-nu` call sites, so it gains the fifth. The remaining `shell-integration-tests` prerequisites — bash, zsh, fish, pwsh, jq — all resolve from `/usr/bin`, which the derivation keeps, so they were never affected. The probe deliberately covers only the four tools `tend-setup` provisions for the agent rather than everything the gate touches. The generated `tend-*.yaml` files are `uvx tend@latest init` output (still 0.1.18); the only change in each is the `sandbox_setup:` block. #3891 landed mid-review and appended its Nix steps at the same end-of-file anchor as the nushell step, so this branch carries a merge resolving that — keeping both, nushell next to the shells it belongs with. Its Nix comment restated the "system PATH entries carry into the sandbox verbatim" rule that this PR centralizes in `.github/CLAUDE.md`, which would have left the rule written three times in one file, so it now points there and keeps only what's specific to Nix. Every #3891 step is unchanged. <details><summary>Evidence, and what it cost</summary> Sandbox PATH, captured from the agent's own launch env in tend-weekly run 32630901712: ``` /home/tend-sandbox/.local/bin:/opt/hostedtoolcache/uv/0.12.5/x86_64:/opt/pipx_bin:/home/tend-sandbox/.cargo/bin:/usr/local/.ghcup/bin:/home/tend-sandbox/.dotnet/tools:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games ``` `error: no such command: insta` appears in runs 31332910927, 31465475753 and 32436240803; `pre-commit: command not found` in 32662391023. Across 3,057 captured Bash calls the bot never once invoked `cargo nextest`, and substituted `cargo test` with `INSTA_UPDATE=always` or `INSTA_FORCE_UPDATE=1` fifteen times — which writes snapshots rather than checking them, and never runs `--check` or `--unreferenced reject`. Since 2026-06-18, when #3122 moved the repo to tend 0.1.6 and the agent first ran sandboxed, 16 of 197 bot PRs pushed a first commit with the `lint` job red. Over the same window 4 of 350 of the maintainer's own PRs did. #3845 is the shape: a closure `cargo fmt` wanted on one line, caught by CI rather than locally. Verified by simulating the block under `bash -eo pipefail` against a fake runner home, in both directions: all three tools land on PATH and it exits 0; remove one and it prints the `::error::` naming it and exits 1. Two things changed after that test — `install -D` is GNU-only, so the parent directory is created explicitly, and `command -v a b c` exits 0 when any one resolves, so the assertion is a loop rather than a single call. </details> > _This was written by Claude Code on behalf of max-sixty_ --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -294,7 +294,7 @@ Pinned third-party versions in CI are invisible to Dependabot — it follows `Ca
|
||||
For each weekly run, check upstream and bump:
|
||||
|
||||
- **`baptiste0928/cargo-install@v3` blocks** in `.github/workflows/{affected,ci,coverage,nightly}.yaml` and `.github/actions/{test,tend}-setup/action.yaml` — every `version: "=X.Y.Z"` against `cargo info <crate>`. Today: `cargo-affected`, `cargo-insta`, `cargo-nextest`, `cargo-llvm-cov`, `cargo-msrv`, `cargo-udeps`, `lychee`, `worktrunk`. `cargo-affected` is pinned twice in `affected.yaml`; move both together. Verify each crate's `rust-version` against the pinned toolchain and note compatibility in the PR body (see PR #1657 for the format).
|
||||
- **`hustcer/setup-nu@v3`** `version:` input — latest from `gh api repos/nushell/nushell/releases/latest --jq '.tag_name'`. Four call sites: `coverage.yaml` (`code-coverage`), `nightly.yaml` (`feature-powerset`), `benchmarks.yaml` (`benchmarks`), and `actions/test-setup/action.yaml`.
|
||||
- **`hustcer/setup-nu@v3`** `version:` input — latest from `gh api repos/nushell/nushell/releases/latest --jq '.tag_name'`. Five call sites: `coverage.yaml` (`code-coverage`), `nightly.yaml` (`feature-powerset`), `benchmarks.yaml` (`benchmarks`), and `.github/actions/{test,tend}-setup/action.yaml` — `tend-setup`'s copy is what puts `nu` in the agent's sandbox, so it moves with the others.
|
||||
- **Codex Cloud tools** — `dev/codex.sh` pins pre-commit, cargo-insta, cargo-nextest, Nushell, and PowerShell; `setup-web` pins Nushell and PowerShell. Keep cargo-insta, cargo-nextest, and Nushell level with `.github/actions/test-setup/action.yaml`, which pins the same three — the gate runs `--all-features`, so Nushell's version moves PTY snapshots. Nothing under `.github/` pins PowerShell (CI runs whatever the runner image ships), so bump that one on its own.
|
||||
- **Docs site packages** in `docs/package.json` are covered by Dependabot's `/docs` npm entry. Do not duplicate those bumps in this manual pin pass. The one thing that pass does own is the `ignore` entry for `typescript` majors in `.github/dependabot.yaml`: it suppresses the proposal Dependabot would otherwise make, so nothing else can surface it. Check `npm view @astrojs/check peerDependencies.typescript` against the current `typescript` major and delete the entry once the range covers it — the rule is version-agnostic, so left in place it blocks a major `@astrojs/check` fully supports just as silently as the TypeScript 7 it was added for (#3877).
|
||||
- **Runner images** — `ubuntu-24.04`, `macos-15`, `windows-2022`. Keep `windows-2022` pinned (actions/runner-images#12677 — windows-2025 lacks the D: drive).
|
||||
|
||||
@@ -1,6 +1,20 @@
|
||||
bot_name: worktrunk-bot
|
||||
setup:
|
||||
- uses: ./.github/actions/tend-setup
|
||||
# `setup:` runs as `runner`, so what it installs into the runner's home never
|
||||
# reaches the agent (see "Sandbox toolchain" in .github/CLAUDE.md). These run as
|
||||
# the sandbox user once its PATH is built, with ~/.local/bin already first on it:
|
||||
# pre-commit installs into the sandbox's own home so uv's shim shebang resolves
|
||||
# there, and the cargo binaries are copied from where `tend-setup` already built
|
||||
# them, at the versions pinned there. The probe is the assertion — sandbox-setup
|
||||
# runs the block under `bash -eo pipefail`, so a tool that goes missing again
|
||||
# fails the job and files a tend-outage issue naming it.
|
||||
sandbox_setup:
|
||||
- mkdir -p ~/.local/bin
|
||||
- install -m755 /home/runner/.cargo-install/cargo-insta/bin/cargo-insta ~/.local/bin/
|
||||
- install -m755 /home/runner/.cargo-install/cargo-nextest/bin/cargo-nextest ~/.local/bin/
|
||||
- uv tool install --quiet pre-commit
|
||||
- 'for t in cargo-insta cargo-nextest pre-commit nu; do command -v "$t" >/dev/null || { echo "::error::tend sandbox is missing $t; wt hook pre-merge cannot run"; exit 1; }; done'
|
||||
secrets:
|
||||
allowed:
|
||||
- CODECOV_TOKEN
|
||||
|
||||
@@ -79,6 +79,19 @@ crates.io publishing holds no stored token — it uses Trusted Publishing.
|
||||
crates.io mints a short-lived one only for an OIDC claim from `release.yaml`
|
||||
running in that same `release` environment.
|
||||
|
||||
## Sandbox toolchain
|
||||
|
||||
The agent runs as `tend-sandbox`, whose PATH tend derives from the runner's by
|
||||
rewriting a leading `/home/runner` to `/home/tend-sandbox` and keeping an entry
|
||||
only where that directory exists. `useradd -m` seeds the sandbox home from
|
||||
`/etc/skel`, so an image-baked toolchain has a sibling there and survives;
|
||||
anything `tend-setup` installs at runtime into the runner's home has none and is
|
||||
dropped, unlogged. So a tool the agent needs has to land in a system location
|
||||
(`/opt/hostedtoolcache/...`, `/usr/local/bin`), which carries across verbatim —
|
||||
the route `nu` takes — or be copied in by `.config/tend.yaml`'s
|
||||
`sandbox_setup:`, which is what the pre-merge gate's `cargo-insta`,
|
||||
`cargo-nextest` and `pre-commit` take. Its closing probe asserts all four.
|
||||
|
||||
## Build environment
|
||||
|
||||
`Swatinem/rust-cache` hashes `CARGO*` and `RUST*` env vars into the cache key.
|
||||
|
||||
@@ -6,7 +6,7 @@ inputs:
|
||||
description: Install cargo-insta and cargo-nextest
|
||||
default: "true"
|
||||
install-shells:
|
||||
description: Install zsh and fish for shell integration tests
|
||||
description: Install zsh, fish, and nushell for shell integration tests
|
||||
default: "true"
|
||||
|
||||
runs:
|
||||
@@ -23,6 +23,11 @@ runs:
|
||||
echo "CARGO_INCREMENTAL=0" >> "$GITHUB_ENV"
|
||||
echo "RUSTFLAGS=-C debuginfo=0" >> "$GITHUB_ENV"
|
||||
|
||||
# `.config/tend.yaml`'s `sandbox_setup:` copies both binaries out of
|
||||
# `$HOME/.cargo-install/<crate>/bin` — the root this action hardcodes — so
|
||||
# the sandboxed agent can reach them (it can't see the runner's home on its
|
||||
# PATH). Swapping this action for one with a different layout, or dropping
|
||||
# these steps, breaks every tend workflow at that copy; update both sides.
|
||||
- name: Install cargo-insta
|
||||
if: inputs.install-cargo-tools == 'true'
|
||||
uses: baptiste0928/cargo-install@v3
|
||||
@@ -77,12 +82,24 @@ runs:
|
||||
sudo timeout -k 30 300 apt-get update
|
||||
sudo timeout -k 30 300 apt-get install -y zsh fish
|
||||
|
||||
# The gate's `insta` step runs `--all-features`, which turns on
|
||||
# `shell-integration-tests` — that needs `nu` as much as zsh and fish, and
|
||||
# two tests `.expect()` it outright. `setup-nu` installs into a system
|
||||
# location, so the agent inherits it (see "Sandbox toolchain" in
|
||||
# .github/CLAUDE.md) with no `sandbox_setup:` line the way the runner-home
|
||||
# installs above need. Level with `test-setup`'s pin.
|
||||
- name: Install nushell
|
||||
if: inputs.install-shells == 'true'
|
||||
uses: hustcer/setup-nu@v3
|
||||
with:
|
||||
version: '0.115.0'
|
||||
|
||||
# Nix, for the weekly toolchain bump's `flake.lock` refresh. The sandboxed
|
||||
# agent can't install it: that user has no sudo, so it can't create /nix.
|
||||
# Installed here as `runner`, /nix/var/nix/profiles/default/bin lands on
|
||||
# $GITHUB_PATH, and tend carries system PATH entries into the sandbox
|
||||
# verbatim (only runner-home entries are rewritten), so the agent gets
|
||||
# `nix`. ~4 s, so it isn't worth gating to the one workflow that needs it.
|
||||
# $GITHUB_PATH — a system path, so the agent inherits `nix` on the same
|
||||
# rule as `nu` above. ~4 s, so it isn't worth gating to the one workflow
|
||||
# that needs it.
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v31
|
||||
with:
|
||||
|
||||
@@ -41,6 +41,12 @@ jobs:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
bot_name: worktrunk-bot
|
||||
model: opus
|
||||
sandbox_setup: |
|
||||
mkdir -p ~/.local/bin
|
||||
install -m755 /home/runner/.cargo-install/cargo-insta/bin/cargo-insta ~/.local/bin/
|
||||
install -m755 /home/runner/.cargo-install/cargo-nextest/bin/cargo-nextest ~/.local/bin/
|
||||
uv tool install --quiet pre-commit
|
||||
for t in cargo-insta cargo-nextest pre-commit nu; do command -v "$t" >/dev/null || { echo "::error::tend sandbox is missing $t; wt hook pre-merge cannot run"; exit 1; }; done
|
||||
prompt: |
|
||||
/tend-ci-runner:ci-fix ${{ github.event.workflow_run.id }}
|
||||
- Run URL: ${{ github.event.workflow_run.html_url }}
|
||||
|
||||
@@ -427,6 +427,12 @@ jobs:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
bot_name: worktrunk-bot
|
||||
model: opus
|
||||
sandbox_setup: |
|
||||
mkdir -p ~/.local/bin
|
||||
install -m755 /home/runner/.cargo-install/cargo-insta/bin/cargo-insta ~/.local/bin/
|
||||
install -m755 /home/runner/.cargo-install/cargo-nextest/bin/cargo-nextest ~/.local/bin/
|
||||
uv tool install --quiet pre-commit
|
||||
for t in cargo-insta cargo-nextest pre-commit nu; do command -v "$t" >/dev/null || { echo "::error::tend sandbox is missing $t; wt hook pre-merge cannot run"; exit 1; }; done
|
||||
prompt: >-
|
||||
${{ steps.delay.outputs.seconds
|
||||
&& format('This job started {0}s after the triggering event (over ~40s means it was queued). ',
|
||||
|
||||
@@ -41,5 +41,11 @@ jobs:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
bot_name: worktrunk-bot
|
||||
model: opus
|
||||
sandbox_setup: |
|
||||
mkdir -p ~/.local/bin
|
||||
install -m755 /home/runner/.cargo-install/cargo-insta/bin/cargo-insta ~/.local/bin/
|
||||
install -m755 /home/runner/.cargo-install/cargo-nextest/bin/cargo-nextest ~/.local/bin/
|
||||
uv tool install --quiet pre-commit
|
||||
for t in cargo-insta cargo-nextest pre-commit nu; do command -v "$t" >/dev/null || { echo "::error::tend sandbox is missing $t; wt hook pre-merge cannot run"; exit 1; }; done
|
||||
prompt: |
|
||||
/tend-ci-runner:nightly
|
||||
|
||||
@@ -156,5 +156,11 @@ jobs:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
bot_name: worktrunk-bot
|
||||
model: opus
|
||||
sandbox_setup: |
|
||||
mkdir -p ~/.local/bin
|
||||
install -m755 /home/runner/.cargo-install/cargo-insta/bin/cargo-insta ~/.local/bin/
|
||||
install -m755 /home/runner/.cargo-install/cargo-nextest/bin/cargo-nextest ~/.local/bin/
|
||||
uv tool install --quiet pre-commit
|
||||
for t in cargo-insta cargo-nextest pre-commit nu; do command -v "$t" >/dev/null || { echo "::error::tend sandbox is missing $t; wt hook pre-merge cannot run"; exit 1; }; done
|
||||
prompt: |
|
||||
/tend-ci-runner:notifications
|
||||
|
||||
@@ -41,5 +41,11 @@ jobs:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
bot_name: worktrunk-bot
|
||||
model: opus
|
||||
sandbox_setup: |
|
||||
mkdir -p ~/.local/bin
|
||||
install -m755 /home/runner/.cargo-install/cargo-insta/bin/cargo-insta ~/.local/bin/
|
||||
install -m755 /home/runner/.cargo-install/cargo-nextest/bin/cargo-nextest ~/.local/bin/
|
||||
uv tool install --quiet pre-commit
|
||||
for t in cargo-insta cargo-nextest pre-commit nu; do command -v "$t" >/dev/null || { echo "::error::tend sandbox is missing $t; wt hook pre-merge cannot run"; exit 1; }; done
|
||||
prompt: |
|
||||
/tend-ci-runner:review-runs
|
||||
|
||||
@@ -156,6 +156,12 @@ jobs:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
bot_name: worktrunk-bot
|
||||
model: opus
|
||||
sandbox_setup: |
|
||||
mkdir -p ~/.local/bin
|
||||
install -m755 /home/runner/.cargo-install/cargo-insta/bin/cargo-insta ~/.local/bin/
|
||||
install -m755 /home/runner/.cargo-install/cargo-nextest/bin/cargo-nextest ~/.local/bin/
|
||||
uv tool install --quiet pre-commit
|
||||
for t in cargo-insta cargo-nextest pre-commit nu; do command -v "$t" >/dev/null || { echo "::error::tend sandbox is missing $t; wt hook pre-merge cannot run"; exit 1; }; done
|
||||
prompt: >-
|
||||
${{ format('/tend-ci-runner:review {0}', github.event.pull_request.number) }}
|
||||
|
||||
|
||||
@@ -53,6 +53,12 @@ jobs:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
bot_name: worktrunk-bot
|
||||
model: opus
|
||||
sandbox_setup: |
|
||||
mkdir -p ~/.local/bin
|
||||
install -m755 /home/runner/.cargo-install/cargo-insta/bin/cargo-insta ~/.local/bin/
|
||||
install -m755 /home/runner/.cargo-install/cargo-nextest/bin/cargo-nextest ~/.local/bin/
|
||||
uv tool install --quiet pre-commit
|
||||
for t in cargo-insta cargo-nextest pre-commit nu; do command -v "$t" >/dev/null || { echo "::error::tend sandbox is missing $t; wt hook pre-merge cannot run"; exit 1; }; done
|
||||
prompt: |
|
||||
/tend-ci-runner:triage ${{ github.event.issue.number }}
|
||||
|
||||
|
||||
@@ -41,5 +41,11 @@ jobs:
|
||||
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
|
||||
bot_name: worktrunk-bot
|
||||
model: opus
|
||||
sandbox_setup: |
|
||||
mkdir -p ~/.local/bin
|
||||
install -m755 /home/runner/.cargo-install/cargo-insta/bin/cargo-insta ~/.local/bin/
|
||||
install -m755 /home/runner/.cargo-install/cargo-nextest/bin/cargo-nextest ~/.local/bin/
|
||||
uv tool install --quiet pre-commit
|
||||
for t in cargo-insta cargo-nextest pre-commit nu; do command -v "$t" >/dev/null || { echo "::error::tend sandbox is missing $t; wt hook pre-merge cannot run"; exit 1; }; done
|
||||
prompt: |
|
||||
/tend-ci-runner:weekly
|
||||
|
||||
Reference in New Issue
Block a user