Skip to content

Commit b50e0a9

Browse files
authored
ci: staged per-arch pipelines — deps→cache→build→fan-out on one hot runner (#78)
* perf(ci): lambda-tests follows the prod arch The lambda binary ships and runs on aarch64 only; running its server-off test config on the x86 lane too bought a duplicate workspace compile+test (~3-4 min now that the checks actually run tests) without covering anything the aarch64-linux and darwin lanes don't. Chaining it into nextest instead was measured and rejected: feature unification with dev-deps makes the test-profile lib a distinct compilation unit (699/699 tests pass either way, but zero compile reuse — 229s chained vs 233s unchained locally). * ci: staged per-arch pipelines — deps first, cache, build, fan out on the hot runner Each arch runs one pipeline: build the crane dependency closures (crate deps; wasm toolchain on arm) and publish the binary cache IMMEDIATELY, so a failure in any later stage never costs the next run its warm deps and every branch benefits from the first build; then the source-keyed build/test/lint stage; then the cheap fan-out — image smoke and the node addon — as steps on the same already-hot runner instead of fresh runners each paying installer+eval+restore. Arch-independent checks (clippy/fmt/no-node-spawn/wasm) move to the arm lane (~1.8x faster per stage), rebalancing the two critical paths. The release workflow stops saving the cache: the deps stage is the single publisher, so entries hold exactly the reusable closure and no source-keyed artifacts. buildId untouched (checks and workflows are outside its hash set). * ci: bound check parallelism (OOM) and strip ANSI in the ran-tests guard The rebalanced arm lane's unbounded derivation parallelism OOM-killed the 16 GB runner (exit 143); --max-jobs 2 bounds memory with cores saturated. The zero-tests guard could never match a CI log: CARGO_TERM_COLOR=always wraps the nextest summary in escapes — strip before grepping. * ci: ran-tests guard must not use grep -q under pipefail -q exits on first match, SIGPIPEs sed, and pipefail converts the successful match into a failure — it only fired when matches occur early in a long log (lambda-tests), which is why nextest passed the same guard.
1 parent 7e71572 commit b50e0a9

4 files changed

Lines changed: 84 additions & 58 deletions

File tree

.github/workflows/ci.yml

Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -124,22 +124,57 @@ jobs:
124124
path: .win-green-marker
125125
key: windows-green-${{ needs.windows-gate.outputs.hash }}
126126

127-
node-addon:
128-
name: node addon
129-
if: github.event_name != 'push'
130-
runs-on: ubuntu-latest
131-
timeout-minutes: 20
127+
# One staged pipeline per arch, everything on a single runner: deps are
128+
# built and published to the binary cache FIRST (a later stage failing
129+
# never costs the next run its warm deps), then the source-keyed
130+
# build/test/lint work, then the cheap fan-out (image, node addon) reuses
131+
# the already-hot store instead of paying setup+restore on fresh runners.
132+
pipeline:
133+
name: ${{ matrix.system }} pipeline
134+
strategy:
135+
fail-fast: false
136+
matrix:
137+
include:
138+
- runner: ubuntu-24.04
139+
system: x86_64-linux
140+
deps: .#checks.x86_64-linux.deps
141+
image: dockerImage
142+
- runner: ubuntu-24.04-arm
143+
system: aarch64-linux
144+
deps: .#checks.aarch64-linux.deps .#checks.aarch64-linux.wasm-deps
145+
image: lambdaImage
146+
runs-on: ${{ matrix.runner }}
147+
timeout-minutes: 60
132148
steps:
133149
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
150+
# No standalone nix installer: the composite runs it with the
151+
# extra-conf this job needs; a prior bare install made that conf
152+
# silently a no-op.
153+
- uses: ./.github/actions/nix-store-cache
154+
- name: deps (dependency closures)
155+
run: nix build --no-link ${{ matrix.deps }}
156+
# Publish immediately and from any branch: the composite no-ops
157+
# unless the exact key is new, so this fires once per input rotation.
158+
- uses: ./.github/actions/nix-store-cache/save
159+
continue-on-error: true
160+
- name: build + checks
161+
run: bash ci/nix-checks.sh ${{ matrix.system }}
162+
# Fan-out: cheap on the hot store. Skipped on main pushes, where the
163+
# release workflow builds the real thing from the same cache.
164+
- name: image smoke (${{ matrix.image }})
165+
if: github.event_name != 'push'
166+
run: nix build --no-link .#${{ matrix.image }}
134167
- uses: ./.github/actions/rust-setup
168+
if: github.event_name != 'push'
135169
with:
136-
# Pure consumer of the cache the release napi linux leg saves from
137-
# main pushes (same shared-key); this job is PR-only, so its own
138-
# saves would be PR-scoped quota waste.
170+
# Pure consumer of the cache the release napi linux legs save from
171+
# main pushes (same shared-key); PR-scoped saves would be quota
172+
# waste.
139173
workspaces: crate/abgen-node
140174
shared-key: abgen-node
141175
save-if: "false"
142-
- name: abgen-node (build + smoke)
176+
- name: node addon (build + smoke)
177+
if: github.event_name != 'push'
143178
working-directory: crate/abgen-node
144179
run: |
145180
npm ci --no-audit --no-fund
@@ -153,29 +188,3 @@ jobs:
153188
bash ../../ci/check-glibc-floor.sh 2.34 \
154189
target/release/libabgen_node.so
155190
156-
nix-ci:
157-
name: nix checks
158-
strategy:
159-
fail-fast: false
160-
matrix:
161-
runner: [ubuntu-24.04, ubuntu-24.04-arm]
162-
runs-on: ${{ matrix.runner }}
163-
timeout-minutes: 45
164-
steps:
165-
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
166-
# No standalone nix installer: the composite runs it with the
167-
# extra-conf this job needs; a prior bare install made that conf
168-
# silently a no-op.
169-
- uses: ./.github/actions/nix-store-cache
170-
- name: build flake checks
171-
run: |
172-
case "$(uname -m)" in
173-
x86_64) system=x86_64-linux ;;
174-
aarch64) system=aarch64-linux ;;
175-
*) echo "unsupported arch: $(uname -m)" >&2; exit 1 ;;
176-
esac
177-
bash ci/nix-checks.sh "$system"
178-
- if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/')
179-
uses: ./.github/actions/nix-store-cache/save
180-
continue-on-error: true
181-

.github/workflows/release.yml

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -392,10 +392,6 @@ jobs:
392392
cargo build --release --locked --target ${{ matrix.target }} -p abgen-native
393393
fi
394394
395-
- if: matrix.builder == 'nix' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
396-
uses: ./.github/actions/nix-store-cache/save
397-
continue-on-error: true
398-
399395
- name: windows self-containment
400396
if: matrix.target == 'x86_64-pc-windows-gnu'
401397
run: |
@@ -829,9 +825,6 @@ jobs:
829825
echo "REF=${ref}"
830826
echo "IMG_SIZE=$(ls -lLh result | awk '{print $5}')"
831827
} >> "$GITHUB_ENV"
832-
- if: needs.build-id.outputs.image_docker == '' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
833-
uses: ./.github/actions/nix-store-cache/save
834-
continue-on-error: true
835828
- name: upload image artifact (input-addressed)
836829
if: needs.build-id.outputs.image_docker == ''
837830
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
@@ -906,9 +899,6 @@ jobs:
906899
echo "REF=${ref}"
907900
echo "IMG_SIZE=$(ls -lLh result | awk '{print $5}')"
908901
} >> "$GITHUB_ENV"
909-
- if: needs.build-id.outputs.image_lambda == '' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/'))
910-
uses: ./.github/actions/nix-store-cache/save
911-
continue-on-error: true
912902
- name: upload image artifact (input-addressed)
913903
if: needs.build-id.outputs.image_lambda == ''
914904
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a

ci/nix-checks.sh

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,21 @@ done <<<"$names"
1717
printf 'building %s check(s):\n' "${#attrs[@]}"
1818
printf ' %s\n' "${attrs[@]}"
1919

20-
nix build --keep-going --no-link --log-format raw "${attrs[@]}" \
21-
|| nix build --keep-going --no-link --print-build-logs "${attrs[@]}"
20+
# --max-jobs 2: the full check set holds several workspace-scale compiles;
21+
# unbounded derivation parallelism OOM-kills the 16 GB runners. Two at a
22+
# time bounds memory while keeping the cores saturated.
23+
nix build --keep-going --no-link --log-format raw --max-jobs 2 "${attrs[@]}" \
24+
|| nix build --keep-going --no-link --print-build-logs --max-jobs 2 "${attrs[@]}"
2225

2326
# A test check that executed zero tests is a silent no-op (doCheck=false
2427
# reached the derivation once and nobody noticed for weeks) — make that
2528
# state red forever: the built check's log must show a nonzero test count.
2629
assert_ran_tests() {
2730
local attr="$1" pattern="$2"
28-
grep -qE "$pattern" <(nix log "$attr") \
31+
# CARGO_TERM_COLOR=always wraps the summary in ANSI escapes; strip them
32+
# or the pattern can never match a CI log. No grep -q: under pipefail its
33+
# early exit SIGPIPEs sed and fails the pipeline on a successful match.
34+
nix log "$attr" | sed $'s/\x1b\\[[0-9;]*[A-Za-z]//g' | grep -E "$pattern" > /dev/null \
2935
|| { echo "$attr built green but ran no tests ($pattern not in its log)" >&2; exit 1; }
3036
}
3137
while IFS= read -r name; do

nix/checks.nix

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ let
77
abgenRoot = ''export ABGEN_ROOT="$PWD"'';
88

99
archIndependent = {
10+
# The wasm toolchain + registry closure, exposed so the pipeline's deps
11+
# stage can build and cache it before any source-keyed work starts.
12+
wasm-deps = wasmCheck.cargoArtifacts;
13+
1014
fmt = craneLib.cargoFmt {
1115
inherit (commonArgs) pname version src;
1216
};
@@ -25,6 +29,11 @@ let
2529
};
2630

2731
archDependent = {
32+
# The crane dependency closure as a first-class attr: the pipeline's
33+
# deps stage builds exactly this, then publishes the binary cache, so a
34+
# failure in any later stage never costs the next run its warm deps.
35+
deps = cargoArtifacts;
36+
2837
nextest = craneLib.cargoNextest (withArtifacts // {
2938
doCheck = true;
3039
__darwinAllowLocalNetworking = true;
@@ -33,15 +42,6 @@ let
3342
preCheck = abgenRoot;
3443
});
3544

36-
lambda-tests = craneLib.cargoTest (withArtifacts // {
37-
doCheck = true;
38-
__darwinAllowLocalNetworking = true;
39-
pname = "abgen-lambda-tests";
40-
cargoExtraArgs = "--locked";
41-
cargoTestExtraArgs = "-p abgen-lambda -p abgen-native --tests";
42-
preCheck = abgenRoot;
43-
});
44-
4545
native-smoke =
4646
let
4747
libName = "libabgen${pkgs.stdenv.hostPlatform.extensions.sharedLibrary}";
@@ -58,5 +58,26 @@ let
5858
touch $out
5959
'';
6060
};
61+
62+
# server-off config. The lambda binary ships and runs on aarch64 only, so
63+
# its test lane follows the prod arch; the config stays covered fleet-wide
64+
# (aarch64-linux in CI, darwin locally) without a second server-off
65+
# workspace compile+test on the slower x86 lane. Folding it into nextest
66+
# is rejected: feature unification differs, a merged compile would not
67+
# test the no-server config the mac/windows legs ship.
68+
lambdaTests = {
69+
lambda-tests = craneLib.cargoTest (withArtifacts // {
70+
doCheck = true;
71+
__darwinAllowLocalNetworking = true;
72+
pname = "abgen-lambda-tests";
73+
cargoExtraArgs = "--locked";
74+
cargoTestExtraArgs = "-p abgen-lambda -p abgen-native --tests";
75+
preCheck = abgenRoot;
76+
});
77+
};
6178
in
62-
archDependent // lib.optionalAttrs (system == "x86_64-linux") archIndependent
79+
# Arch-independent checks ride the aarch64 lane: those runners are ~1.8x
80+
# faster per stage, which rebalances the two pipelines' critical paths.
81+
archDependent
82+
// lib.optionalAttrs (system != "x86_64-linux") lambdaTests
83+
// lib.optionalAttrs (system == "aarch64-linux") archIndependent

0 commit comments

Comments
 (0)