open-standards/.github/workflows/standards-build.yml
Transaction Science 4ec33b90e6
All checks were successful
standards workspaces build + test / arl-rs (pull_request) Successful in 1m7s
standards workspaces build + test / eoc-rs (pull_request) Successful in 1m58s
standards workspaces build + test / jcp-rs (pull_request) Successful in 1m28s
standards workspaces build + test / joule-ui-rs (pull_request) Successful in 1m12s
standards workspaces build + test / joulehook-rs (pull_request) Successful in 1m12s
standards workspaces build + test / proof-rs (pull_request) Successful in 1m17s
standards workspaces build + test / sandbox-rs (pull_request) Successful in 2m13s
standards workspaces build + test / sense-rs (pull_request) Successful in 1m10s
standards workspaces build + test / smart-byte-rs (pull_request) Successful in 3m45s
workflow hygiene / every workflow supersedes its own queue (pull_request) Successful in 3s
standards workspaces build + test / jouleclaw-rs (pull_request) Successful in 2m29s
standards workspaces build + test / aac-rs (pull_request) Successful in 1m30s
standards workspaces build + test / joule-code-rs (pull_request) Successful in 55s
standards workspaces build + test / joulecontract-rs (pull_request) Successful in 37s
standards workspaces build + test / map-rs (pull_request) Successful in 36s
standards workspaces build + test / mesh-rs (pull_request) Successful in 31s
standards workspaces build + test / openpay (pull_request) Successful in 1m46s
standards workspaces build + test / jouledesk-rs (pull_request) Successful in 3m52s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 5m44s
ci: guard the concurrency groups, and stop the dev Mac serving 13 of 21 contexts
Two things I argued myself out of doing in #161 and should not have.

## The guard

#161 added a concurrency group to all 15 workflows and nothing stopped the
sixteenth from omitting one — which reintroduces the failure for the whole
repo, invisibly: the workflow still passes, it just queues forever under load.

`scripts/check-workflow-concurrency.sh` requires a top-level `concurrency:`
block whose group varies by `github.ref`. The second condition matters as much
as the first: a fixed group serialises every branch against every other, which
is worse than no group at all.

My reason for skipping it was that a guard "would need its own CI job on the
runner this change exists to unblock". That was wrong on the facts. The guard
runs on `linux` — jetson-hub — not the dev Mac, so it costs nothing I was
trying to protect, and it needs no toolchain.

It also has a self-test, because this repo has a history of gates that passed
while guarding nothing. `check-workflow-concurrency-selftest.sh` breaks the
property both ways against a scratch copy and asserts the check rejects each.

That self-test began as an inline Python heredoc in a `run:` block, and its
body sat at column zero, which made the workflow file **invalid YAML**. An
unparseable workflow means no CI at all — the exact failure the change exists
to prevent, reintroduced by the fix for it. Caught by parsing every workflow
with `yaml.safe_load` before committing; it is a script now.

## The rebalance

#161 named this and left it: the dev Mac hosts **both** the `macos` runner and
the `linux-arm64` colima VM, so it served **13 of 21 contexts** while jetson-hub
sat idle. That is why a PR's `linux` contexts returned in minutes and its macOS
ones could wait indefinitely.

Four workspaces move to `linux` on evidence rather than preference. `arl-rs`,
`sense-rs`, `joulehook-rs` and `joule-ui-rs` each have **zero**
`target_os = "macos"` cfgs, **zero** Apple dependencies, and **zero** Apple
crates in their `Cargo.lock` — so running them on macOS asserted nothing the
repo relies on.

`jouledesk-rs` stays on `macos`, because it does need it: `libwebrtc` and 19
Apple crates in its lockfile. The `linux-arm64` placements stay as well — arm64
is the deployment target, and `jouleclaw-rs` requires `+fp16`.

Labels go from 5 `linux` / 7 `linux-arm64` / 5 `macos` to **9 / 7 / 1**, plus
the dedicated `macOS energy backends` job. The dev Mac drops from 13 contexts
to 9; jetson-hub rises from 5 to 9. macOS is still exercised, by the workspace
that actually depends on it and by the energy job whose whole purpose is the
platform-gated meters.

If one of the four does not build on x86_64, that is a finding rather than a
regression: it would mean a workspace only built on macOS without saying so.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-10-05 12:18:21 -04:00

367 lines
19 KiB
YAML

name: standards workspaces build + test
# Build gate for every Rust workspace in the repo.
#
# It started as three. Then a toolchain bump compiled the rest for the first
# time and found FIVE more that did not build — smart-byte (47k LOC), openpay
# (six separate breakages across five crates), eoc, mesh and joule-ui — all
# broken by the same handful of upstream migrations (hmac -> KeyInit, reqwest
# feature-gating, cipher 0.4 -> 0.5, a struct gaining fields), and all invisible
# because nothing ever compiled them. Partial coverage is how that happens, so
# the matrix now covers everything.
#
# The original case, still the clearest: smart-byte-rs had never been compiled by
# anything, and it did not build. A single wrong `rand` pin — 0.10 against code
# written for 0.8, whose crypto stack requires rand_core 0.6 — silently disabled
# the entire identity pillar, BBS+ included. `cargo check` would have caught it
# on day one. An unbuilt standard is not a standard, so this workflow is the
# floor under every claim the specs make.
#
# One workflow with a matrix rather than sixteen files: the bootstrap step is
# identical everywhere, and a shared dependency change should rebuild all of
# them anyway. `fail-fast: false` so one red workspace does not mask the others.
# One run per branch at a time. A push supersedes the run before it.
#
# There is one `macos` runner and it is the dev Mac, so every macOS job in this
# repo queues on it. With no concurrency group, a session pushing four versions
# of a branch queued four complete sets of runs and every one of them had to
# drain: on 2026-10-05 a PR's 21 contexts settled none in 70 minutes behind a
# backlog of 25 runs, and the queue grew faster than it cleared. Only the newest
# commit's verdict is wanted, so the older runs are cancelled.
#
# Verified on this forge (Forgejo 16.0.5) in `ci-test` before being added here:
# the key parses, and a newer push moves an older run to `cancelled` even while
# it is still queued, which is the case that matters.
#
# `cancel-in-progress: true` also applies to `main`. That is deliberate and
# safe here: main advances by merge commits whose tree contains the commits
# before them, so the newer run covers the older one's changes.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [main]
pull_request:
# Deliberately NO `paths:` filter.
#
# The first attempt used `**/*.rs` and `**/Cargo.toml`, and the workflow then
# silently did not fire — pushes touching exactly those files triggered other
# workflows but not this one, so `**`-prefixed globs evidently do not match here
# the way they do on GitHub. A build gate that quietly fails to run is worse than
# no gate at all: it reports nothing and everyone assumes green.
#
# This is the repo-wide floor, so it runs on every push and every PR. The cost is
# CI minutes on doc-only changes. The alternative cost is what this branch spent
# a day fixing — five workspaces silently broken because nothing compiled them.
jobs:
build:
name: ${{ matrix.name }}
# Spread across the fleet's runners rather than pinning every job to `linux`.
# For most of these the runner is a capacity choice, not a correctness one —
# and 17 jobs on a single runner queue behind whatever else is using it.
# `jouleclaw-rs` is the exception: see its matrix entry.
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- { name: jcp-rs, dir: jcp/jcp-rs, runner: linux, conformance: jcp-conformance, feature_tests: "-p jcp-energy --features nvml" }
- { name: sandbox-rs, dir: sandbox/sandbox-rs, runner: linux, conformance: sandbox-conformance }
- { name: smart-byte-rs, dir: smart-byte/smart-byte-rs, runner: linux }
- { name: openpay, dir: openpay, runner: linux-arm64 }
- { name: joule-code-rs, dir: joule-code/joule-code-rs, runner: linux-arm64, z3: true }
- { name: map-rs, dir: map/map-rs, runner: linux-arm64, conformance: map-conformance }
- { name: aac-rs, dir: aac/aac-rs, runner: linux-arm64, conformance: aac-conformance }
- { name: joulecontract-rs, dir: joulecontract/joulecontract-rs, runner: linux-arm64, z3: true }
# Runner placement is by what a workspace actually needs, not by
# habit. The dev Mac hosts BOTH the `macos` runner and the
# `linux-arm64` colima VM, so it served 13 of this repo's 21
# contexts while jetson-hub (`linux`) sat idle — which is why a PR's
# macOS contexts could wait indefinitely while its `linux` ones
# returned in minutes.
#
# `arl-rs`, `sense-rs`, `joulehook-rs` and `joule-ui-rs` moved to
# `linux` on that basis: each has zero `target_os = "macos"` cfgs,
# zero Apple dependencies, and zero Apple crates in its Cargo.lock,
# so running them on macOS asserted nothing the repo relies on.
# `jouledesk-rs` stays, because it does need it — `libwebrtc` and 19
# Apple crates in its lockfile. `linux-arm64` placements stay too:
# arm64 is the deployment target, and `jouleclaw-rs` needs
# `+fp16`.
- { name: jouledesk-rs, dir: jouledesk/jouledesk-rs, runner: macos }
- { name: arl-rs, dir: arl/arl-rs, runner: linux }
- { name: sense-rs, dir: sense/sense-rs, runner: linux }
- { name: proof-rs, dir: proof/proof-rs, runner: linux, z3: true, conformance: proof-conformance }
- { name: joulehook-rs, dir: joulehook/joulehook-rs, runner: linux }
- { name: eoc-rs, dir: eoc/eoc-rs, runner: linux, feature_tests: "-p eoc-meter --features cuda" }
- { name: mesh-rs, dir: mesh/mesh-rs, runner: linux-arm64 }
- { name: joule-ui-rs, dir: joule-ui/joule-ui-rs, runner: linux }
# jouleclaw-rs is the one entry whose runner IS a correctness choice.
# It reaches candle → ug → gemm, which uses inline aarch64 assembly
# (`fmla {0:v}.8h, ...`) requiring the ARMv8.2 FP16 extension.
# `aarch64-apple-darwin` carries target_feature="fp16" in its baseline
# because Apple Silicon guarantees it; `aarch64-unknown-linux-gnu`
# baselines at ARMv8.0-A and does not, so the same asm fails to
# ASSEMBLE — a codegen error `cargo check` never reaches. Verified on a
# native arm64 Linux container (rustc 1.98.0): without the flag, 12
# errors; with it, 4457 tests pass.
#
# CAPACITY: this is by far the largest entry — its target tree is
# ~16 GB, against a 120 GB volume on tx-dev-linux-arm64 that already
# holds ~74 GB of other workspaces' caches. If linux-arm64 jobs start
# failing for no apparent reason, check `df -h /mnt/lima-colima-txarm`
# on the txarm VM before suspecting the code: a full cache volume
# fails whatever happens to be building, not whatever changed.
- { name: jouleclaw-rs, dir: jouleclaw/jouleclaw-rs, runner: linux-arm64, rustflags: "-C target-feature=+fp16", cache_suffix: "-nodebug", feature_tests: "-p jouleclaw-energy --features rapl,nvml" }
steps:
- uses: actions/checkout@v5
# Three workspaces shell out to `z3`, and the runner image has no solver.
#
# This step used to be load-bearing for correctness: without a solver the
# ladder escalated past its real rungs into a model STAND-IN that reported
# Proved, so every goal on a solverless machine came back proved. That is
# now fixed at the source — the real ladders hold decision procedures only
# and an absent rung yields Unproved (proof-solvers `ladder_is_sound` and
# `ladder_never_fabricates_a_proof_when_a_rung_is_absent`).
#
# So the suites pass with or without z3, and this step is kept purely for
# COVERAGE: with a solver present the real T1 rung actually runs, and the
# SMT paths are exercised rather than skipped. The absent-rung semantics
# stay covered either way — that regression test uses a goal z3 cannot
# parse, so it asserts the same thing on a machine that has one.
#
# joule-code-remediate's formal gate is the third. Its tests assert in both
# branches, so they pass without a solver, and JOULE_FIX_EXPECT_Z3=1 below
# makes a missing z3 fail them where this step installed one. Otherwise CI
# would only ever run the no-solver branch.
#
# The z3 jobs are pinned to Linux runners deliberately: the macOS runner is
# a host executor with no apt. `sudo` may or may not exist depending on
# whether the container runs as root, so fall back to a bare invocation.
- name: Install z3
if: matrix.z3
shell: bash
run: |
SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo"
$SUDO apt-get update -qq
$SUDO apt-get install -y -qq z3
z3 --version
# The docker runner image ships no Rust at all, so rustup is bootstrapped
# here. dtolnay/rust-toolchain cannot be used: it shells out to `rustup`,
# which the image lacks. Harmless where cargo already exists.
- name: Install Rust (workspace pin)
shell: bash
run: |
export PATH="$HOME/.cargo/bin:$PATH"
command -v rustup >/dev/null 2>&1 || \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
# jouleclaw-rs is the CAPACITY entry above. Linux embeds full DWARF in every
# test binary, so with default debuginfo its aarch64 test target outgrows
# the ~15 GB left on the shared txarm volume and the link fails with
# "No space left on device" (reproduced with the same toolchain, flags and
# image). Without debuginfo the same target is ~6 GB and the whole suite
# passes. Panic messages keep their file:line: that comes from the panic
# location, not from DWARF.
#
# Set through $GITHUB_ENV rather than a matrix field on purpose: a matrix
# field would hand every other job CARGO_PROFILE_DEV_DEBUG="", which cargo
# can reject. `cache_suffix` gives this job a fresh cache namespace so it
# cannot restore a full-debuginfo target and hold both builds at once;
# for every other job the suffix is empty and its key string is unchanged.
- name: Trim debuginfo (arm64 jouleclaw)
if: matrix.name == 'jouleclaw-rs'
shell: bash
run: echo "CARGO_PROFILE_DEV_DEBUG=0" >> "$GITHUB_ENV"
- name: Cache cargo registry + target
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
${{ matrix.dir }}/target
key: standards-${{ matrix.name }}${{ matrix.cache_suffix }}-${{ hashFiles(format('{0}/Cargo.lock', matrix.dir), 'rust-toolchain.toml') }}
restore-keys: |
standards-${{ matrix.name }}${{ matrix.cache_suffix }}-
# --all-targets is load-bearing: a plain `cargo check` does not build test
# targets, and smart-byte's dev-dependencies drifted independently of its
# normal ones (rand_chacha 0.10 against rand 0.8). The library compiled
# while the tests did not.
- name: Check (all targets)
working-directory: ${{ matrix.dir }}
env:
RUSTFLAGS: ${{ matrix.rustflags }}
run: cargo check --workspace --all-targets
- name: Test
working-directory: ${{ matrix.dir }}
env:
RUSTFLAGS: ${{ matrix.rustflags }}
# Where z3 was installed above, its absence is a failure, not a skip.
JOULE_FIX_EXPECT_Z3: ${{ matrix.z3 && '1' || '' }}
run: cargo test --workspace
# The NVML backends sit behind features no workspace member enables
# (jcp-energy `nvml`, eoc-meter `cuda`, jouleclaw-energy `nvml`), so the
# step above never compiles them, let alone runs their tests. Here they
# build and their tests run. The container has no GPU and Docker masks
# powercap, so the hardware tests take their "unavailable, and the
# oracle agrees" branch; the rule's unit tests run in full.
- name: Feature-gated energy tests
if: matrix.feature_tests
working-directory: ${{ matrix.dir }}
env:
RUSTFLAGS: ${{ matrix.rustflags }}
run: cargo test ${{ matrix.feature_tests }}
# Run each standard's conformance suite.
#
# These suites live in a binary's `main()`, not in `#[test]`, so
# `cargo test` never executes them — verified, not assumed:
# `cargo test -p aac-conformance` runs **0 tests** while
# `cargo run -p aac-conformance` runs **40 checks**; map is 1 test vs 15
# checks. Same shape for jcp, proof and sandbox. The suites include
# negative vectors (`C-neg`, `Cneg`), which are the ones that catch a
# verifier ACCEPTING what it must reject.
#
# This is the `main()` instance of the same bug as a feature-gated test:
# the code compiles under `--all-targets`, reports nothing, and looks
# covered. Compiling a suite is not running it.
#
# `--bin` is explicit because these crates also ship a `gen-vectors`
# binary; `cargo run -p <crate>` is ambiguous with two binaries and fails
# rather than choosing — which is how this step would silently never run.
#
# No path argument: these binaries take none. They check the
# implementation against in-code expectations, which is a different claim
# from validating the published `conformance/v1` files on disk — nothing
# in CI does that yet, and this step does not pretend to.
- name: Conformance suite
if: matrix.conformance
working-directory: ${{ matrix.dir }}
env:
RUSTFLAGS: ${{ matrix.rustflags }}
run: cargo run -q -p ${{ matrix.conformance }} --bin ${{ matrix.conformance }}
# ── The macOS energy backends are exercised NOWHERE otherwise ─────────────
#
# Five crates carry an Apple Silicon IOReport energy meter: jcp-energy,
# proof-cascade, eoc-meter, jouleclaw-energy and sandbox-meter. Each is
# behind `#[cfg(target_os = "macos")]` (jouleclaw-energy also behind its
# `ioreport` feature), and every workspace holding one is pinned to a linux
# runner above. A platform-gated test on a single-platform runner is zero
# coverage, and it is indistinguishable from passing.
#
# What this job checks, and why it used to flake. On macOS 27 an unprivileged
# reader does not see the IOReport "Energy Model" CPU rails (`CPU Energy`,
# `DIE_<n>_CPU Energy`) move under load. They are republished only in rare
# lumps: on the M3 Ultra both dies held for over 22 minutes, then jumped
# about 1.3 kJ each in a single 100 ms sample. On the M5 Max the GPU rail
# stays live. The old jcp-energy test asserted that the *summed* rails
# advanced during a CPU burn, and the GPU rail satisfied that whenever
# something drew on screen. It passed by day and failed at night. The
# 2026-09-19 zero that "healed" by 09-21 was this effect, not a recovery.
#
# Every meter now uses IOReport only when the CPU rail itself keeps
# advancing while the probe burns CPU: in at least 5 sample intervals, at
# least 4 in every 5, over at least 300 ms. A lump is published in steps (one
# was caught as +61.8 kJ, then +3.4 J 64 ms later), so two advancing
# intervals are still just a lump. Otherwise the meter falls back to its
# estimator rung. Its tests assert against an independent oracle that reads
# the CPU rail directly, so this job is deterministic:
# - green while each meter declines a rail that does not keep moving;
# - red if any meter selects one;
# - on a host whose rail is live, red unless CPU work meters non-zero.
#
# Each step fails unless its test printed the `ioreport liveness:` line. That
# line is the only proof the platform-gated test ran and asserted a branch,
# rather than a filter, a feature or a cfg leaving zero tests to pass. It goes
# to stderr and is captured on success, hence `--nocapture` and `2>&1`.
macos-energy-backend:
name: macOS energy backends
runs-on: macos
steps:
- uses: actions/checkout@v4
- name: Install Rust (workspace pin)
shell: bash
run: |
export PATH="$HOME/.cargo/bin:$PATH"
command -v rustup >/dev/null 2>&1 || \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Cache cargo registry + targets
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
jcp/jcp-rs/target
proof/proof-rs/target
eoc/eoc-rs/target
jouleclaw/jouleclaw-rs/target
sandbox/sandbox-rs/target
key: standards-macos-energy-${{ hashFiles('jcp/jcp-rs/Cargo.lock', 'proof/proof-rs/Cargo.lock', 'eoc/eoc-rs/Cargo.lock', 'jouleclaw/jouleclaw-rs/Cargo.lock', 'sandbox/sandbox-rs/Cargo.lock', 'rust-toolchain.toml') }}
restore-keys: |
standards-macos-energy-
# Each crate's lib tests plus its integration tests. Each crate has a
# tests/ file that checks the liveness probe runs exactly once per process,
# and that check only holds in a fresh process. `--lib` would skip it.
- name: jcp-energy
shell: bash
working-directory: jcp/jcp-rs
run: |
set -o pipefail
log=$(mktemp)
cargo test -p jcp-energy -- --nocapture 2>&1 | tee "$log"
grep -q 'ioreport liveness:' "$log"
- name: proof-cascade
shell: bash
working-directory: proof/proof-rs
run: |
set -o pipefail
log=$(mktemp)
cargo test -p proof-cascade -- --nocapture 2>&1 | tee "$log"
grep -q 'ioreport liveness:' "$log"
- name: eoc-meter
shell: bash
working-directory: eoc/eoc-rs
run: |
set -o pipefail
log=$(mktemp)
cargo test -p eoc-meter -- --nocapture 2>&1 | tee "$log"
grep -q 'ioreport liveness:' "$log"
# Double-gated: `cfg(all(feature = "ioreport", target_os = "macos"))`.
# A macOS runner alone reaches none of it.
- name: jouleclaw-energy (feature AND platform gated)
shell: bash
working-directory: jouleclaw/jouleclaw-rs
run: |
set -o pipefail
log=$(mktemp)
cargo test -p jouleclaw-energy --features ioreport -- --nocapture 2>&1 | tee "$log"
grep -q 'ioreport liveness:' "$log"
- name: sandbox-meter
shell: bash
working-directory: sandbox/sandbox-rs
run: |
set -o pipefail
log=$(mktemp)
cargo test -p sandbox-meter -- --nocapture 2>&1 | tee "$log"
grep -q 'ioreport liveness:' "$log"