From 2abbc3d7086a07ae7aff3cf35e027fad3948d7c2 Mon Sep 17 00:00:00 2001 From: Will Chandler Date: Fri, 23 Oct 2026 21:11:01 -0400 Subject: [PATCH] ci: read the illumos set on Linux instead of macOS The macos job builds the workspace and the Mach-O fixtures, waits for the linux job and buildomat's helios job, and runs the suite over the cores both took. Its point is reading each set away from the system that took it. For the Linux sets that adds nothing: the testkit opens every Linux core with its set's own copy of the libraries the core maps (Capture::open, testkit::paths), on Linux and macOS alike, so both read the same files the same way. Only the illumos reader takes a different path off illumos: there resolve_path canonicalizes a mapped object's path against the local filesystem, and anywhere else it keeps the path the core recorded. Meanwhile the job is a second full build on a runner billed at a higher rate than Linux's, and in the three runs before this change 5 to 11 of its 13 to 18 minutes were spent waiting for the other two. The linux job now does the illumos read itself. After its own captures it waits for helios and unpacks the illumos set into HANSEI_CORES, where a capturing system reads every set it finds, so one suite run covers all three. A helios job that delivers nothing leaves the Linux sets read and fails the job by name afterwards, as the macos job did. The wait moves into the linux job's time, so its timeout goes from 120 to 180 minutes, the macos job's old ceiling. CI no longer sets HANSEI_SETS. What CI no longer covers is macOS itself: the Mach-O fixture builds and the dSYM extraction the golden tests read there, and the build against macOS's libc. Only a run on a Mac checks those now. The suite, run in the test image over fresh linux and linux-floor captures beside the illumos set CI published for 768047290 (#18), passed in full on that tree. The workflow's new steps have not yet run. Co-Authored-By: Claude Opus 5.5 --- .github/actions/macos-setup/action.yml | 60 ---------- .github/buildomat/jobs/helios.sh | 10 +- .github/scripts/wait-helios.sh | 2 +- .github/scripts/wait-linux.sh | 59 ---------- .github/workflows/ci.yml | 152 ++++++++----------------- CLAUDE.md | 7 +- hansei-runtime/src/testkit/cores.rs | 4 +- 7 files changed, 61 insertions(+), 233 deletions(-) delete mode 100644 .github/actions/macos-setup/action.yml delete mode 100755 .github/scripts/wait-linux.sh diff --git a/.github/actions/macos-setup/action.yml b/.github/actions/macos-setup/action.yml deleted file mode 100644 index 14e315654..000000000 --- a/.github/actions/macos-setup/action.yml +++ /dev/null @@ -1,60 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. - -name: macos setup -description: > - The toolchain, cargo-nextest and build caches for a macOS check. - The job selects its Xcode through DEVELOPER_DIR: the Mach-O fixture - builds feed the extraction goldens and the bundle-only tests, and a - new linker can change which identical functions it folds, so - changing Xcode (or the runner image's macOS) is a deliberate step, - like a Linux image bump. - -inputs: - save: - description: > - Whether to save the build caches when this job missed them. One - macOS job saves; two saving one key would race, and whichever won - would decide what the other restores. - required: true - -runs: - using: composite - steps: - - name: Versions - shell: bash - run: | - sw_vers - xcodebuild -version - cargo --version - rustc --version - df -h "$GITHUB_WORKSPACE" - - - name: cargo-nextest - shell: bash - env: - NEXTEST_VERSION: 0.9.143 - NEXTEST_SHA256: 4830d430411148d17602a75cc880bfb4dc8dac153dea59a48a2ef4cc93577f07 - run: | - curl -sSfL --retry 5 --retry-all-errors -o "$RUNNER_TEMP/nextest.tar.gz" \ - "https://github.com/nextest-rs/nextest/releases/download/cargo-nextest-$NEXTEST_VERSION/cargo-nextest-$NEXTEST_VERSION-universal-apple-darwin.tar.gz" - echo "$NEXTEST_SHA256 $RUNNER_TEMP/nextest.tar.gz" | shasum -a 256 -c - - tar -xzf "$RUNNER_TEMP/nextest.tar.gz" -C "$HOME/.cargo/bin" cargo-nextest - cargo nextest --version - - # The dependency builds of the workspace and of the Mach-O fixture - # builds, and the crates downloaded for them. rust-cache drops the - # workspace's crates, test binaries and the fixture programs - # themselves, which a checkout's fresh mtimes rebuild every run. A - # failing test leaves those builds as good as a passing one, so a - # red run saves them too. - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - with: - shared-key: macos-${{ env.DEVELOPER_DIR }}-${{ hashFiles('test-programs/matrix.toml', 'test-programs/locks/*.lock') }} - save-if: ${{ inputs.save }} - cache-on-failure: "true" - cache-bin: "false" - workspaces: | - . -> target - test-programs -> fixtures/target diff --git a/.github/buildomat/jobs/helios.sh b/.github/buildomat/jobs/helios.sh index 18e1c4aed..7a527b9af 100755 --- a/.github/buildomat/jobs/helios.sh +++ b/.github/buildomat/jobs/helios.sh @@ -20,12 +20,12 @@ #: from_output = "/work/cores-illumos.tar.zst" # # The whole suite on illumos, acceptance included, capturing the -# illumos fixture set. The set is published for the macOS job that -# reads it (`macos` in .github/workflows/ci.yml), which fetches it by +# illumos fixture set. The set is published for the linux job that +# reads it (`linux` in .github/workflows/ci.yml), which fetches it by # commit once this job's check run has passed. # -# The default target size: the job is done minutes before that macOS -# check finishes building, so a larger machine only reserves more. +# The default target size. The linux job waits for this one once its +# own captures are done; a larger machine would shorten that wait. # # Buildomat publishes the first file it is given for a commit and # refuses every later one, even from a rerun, so the archive is built @@ -109,7 +109,7 @@ ls -l /work/cores-illumos.tar.zst # Buildomat does not upload an output over its per-file cap (1 GiB by # default), and the publish naming it then publishes nothing, silently. # A red check here says so, where a missing file would only time out -# the macOS check waiting for it. +# the linux check waiting for it. max=$((1024 * 1024 * 1024)) size=$(wc -c max )); then diff --git a/.github/scripts/wait-helios.sh b/.github/scripts/wait-helios.sh index ee15bf720..2a41831f3 100755 --- a/.github/scripts/wait-helios.sh +++ b/.github/scripts/wait-helios.sh @@ -12,7 +12,7 @@ # The job is not an Actions job, so nothing here can `needs:` it: this # polls its check run instead, for as long as buildomat takes to queue # and run it, up to a ceiling that only a hang should reach. A failed -# helios job fails this at once, naming it, so a red macOS check says +# helios job fails this at once, naming it, so a red linux check says # which side broke. A queue that is merely slow delays the check # without failing it. # diff --git a/.github/scripts/wait-linux.sh b/.github/scripts/wait-linux.sh deleted file mode 100755 index 7fb5b5def..000000000 --- a/.github/scripts/wait-linux.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at https://mozilla.org/MPL/2.0/. -# -# Wait for this workflow run's linux job to finish, then unpack the -# Linux fixture sets it archived (cores-linux) into a directory. -# -# wait-linux.sh DIR -# -# The macOS job builds while the linux job runs, so it cannot `needs:` -# it: this polls the job instead, as wait-helios.sh polls buildomat's. -# The linux job archives whatever it captured whether or not its own -# suite passed, so a red linux job still has cores to read; one that -# finished with none — cancelled, or never given a runner — fails this -# at once, naming it. -# -# Environment: -# GH_TOKEN a token that can read the run's jobs and -# artifacts -# GITHUB_REPOSITORY owner/repo -# GITHUB_RUN_ID the run whose linux job to wait for -# LINUX_WAIT_MINUTES the ceiling (default: 150) - -set -euo pipefail - -die() { printf 'wait-linux.sh: %s\n' "$*" >&2; exit 1; } - -[ $# -eq 1 ] || die "usage: wait-linux.sh DIR" -dest=$1 -repo=${GITHUB_REPOSITORY:?} -run=${GITHUB_RUN_ID:?} -job=linux -deadline=$(( $(date +%s) + ${LINUX_WAIT_MINUTES:-150} * 60 )) - -while :; do - # The latest attempt of each job: a rerun of failed jobs reuses - # the linux job's earlier result, and its artifact with it. - state=$(gh api "repos/$repo/actions/runs/$run/jobs?filter=latest&per_page=100" \ - --jq ".jobs[] | select(.name == \"$job\") | [.status, (.conclusion // \"-\"), .html_url] | @tsv") - if [ -n "$state" ]; then - IFS=$'\t' read -r status conclusion link <<<"$state" - [ "$status" = completed ] && break - printf '%s: %s job %s\n' "$(date -u +%H:%M:%S)" "$job" "$status" - else - printf '%s: no %s job in run %s yet\n' "$(date -u +%H:%M:%S)" "$job" "$run" - fi - [ "$(date +%s)" -lt "$deadline" ] \ - || die "gave up on the $job job after ${LINUX_WAIT_MINUTES:-150} minutes" - sleep 30 -done - -work=$(mktemp -d) -trap 'rm -rf "$work"' EXIT -gh run download "$run" --repo "$repo" -n cores-linux -D "$work" \ - || die "the $job job finished $conclusion without archiving any cores: $link" -mkdir -p "$dest" -zstd -dc "$work/cores-linux.tar.zst" | tar -xf - -C "$dest" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 486e4b9ae..8a2b7f5a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,23 +4,22 @@ # # The merge gate. Every Linux and illumos run builds the fixture # programs, cores them, and checks the cores against the committed -# goldens; macOS cannot core an ELF process, so it reads the cores -# those runs took, through the same production readers, against the -# same goldens. Four checks: +# goldens. The linux job also reads the illumos set, through the same +# production readers, against the same goldens: a core read away from +# the system that took it, as one is whenever an illumos core is +# copied to a laptop. Three checks: # # lint rustfmt over both manifests, clippy # linux the whole suite in the pinned image (.github/image/), -# capturing the linux and linux-floor sets +# capturing the linux and linux-floor sets and reading the +# illumos set beside them # helios the whole suite on illumos, capturing the illumos set # (buildomat: .github/buildomat/jobs/helios.sh) -# macos the whole suite over the cores both of them took, built -# once while they run # -# The jobs that read cores test a pull request's head commit, not the -# merge commit Actions checks out by default: buildomat builds the head -# and publishes the illumos cores under it, and one macOS build reads -# every set, each of which must have been captured from the tree it -# runs in. +# The linux job tests a pull request's head commit, not the merge +# commit Actions checks out by default: buildomat builds the head and +# publishes the illumos cores under it, and a set is read only by the +# tree it was captured from. # # The version matrix is not among them: it is dispatched by hand # (matrix.yml). @@ -30,8 +29,9 @@ # test binary, and a runner has disk for little else. # # A golden read from cores is blessed only by the host that captures -# its set; a diff only macOS shows is a reader bug, and one a host -# and this workflow disagree on is a canonicalization gap. +# its set; a diff only the linux job's read of the illumos set shows +# is a reader bug, and one a host and this workflow disagree on is a +# canonicalization gap. name: ci @@ -60,8 +60,7 @@ env: HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} jobs: - # Its own job, so a lint failure neither stops the suite from running - # nor leaves the macOS check without cores to read. + # Its own job, so a lint failure does not stop the suite from running. lint: name: lint runs-on: ubuntu-26.04 @@ -88,7 +87,14 @@ jobs: linux: name: linux runs-on: ubuntu-26.04 - timeout-minutes: 120 + # The wait for helios below lasts as long as buildomat does. + timeout-minutes: 180 + permissions: + contents: read + packages: read + checks: read + env: + GH_TOKEN: ${{ github.token }} steps: - uses: actions/checkout@v7 with: @@ -118,19 +124,39 @@ jobs: - name: Fixtures run: .github/scripts/in-image.sh target/ci/examples/build_fixtures + # The illumos set, unpacked beside this job's own: a capturing + # system reads every set it finds there (testkit::cores::sets). + # Waited for only now, so that buildomat's run overlaps this + # job's build and captures. A helios job that delivers nothing + # leaves this job's own sets read, and fails it by name after. + - id: helios + name: Wait for helios + continue-on-error: true + run: .github/scripts/wait-helios.sh "$HEAD_SHA" "$HANSEI_CORES" + + # Again, now that it is there: the illumos set's bundles. + - name: Fixtures (illumos) + if: steps.helios.outcome == 'success' + run: .github/scripts/in-image.sh target/ci/examples/build_fixtures + - name: Test run: .github/scripts/in-image.sh cargo nextest run --workspace --locked --no-fail-fast --profile ci --cargo-profile ci + - name: Illumos set read? + if: ${{ !cancelled() && steps.helios.outcome == 'failure' }} + run: | + echo "::error::helios delivered no cores, so the illumos set went unread; see its check" + exit 1 + - name: Disk if: always() run: df -h / "${{ steps.image.outputs.scratch }}" - # Whatever was captured, pass or fail: the macOS job reads it - # (.github/scripts/wait-linux.sh), and a failure here is - # root-caused from it. A Linux core is - # mostly holes, which --sparse keeps out of the archive and the - # reader's disk alike. The dot-named bookkeeping is this host's - # own. + # Whatever was captured, pass or fail: a failure here is + # root-caused from it, and a Mac reads it with `gh run download`. + # A Linux core is mostly holes, which --sparse keeps out of the + # archive and the reader's disk alike. The dot-named bookkeeping + # is this host's own. - id: cores name: Archive cores if: always() @@ -155,85 +181,3 @@ jobs: path: ${{ runner.temp }}/cores-linux.tar.zst compression-level: 0 retention-days: ${{ github.event_name == 'pull_request' && 7 || 30 }} - - # One build reads every set: it compiles while the linux job and - # buildomat's helios job take their cores, waits for both, and runs - # the suite once over whatever they delivered. A side that delivered - # nothing leaves the others read, and fails the job by name after. - macos: - name: macos - runs-on: macos-26 - # The waits below last as long as the linux job and buildomat do. - timeout-minutes: 180 - permissions: - contents: read - actions: read - checks: read - env: - DEVELOPER_DIR: /Applications/Xcode_26.5.app/Contents/Developer - GH_TOKEN: ${{ github.token }} - steps: - - uses: actions/checkout@v7 - with: - ref: ${{ env.HEAD_SHA }} - - - uses: ./.github/actions/macos-setup - with: - save: "true" - - - name: Fixture toolchain - run: .github/scripts/fixture-toolchain.sh - - # Before any cores arrive: the Mach-O fixture builds and the - # bundles extracted from them. - - name: Build - run: | - cargo nextest run --workspace --locked --no-run --cargo-profile ci - target/ci/examples/build_fixtures - - - id: linux - name: Wait for linux - continue-on-error: true - run: .github/scripts/wait-linux.sh "$RUNNER_TEMP/cores" - - - id: helios - name: Wait for helios - continue-on-error: true - run: .github/scripts/wait-helios.sh "$HEAD_SHA" "$RUNNER_TEMP/cores" - - - name: Sets - env: - LINUX: ${{ steps.linux.outcome }} - HELIOS: ${{ steps.helios.outcome }} - run: | - sets=() - [ "$LINUX" = success ] && sets+=(linux linux-floor) - [ "$HELIOS" = success ] && sets+=(illumos) - if [ ${#sets[@]} -eq 0 ]; then - echo "::error::neither the linux job nor helios delivered cores; see their logs" - exit 1 - fi - { - echo "HANSEI_CORES=$RUNNER_TEMP/cores" - echo "HANSEI_SETS=$(IFS=,; echo "${sets[*]}")" - } >>"$GITHUB_ENV" - - # Again, now that there are cores: each set's bundles. - - name: Fixtures - run: target/ci/examples/build_fixtures - - - name: Test - run: cargo nextest run --workspace --locked --no-fail-fast --profile ci --cargo-profile ci - - - name: Every set read? - if: ${{ !cancelled() }} - env: - LINUX: ${{ steps.linux.outcome }} - HELIOS: ${{ steps.helios.outcome }} - run: | - missing=0 - [ "$LINUX" = success ] \ - || { echo "::error::the linux job delivered no cores, so its sets went unread; see its log"; missing=1; } - [ "$HELIOS" = success ] \ - || { echo "::error::helios delivered no cores, so the illumos set went unread; see its check"; missing=1; } - exit "$missing" diff --git a/CLAUDE.md b/CLAUDE.md index 54acb55bb..77b832c06 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -193,7 +193,8 @@ Green means green on **all three platforms**: macOS, illumos and Linux. Each has caught a failure the other two could not, so a suite run on one proves little about the others. Detection is covered portably by the golden tests below; a real cored target on an illumos or Linux host is where the -render side and the acceptance suite are exercised. +render side and the acceptance suite are exercised. CI runs on Linux and +illumos only, so macOS is green only when someone has run it there. **A flaky test is a bug with a cause, and the cause must be identified every time one occurs.** Re-running a failed test and moving on because @@ -292,7 +293,9 @@ check: reader, canonicalized and masked so two captures print the same. - Linux and illumos take their own sets every run (`linux` and `linux-floor`; `illumos`) into `HANSEI_CORES` (default - `test-programs/fixtures/cores`). macOS takes none and reads copies: + `test-programs/fixtures/cores`), and read any other set already + there: CI's linux job reads the illumos set that way, a core read + away from the system that took it. macOS takes none and reads copies: `check-all.sh`'s local leg copies the hosts', and `gh run download ` fetches CI's. A set whose cores are missing fails every test reading it, with one message saying how to get them. diff --git a/hansei-runtime/src/testkit/cores.rs b/hansei-runtime/src/testkit/cores.rs index 24b02c187..07e0a72b2 100644 --- a/hansei-runtime/src/testkit/cores.rs +++ b/hansei-runtime/src/testkit/cores.rs @@ -71,8 +71,8 @@ pub const CAPTURED: &[&str] = if cfg!(target_os = "linux") { /// Names the sets a run reads, comma-separated, in place of the ones /// [`sets`] would choose. For a run that holds only some sets' cores on -/// purpose: CI's macOS job names the sets whose cores arrived, so a -/// capturing system that delivered none leaves the others read. +/// purpose: a Mac that has copied one host's sets and not the other's +/// names those it has, and reads them without failing on the rest. pub const SETS: &str = "HANSEI_SETS"; /// The sets a run reads cores of: those [`SETS`] names, if it names