From 52d3f333017f4cb921adbc9756b274f5d7493db2 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Fri, 23 Oct 2026 23:04:09 -0700 Subject: [PATCH 1/4] chore(quest): claim quest/m0/lifecycle-flakes.md Quest-Claim: fb39382efc3023a6 From 07f829d492737b70b18f233eb940c3afac3d2639 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 24 Oct 2026 00:01:42 -0700 Subject: [PATCH 2/4] ci: run the macOS lifecycle job on main, not pull requests Hosted macOS runners can queue past the job's timeout, which reports a job that never ran as failed. Pull requests keep native macOS coverage through Release's lifecycle checks; main still checks the flake's macOS build. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/check.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index b42cf1ce..b691dcae 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -69,8 +69,11 @@ jobs: if: github.event_name != 'pull_request' || github.event.pull_request.draft == false strategy: fail-fast: false + # Hosted macOS runners can queue past the timeout, which then reports a job + # that never ran as failed. Pull requests rely on Release's native macOS + # lifecycle checks instead; main still checks the flake's macOS build. matrix: - runner: [ubuntu-latest, macos-14] + runner: ${{ github.event_name == 'pull_request' && fromJSON('["ubuntu-latest"]') || fromJSON('["ubuntu-latest", "macos-14"]') }} runs-on: ${{ matrix.runner }} timeout-minutes: 15 steps: From 44efbe9364af6fb4fabd54bb7eb644c4ecaac7b6 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 24 Oct 2026 00:00:38 -0700 Subject: [PATCH 3/4] fix(ci): never delete an executable the lifecycle check just ran On windows-11-arm, GitHub's hosted runner agent (provjobd, started by hosted-compute-agent) periodically opens the images of running processes without delete sharing for a few hundred milliseconds. When that caught the check's last quest.exe run, removing the check's copy of the binary failed with EBUSY. Run the caller's binary in place, and let the runner discard install-check.ps1's scratch under RUNNER_TEMP rather than deleting it. Co-Authored-By: Claude Opus 5.5 --- scripts/install-check.ps1 | 6 ++++-- scripts/lifecycle-check.mjs | 17 +++++------------ scripts/lifecycle-check.sh | 7 +++++-- 3 files changed, 14 insertions(+), 16 deletions(-) diff --git a/scripts/install-check.ps1 b/scripts/install-check.ps1 index 236c39f5..29bb8b77 100644 --- a/scripts/install-check.ps1 +++ b/scripts/install-check.ps1 @@ -10,7 +10,10 @@ if ($Tag -notmatch '^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?( $architecture = [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString() $expectedArchitecture = if ($Target -eq 'aarch64-pc-windows-msvc') { 'Arm64' } else { 'X64' } if ($architecture -ne $expectedArchitecture) { throw "Expected native $expectedArchitecture runner; got $architecture" } -$work = Join-Path ([System.IO.Path]::GetTempPath()) ('quest install ' + [guid]::NewGuid()) +# The runner empties RUNNER_TEMP after the job, so nothing here deletes the installed binary: on +# windows-11-arm, the runner's agent briefly opens running executables without delete sharing. +if (-not $env:RUNNER_TEMP) { throw 'Run this on a GitHub Actions runner, which empties RUNNER_TEMP' } +$work = Join-Path $env:RUNNER_TEMP ('quest install ' + [guid]::NewGuid()) $originalDirectory = Get-Location $originalMise = @{} foreach ($name in @('MISE_CONFIG_DIR', 'MISE_DATA_DIR', 'MISE_CACHE_DIR')) { $originalMise[$name] = [Environment]::GetEnvironmentVariable($name, 'Process') } @@ -53,5 +56,4 @@ try { } finally { Set-Location $originalDirectory foreach ($name in $originalMise.Keys) { [Environment]::SetEnvironmentVariable($name, $originalMise[$name], 'Process') } - if (Test-Path -LiteralPath $work) { Remove-Item -LiteralPath $work -Recurse -Force } } diff --git a/scripts/lifecycle-check.mjs b/scripts/lifecycle-check.mjs index e8e39aa6..c303b1eb 100644 --- a/scripts/lifecycle-check.mjs +++ b/scripts/lifecycle-check.mjs @@ -2,8 +2,6 @@ import assert from "node:assert/strict"; import { execFileSync, spawnSync } from "node:child_process"; import { - chmodSync, - copyFileSync, existsSync, lstatSync, mkdirSync, @@ -29,17 +27,12 @@ const assertFile = (root, path, expected) => assert.equal(read(root, path), expe const skills = ["audit", "complete", "delete", "export", "import", "iterate", "merge", "plan", "spawn", "start"]; const destinations = [".claude/skills", ".agents/skills"]; try { + // Run the caller's binary in place, so the cleanup below never deletes an executable it just ran: + // on windows-11-arm, the hosted runner's agent briefly opens running executables without delete + // sharing, and removing one then fails with EBUSY. Callers test a binary path with spaces. const javascript = extname(input) === ".js"; - const binary = join( - work, - "tool bin", - javascript ? "quest.js" : process.platform === "win32" ? "quest.exe" : "quest", - ); - mkdirSync(dirname(binary)); - copyFileSync(input, binary); - if (process.platform !== "win32") chmodSync(binary, 0o755); - const command = javascript ? process.execPath : binary; - const prefix = javascript ? [binary] : []; + const command = javascript ? process.execPath : input; + const prefix = javascript ? [input] : []; const run = (root, ...args) => execFileSync(command, [...prefix, "--root", root, ...args], { cwd: root, diff --git a/scripts/lifecycle-check.sh b/scripts/lifecycle-check.sh index a984ea8e..5f58c0fc 100644 --- a/scripts/lifecycle-check.sh +++ b/scripts/lifecycle-check.sh @@ -14,8 +14,11 @@ esac work=$(mktemp -d "${TMPDIR:-/tmp}/quest-lifecycle.XXXXXX") trap 'rm -rf "$work"' EXIT -# The shared lifecycle is also run directly by native Windows CI. -bun "$(dirname -- "${BASH_SOURCE[0]}")/lifecycle-check.mjs" "$binary" +# The shared lifecycle is also run directly by native Windows CI. It runs the +# binary in place, so give it a path with spaces. +mkdir "$work/tool bin" +cp "$binary" "$work/tool bin/" +bun "$(dirname -- "${BASH_SOURCE[0]}")/lifecycle-check.mjs" "$work/tool bin/$(basename -- "$binary")" # A reader that closes stdout early, like `quest unblocked | head -1`, ends the # command quietly. The writer waits on a FIFO until the reader has closed, so From 7c8f9c206fd2fd620d8d6e4d36f96b9d382c3323 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sat, 24 Oct 2026 00:15:38 -0700 Subject: [PATCH 4/4] quest: complete lifecycle flakes Co-Authored-By: Claude Opus 5.5 --- quest/m0/README.md | 1 - quest/m0/lifecycle-flakes.md | 30 ------------------------------ 2 files changed, 31 deletions(-) delete mode 100644 quest/m0/lifecycle-flakes.md diff --git a/quest/m0/README.md b/quest/m0/README.md index 98a86a04..7e26660a 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -35,7 +35,6 @@ ownership-manifest design: ## Required -- [Lifecycle flakes](/quest/m0/lifecycle-flakes.md) - lifecycle CI jobs stop failing on Windows `EBUSY` and unstarted macOS jobs - [Worker tests](/quest/m0/worker-tests.md) - every Worker test passes alone and in any order - [Gate bench](/quest/m0/gate-bench.md) - measure headroom admission on an idle host - [Agent lifecycle integration](/quest/m0/worktree-agents.md) - reclaim completed and suspended checkouts through their harness diff --git a/quest/m0/lifecycle-flakes.md b/quest/m0/lifecycle-flakes.md deleted file mode 100644 index 11ea183e..00000000 --- a/quest/m0/lifecycle-flakes.md +++ /dev/null @@ -1,30 +0,0 @@ -# [S] Stop the lifecycle CI jobs failing at random - -## Goal - -The lifecycle CI jobs fail only when the code under test is broken: the -Windows check never fails with `EBUSY` while removing its temporary -directory, and a macOS job that never gets a runner does not show as a -failed check. - -## Plan - -Decided 2026-10-22 (user) as follow-ups of -[kixelated/quest#192](https://github.com/kixelated/quest/pull/192) and -[kixelated/quest#191](https://github.com/kixelated/quest/pull/191): - -- **Windows `EBUSY`.** The Windows arm64 Release job failed after its own - checks passed, in the final `rmSync(work, { recursive: true, force: true })` - of `scripts/lifecycle-check.mjs`, on #192 and on the `quest/m0/worktree` - branch (run 39679528766). A rerun passed both times. Find which process or - handle still holds the directory (for example a child process the check - started, or a file it left open) and fix that cause. No retries, sleeps, or - ignored errors. Reproduce it on a native Windows runner before fixing. -- **macOS queue timeout.** On #191 and - [kixelated/quest#185](https://github.com/kixelated/quest/pull/185), the - optional `lifecycle (macos-14)` job waited 15 minutes for a hosted runner - during a GitHub macOS capacity notice, was cancelled without running, and - showed as failed. Decide from the workflow (`.github/workflows/check.yml`) - whether to shorten its queue limit, report it as skipped, or drop it from - pull request runs, keeping macOS covered somewhere. -- Keep both fixes covered by the existing lifecycle jobs.