Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,12 @@ jobs:
flake:
name: Flake
runs-on: ubuntu-latest
# A runner-level guard, not a target. The gate builds every check serially
# from a cold hosted store; the bedwars binary alone measured 620 to 670
# seconds across four runs on 2026-07-28 and everything else is minutes.
# A runner-level guard, not a target. The gate realises every check from a
# cold hosted store in one `nix build --keep-going`, so nix schedules them
# up to `max-jobs` (auto, four here) rather than one at a time. The floor is
# the release compile plus the longest single gate behind it: on run
# 30500640846 that was roughly 500 seconds of crate graph and then
# `smash-e2e` at 639.
timeout-minutes: 90
steps:
- uses: actions/checkout@v4
Expand Down
76 changes: 71 additions & 5 deletions nix/ci/flake-gate.nix
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,75 @@ let
${reasonArms}esac
}

build() {
nix build --accept-flake-config --no-link --print-build-logs \
"$flake#checks.${system}.$1"
# One `nix build` for the whole set, rather than one per name.
#
# The checks ran strictly one after another and nothing about a check
# needs the machine to itself, so all the serialisation bought was wall
# clock. Measured on GitHub Actions run 30500640846: 67 checks, 2102
# seconds, of which `smash-e2e` was 639 and `bedwars-bow-e2e` was 546.
# The other 65 queued behind those two, and 55 of the 67 were under 20
# seconds each.
#
# Concurrency is nix's own `max-jobs` and is deliberately not pinned
# here. `nix-installer-action` writes `max-jobs = auto`, so on the hosted
# runner it is the core count -- four, which the hud gate reads back off
# the same machine as `CPU 2% of 400%`. A number written into this script
# would be that machine's number on everybody else's. It also does not
# raise the ceiling: one `nix build` of one e2e check already scheduled
# its crate units four wide, so four is what the compile phase ran at
# before this. What changes is that whole checks overlap, and an e2e gate
# spends its time waiting on a server to boot rather than on a core.
#
# `--keep-going` is load bearing. Without it the first failed check
# abandons the rest, and every name after it would report as failed
# having never been attempted -- the opposite of the complete report the
# loops below exist to produce.
#
# The cost, named: an evaluation error in any one check now aborts the
# command before anything builds, so every name reports FAILED where only
# the broken one used to. nix prints the eval error with the offending
# attribute in it immediately above the report, which is where to look
# when every name goes red at once.
installables=()
for name in "''${enforced[@]}" "''${excluded[@]}"; do
installables+=("$flake#checks.${system}.$name")
done

# `--print-build-logs` prefixes every line with the derivation that
# emitted it, which is what keeps interleaved logs attributable. The
# prefix is the derivation name and not the check name, so `smash-e2e`
# reads as `hyperion-smash-e2e`.
#
# The exit status is discarded on purpose: it says only that something
# failed, and which checks failed is the question the loops below answer
# per name. It is also the expected status whenever `excluded` is
# non-empty.
nix build --accept-flake-config --no-link --print-build-logs --keep-going \
"''${installables[@]}" || true

# Did this check pass? Asked of the store, never by building it again.
#
# `--max-jobs 0` forbids starting a local build, so this exits 0 exactly
# when the output is already realised and fails otherwise; `--builders ""`
# closes the same door for a contributor whose nix.conf names a remote
# builder. A plain `nix build` per name would instead rebuild every check
# that just failed, because a failed derivation is not in the store, and
# the slowest thing in the job would be paid for twice.
#
# Asked of nix rather than by comparing `.outPath` against the store,
# because cargoUnit content-addresses every crate unit and a derivation
# downstream of a content-addressed one has a deferred output path.
# `nix eval .#checks.${system}.smash-e2e.outPath` answers with a
# placeholder rather than a store path for 14 of the 67 checks, and those
# 14 are exactly the e2e gates.
#
# Output is dropped because the only thing on the failing path is nix
# explaining that it will not build with max-jobs 0, which is the answer
# rather than a problem. The build above is where a check says why it
# failed.
realised() {
nix build --accept-flake-config --no-link --max-jobs 0 --builders "" \
"$flake#checks.${system}.$1" > /dev/null 2>&1
}

status=0
Expand All @@ -112,7 +178,7 @@ let
# Every failure is reported rather than the first one only, so a single
# push can fix all of them.
for name in "''${enforced[@]}"; do
if build "$name"; then
if realised "$name"; then
echo "ok $name"
else
echo "FAILED $name"
Expand All @@ -122,7 +188,7 @@ let
done

for name in "''${excluded[@]}"; do
if build "$name"; then
if realised "$name"; then
echo "STALE $name"
{
echo "checks.${system}.$name is excluded from CI but now builds."
Expand Down
Loading