refactor(codex-cloud): replace the Taskfile and mise with a shell script - #3839
Conversation
The saved setup and maintenance commands verified a SHA-256 digest of scripts/codex-cloud/Taskfile.yaml before running it as root, and pinned Task through `mise x task@<version>`. Every Taskfile change therefore meant editing two Codex Cloud settings fields, which also invalidated the environment cache. The launchers are now `mise x task -- task -t scripts/codex-cloud/Taskfile.yaml <task>`, and they stay fixed across Taskfile edits. Setup installs Task globally and symlinks it into /root/.local/bin, so the Cargo wrapper's PATH no longer names a version-specific mise install directory. This gives up the property that a task branch cannot change the code setup runs as root, along with the `MISE_NO_CONFIG=1` guard against a branch-supplied mise config. The environment carries no variables or secrets and is recreated per task. The launcher-sync test drops the digest derivation and instead checks that the Taskfile still defines the tasks the README launches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
worktrunk-bot
left a comment
There was a problem hiding this comment.
Two references to the checksum gate outlive it, and the diff drops a retry setting that reads as incidental rather than intended.
CLAUDE.md still points at a checksum-gated setup. Its Quick Start line reads Codex Cloud: use the checksum-gated setup in scripts/codex-cloud/README.md — after this PR there is no checksum to gate on. CLAUDE.md → Documentation says "Behavior changes require doc updates", and draft #3838 deletes this exact line, which is what makes the coupling visible.
.claude/skills/running-tend/SKILL.md now instructs a future weekly run to do something impossible. Under Weekly Maintenance: CI Pin Bumps, the Codex Cloud bullet opens with "pins Task through mise" and ends "…then copy the Taskfile's new digest into both README launchers". Both are false after this change, so the next tend-weekly run either invents a digest step or burns it, and keeps hunting for a Task pin that no longer exists. Dropping the digest clause and either removing Task from that bullet or noting it tracks latest would settle it.
Both lines are outside the diff — happy to push a commit with the two edits if you'd rather they land on this branch than in whichever of #3835–#3838 you keep.
Sibling drafts. #3835, #3836, #3837, and #3838 are all open against the same three files (#3838 additionally covers CLAUDE.md, tests/CLAUDE.md, and the root Taskfile.yaml). Assuming this is the one you're landing, closing the four keeps the next reviewer from diffing them against each other.
Checked and clean: no {{.TASK_VERSION}} references survive the var's removal; sha2 and Regex both still have other users in the workspace, so neither the dev-dependency nor the import goes unused; and test -x /root/.local/bin/task catches a dangling symlink as well as a missing one. The MISE_NO_CONFIG and Task-pin tradeoffs are argued in the PR body, so I'm not re-litigating them.
…rences `MISE_HTTP_RETRIES: 6` is the retry budget for the `mise use --global task` fetch, which is now the one download in setup without a retry cushion, so it stays on the task. Keeping it off the launcher preserves the fixed launcher. The `CLAUDE.md` Quick Start line and the `running-tend` weekly bullet both still described a checksum gate and a Task pin that no longer exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
worktrunk-bot
left a comment
There was a problem hiding this comment.
All three points from the last review are addressed in 230f790: MISE_HTTP_RETRIES: 6 is back on the setup task (thread resolved), the CLAUDE.md Quick Start line no longer calls the setup checksum-gated, and the weekly running-tend bullet drops the digest-copy step and now says Task tracks latest through mise — which matches the unpinned mise use --global task in the Taskfile. Nothing outstanding on the diff; the approval is held only on CI.
The red on this head is a GitHub incident, not the PR. GET /repos/nushell/nushell/releases is returning [] right now, authenticated and unauthenticated alike, while GET /repos/nushell/nushell/releases/tags/0.115.0 returns the release with all 16 assets — including nu-0.115.0-x86_64-unknown-linux-gnu.tar.gz. hustcer/setup-nu resolves its asset off the list endpoint, so it fails with No release for Nushell matching version specifier 0.115.0 and takes test (linux), test (macos), fast-checks, code-coverage, and both advisory affected tests down at the setup step, before any of this PR's code is reached. The same jobs were green on ca8e92d thirty minutes earlier against the same pin.
I reran the failed jobs. That cleared lint, which had died on a different symptom of the same incident (429 Too Many Requests from codeload fetching Swatinem/rust-cache), but setup-nu fails identically on attempt 2 — so further reruns won't help until the releases endpoint recovers. test (windows) passed on both attempts.
Not approving over a red rollup; I'll approve on the next green run.
Evidence
Reproducible from outside CI while the incident lasts:
$ gh api 'repos/nushell/nushell/releases?per_page=3' --jq 'length'
0
$ gh api repos/nushell/nushell/releases/tags/0.115.0 --jq '.assets|length'
16
Failing step, identical on attempt 1 and attempt 2:
Try to get assets for Nu: arch = x64, platform = linux, feature = default
##[error]No release for Nushell matching version specifier 0.115.0 of default feature.
lint, attempt 1 only — cleared by the rerun:
##[error]Response status code does not indicate success: 429 (Too Many Requests).
##[error]Failed to download archive 'https://codeload.github.com/Swatinem/rust-cache/tar.gz/6323deb...' after 3 attempts.
setup-nu is used at four sites, so the same blast radius applies to nightly and benchmarks if the endpoint is still degraded when they fire: .github/actions/test-setup/action.yaml, .github/workflows/nightly.yaml, .github/workflows/benchmarks.yaml, .github/workflows/coverage.yaml.
mise was in the environment commands only to fetch Task, and Task only ran a shell block. Task could not join the script's own `install_binary` list, because the Taskfile was what ran that list — installing Task from it needs Task already present. Dropping Task as the runner breaks the circle: `codex.sh` is plain bash, and Task installs from its release archive with a SHA-256 check like cargo-insta, cargo-nextest, Nushell, and PowerShell. Nothing bootstraps anything, so the launchers are `bash scripts/codex-cloud/codex.sh setup` and `… maintain`. The root Taskfile's `setup-codex` and `maintain-codex` forwarded into the deleted Taskfile and nothing called them: the Cloud launchers targeted the sub-Taskfile directly, and both require root on the universal image, so neither runs on a dev machine. `test_codex_cloud_launchers_match_taskfile` derived the Taskfile's digest so the README launchers could not drift from its bytes. Without a digest both sides were constants in the same repo, and it never could observe the Codex Cloud settings field that actually holds the command. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#3841) Consolidates the two remaining Codex Cloud drafts, #3835 and #3838, onto what #3839 landed, then cuts what was left. Main's setup is a 163-line script and a 26-line README under `scripts/codex-cloud/`; this is one 79-line file at `dev/codex.sh`, beside the repo's other development files, with the README folded into its header. That empties `scripts/` — the directory existed only for this. **Codex Cloud runs as root** (from #3835). Setup used to replace `/root/.cargo/bin/cargo` with a wrapper that re-executed cargo as a UID-1000 `ubuntu` user under `tini`, and maintenance chowned the checkout, the rustup home, and three cache directories to match. All of it existed to keep the suite's permission tests from skipping, since root can write to a read-only file. Worth stating plainly: ten tests now skip on Codex Cloud. The pair carrying the most weight is `test_remove_foreground_succeeds_with_stuck_directory` and its `_detached` twin, the only automated coverage of `wt remove` against a directory it cannot delete. It is not a new hole — `setup-web` creates no non-root user, so the Claude Code web environment has always skipped them, and `tests/integration_tests/approval_pty.rs:157` carries a standing TODO about it. Codex Cloud was the one environment buying an exception, and a cargo wrapper, `tini`, `runuser`, and four chown passes were the price. Both environments now agree about what the suite observes, and that TODO is the single place to fix it for both. Three of the ten decided that skip by reading `$USER` rather than by probing the filesystem, which fails open: a container that runs as root without exporting `USER` runs them and asserts an error root never gets. They probe now, through one helper, like the other seven. **Task, the checksums, and the retries are gone.** Nothing invoked Task on Codex Cloud once #3839 stopped routing the launchers through it, so it is no longer installed. The archive checksums follow the Taskfile digest for the same reason that one went: HTTPS authenticates GitHub and the container is disposable and secret-free, so verifying each download bought a helper pair and a 64-char line per tool for very little. Each install is now `curl | tar` and an `install`. The version numbers stay. The gate runs `--all-features`, so nu and pwsh drive PTY snapshots their own versions can move, and `.github/actions/test-setup/action.yaml` pins cargo-insta, cargo-nextest, and nu to the same three versions — unpinning those would make the environment and CI disagree about snapshot output. Nothing under `.github/` pins PowerShell, so CI runs whatever the runner image ships and that version answers to nothing but these scripts. **`setup-web` catches up** (from #3838). It gains `lsof`, installs Nushell from its release archive rather than checking that one is already present, bootstraps `uv` with pre-commit, and puts `$HOME/.local/bin` on PATH. `wt` installs from the debug build produced a few lines earlier instead of through a second full compile. Its `cargo install` of cargo-insta and cargo-nextest stays unconditional, as on main: neither form pins, and a `command -v` guard would have frozen whatever versions the image happened to carry. ## Testing `cargo run -- hook pre-merge --yes` passes, shellcheck is clean at warning level, and `task --list` still parses. The `.tar.xz` and `.tar.gz` extractions were run against the real release archives to confirm the tar flags and the paths inside them. The three converted tests pass unprivileged; their skip branch rests on the same probe the other seven root-skipping tests already use. Neither setup path is executable from a dev machine — Codex Cloud needs Linux and root on the universal image, `setup-web` needs a web image — so the first real exercise is the next environment build. > _This was written by Claude Code on behalf of max-sixty_ --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Codex Cloud environment commands ran through four layers: a settings field invoked mise, mise fetched Task, Task ran
scripts/codex-cloud/Taskfile.yaml, and that Taskfile's whole body was one shell block. This collapses them tobash scripts/codex-cloud/codex.sh setupandbash scripts/codex-cloud/codex.sh maintain.mise was there only to break a bootstrap circle. The script already installs cargo-insta, cargo-nextest, Nushell, and PowerShell from their release archives with SHA-256 checks, but Task could not join that list while Task was also running it — installing Task from the Taskfile needs Task already present. Dropping Task as the runner dissolves the circle:
codex.shis plain bash, and Task installs from its own archive with a checksum like the other four. Nothing bootstraps anything.Two things go with it. The launchers no longer verify a SHA-256 digest of the script before running it as root, and
MISE_NO_CONFIG=1is gone because mise is. Both were introduced in #3810 to stop a task branch changing the code setup runs as root. The environment carries no variables or secrets, it is recreated per task, and the agent already runs repo code there asubuntu. The payoff is that the two settings fields never need editing again — previously every Taskfile change meant pasting a fresh digest into both and invalidating the cache.The root
Taskfile.yamlhadsetup-codexandmaintain-codexforwarding into the deleted Taskfile. Nothing called them: the Cloud launchers targeted the sub-Taskfile directly with-t, and both require root on the universal image, so neither runs on a dev machine. Deleted.test_codex_cloud_launchers_match_taskfileis deleted too. It derived the Taskfile's digest and Task version so the README launchers could not drift from the file's actual bytes. Without a digest both sides were hardcoded constants in the same repo, and it never could observe the Codex Cloud settings field that actually holds the command — an environment-specific test in a suite whose job is testingwt.Testing
cargo run -- hook pre-merge --yespasses. The Task archive's checksum was verified against a fresh download rather than copied from the release notes.codex.shitself needs Linux, root, and the universal image's UID-1000ubuntuuser, so nothing here executes it —bash -nand shellcheck are clean, and the first real exercise is the next Codex Cloud environment build. The shell body is carried over from the Taskfile unchanged apart from the Task install and the dropped mise lines.