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