Skip to content

Commit d7ce789

Browse files
committed
ci: shard the C-suite legs, prove cross-shard coverage, warm compiler caches
PR iteration was bounded by single-job suite runs (~40-50 min critical path) and by every first-of-ref build starting cold. Same suites, same sanitizers, same gates — the schedule, the cache reuse, and a new runtime coverage proof change. - run-tests-parallel.sh accepts CBM_TEST_SHARD="i/N" and runs a deterministic slice of --list-suites; unset selects everything, so the path is inert outside CI. Known-heavy suites are dealt to shards in weight order first (naive modulo stacked the two slowest, store_arch and daemon_runtime, onto one job), the parallel wave and the serial tail are sliced separately so every shard keeps its own quiet tail, and the per-shard union guard proves each job ran exactly its slice. - shard-completeness job: per-shard guards cannot see a mis-plumbed CBM_TEST_SHARD (two jobs running the same slice passes every local check while a slice runs nowhere). Every leg now uploads a manifest (leg, i/N, sha of the full suite list, its slice) and one aggregation job re-proves per leg that the shards agreed on the list, indices are exactly 1..N, and the union of slices IS the list. Runs on unsharded topologies too, where each leg's single manifest must cover the list. - workflow topology (shard_suites input, off by default = byte-identical to the pre-shard matrix): ubuntu legs run 3 shards, Windows 2 (every extra Windows shard re-pays ~5 min of MSYS2 setup), macOS stays at 1 (not the critical path; mac runner concurrency ceilings are the tightest). The Windows test job also pre-creates build\c with a protected DACL so build products inherit it — workspace drive roots grant Authenticated Users Modify by inheritance, which the activation transaction's source-directory policy correctly refuses. - ccache: a final ref-less restore key lets a fresh PR start from the newest cache GitHub's scoping permits, and a nightly build-only cache-warm workflow keeps main-scoped caches at most a day stale — without it the fallback had no warm source, since nothing built in main's scope. The strictly-per-ref policy this replaces cost ~8-12 minutes on every first-of-ref build for no safety gain: CCACHE_COMPILERCHECK=content makes a stale or foreign cache able to miss but never to return wrong output. Sharded legs restore-all/ save-one (every shard builds the identical objects, so shard 1's cache carries the full set) — save volume stays flat and warm caches stop being evicted by per-shard duplicates. - suite timeouts: daemon_runtime joins the slow tier. It measures ~610s solo on arm64 under ASan (10-round loop, no hang), so the 900s default under a loaded 4-job CI runner was a slowness kill masquerading as a hang detector. Validated locally: 3-shard union over the real suite list is complete with no duplicates, one top-heavy suite per shard, and the summed 3-shard totals reproduce the unsharded run exactly (6776 passed, 0 failed, 4 skipped). Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1 parent f1beaec commit d7ce789

3 files changed

Lines changed: 363 additions & 13 deletions

File tree

‎.github/workflows/_test.yml‎

Lines changed: 154 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ on:
1212
description: 'Test the broad platform matrix (older glibc + extra OS versions) instead of the core set'
1313
type: boolean
1414
default: false
15+
shard_suites:
16+
description: 'Split each C-suite leg across parallel shard jobs (ubuntu x3, windows x2); off = one job per leg, byte-identical to the pre-shard topology'
17+
type: boolean
18+
default: false
1519

1620
permissions:
1721
contents: read
@@ -31,6 +35,7 @@ jobs:
3135
id: set
3236
env:
3337
BROAD: ${{ inputs.broad_platforms }}
38+
SHARDS: ${{ inputs.shard_suites }}
3439
run: |
3540
CORE_UNIX='[
3641
{"os":"ubuntu-latest","cc":"gcc","cxx":"g++"},
@@ -63,6 +68,25 @@ jobs:
6368
UNIX=$(jq -cn --argjson a "$CORE_UNIX" '$a')
6469
WIN=$(jq -cn --argjson a "$CORE_WIN" '$a')
6570
fi
71+
# Suite sharding (opt-in via the shard_suites input): each platform's
72+
# C-suite run is split across N runner jobs. CBM_TEST_SHARD="i/N"
73+
# slices deterministically inside run-tests-parallel.sh; each
74+
# shard's union guard proves it ran exactly its slice; shards are
75+
# minted HERE from one expansion so a missing shard cannot be
76+
# configured silently; and the shard-completeness job re-proves the
77+
# cross-shard union at runtime. Ubuntu runners are plentiful (3
78+
# shards); Windows gets 2 (every extra shard re-pays ~5 min of
79+
# MSYS2 setup); macOS stays at 1 — mac legs are not the critical
80+
# path and mac runner concurrency ceilings are the tightest.
81+
if [ "$SHARDS" = "true" ]; then
82+
UNIX=$(jq -c '[.[] | . as $e
83+
| (if ($e.os | startswith("ubuntu")) then 3 else 1 end) as $n
84+
| range(1; $n + 1) as $i
85+
| $e + {shard: "\($i)/\($n)"}]' <<< "$UNIX")
86+
WIN=$(jq -c '[.[] | . as $e
87+
| range(1; 3) as $i
88+
| $e + {shard: "\($i)/2"}]' <<< "$WIN")
89+
fi
6690
echo "unix={\"include\":$UNIX}" >> "$GITHUB_OUTPUT"
6791
echo "windows={\"include\":$WIN}" >> "$GITHUB_OUTPUT"
6892
@@ -91,24 +115,54 @@ jobs:
91115
# on the compiler-binary CONTENT plus the fully preprocessed input, so a
92116
# hit is provably the identical compilation — a stale or foreign cache
93117
# can only miss, never return wrong output (see scripts/env.sh).
94-
# Keys embed github.ref on top of GitHub's own branch scoping: caches are
95-
# STRICTLY per-ref by policy — no base-branch fallback, a new PR builds
96-
# cold once and only its own pushes warm it.
97-
- name: Compiler cache (content-verified)
98-
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
118+
# Keys embed github.ref; the final ref-less restore key falls back to
119+
# the newest cache GitHub's scoping permits (a PR can only ever see its
120+
# own ref and the base branch, and base-branch caches are written only
121+
# by base-branch runs), so a fresh PR starts from main's warm cache
122+
# instead of building cold. Content verification is what makes that
123+
# safe; the fallback buys ~8-12 minutes on every first-of-ref build.
124+
- name: Compiler cache (content-verified, restore)
125+
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
99126
with:
100127
path: ${{ github.workspace }}/.ccache
101128
key: ccache-test-${{ matrix.os }}-${{ matrix.cc }}-${{ github.ref }}-${{ github.sha }}
129+
# The cross-ref fallback is what keeps fresh PRs warm: without it a
130+
# new PR always started from a cold cache (observed 0% hit legs).
131+
# ccache verifies content hashes itself, so a stale cache can only
132+
# miss, never mis-compile.
102133
restore-keys: |
103134
ccache-test-${{ matrix.os }}-${{ matrix.cc }}-${{ github.ref }}-
135+
ccache-test-${{ matrix.os }}-${{ matrix.cc }}-
104136
105137
- name: Test
106138
run: scripts/test.sh CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
107139
env:
140+
CBM_TEST_SHARD: ${{ matrix.shard }}
141+
CBM_TEST_LEG: ${{ matrix.os }}-${{ matrix.cc }}
108142
CBM_SKIP_PERF: ${{ inputs.skip_perf && '1' || '' }}
109143
CCACHE_DIR: ${{ github.workspace }}/.ccache
110144
CCACHE_MAXSIZE: 1500M
111145

146+
# Every shard of a leg builds the identical runner, so one shard's
147+
# cache carries the complete object set: shard 1 (or the unsharded
148+
# job) saves, the rest only restore — same save volume as before
149+
# sharding, no quota thrash, no same-key save races. Saving on failure
150+
# too keeps red iterations warm.
151+
- name: Shard manifest (completeness proof)
152+
if: always()
153+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
154+
with:
155+
name: shard-manifest-${{ matrix.os }}-${{ matrix.cc }}-${{ strategy.job-index }}
156+
path: build/c/test-logs/shard-manifest.txt
157+
if-no-files-found: error
158+
159+
- name: Compiler cache (save)
160+
if: always() && (matrix.shard == null || startsWith(matrix.shard, '1/'))
161+
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
162+
with:
163+
path: ${{ github.workspace }}/.ccache
164+
key: ccache-test-${{ matrix.os }}-${{ matrix.cc }}-${{ github.ref }}-${{ github.sha }}
165+
112166
- name: ccache stats
113167
if: always()
114168
run: ccache -s || true
@@ -152,6 +206,7 @@ jobs:
152206
key: ccache-tsan-${{ matrix.os }}-${{ github.ref }}-${{ github.sha }}
153207
restore-keys: |
154208
ccache-tsan-${{ matrix.os }}-${{ github.ref }}-
209+
ccache-tsan-${{ matrix.os }}-
155210
156211
- name: ThreadSanitizer tests
157212
# Calls make directly (no env.sh), so route the compilers through
@@ -225,14 +280,28 @@ jobs:
225280
$acl.AddAccessRule($rule) | Out-Null
226281
Set-Acl -LiteralPath $root -AclObject $acl
227282
"CBM_CI_TEMP_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
283+
# Pre-create the build directory with the same protected DACL so
284+
# every build product inherits it. The workspace drive root grants
285+
# Authenticated Users Modify by inheritance — a shape real user
286+
# checkouts (under the profile) do not have — and the activation
287+
# transaction's source-directory policy correctly refuses it, which
288+
# would fail the install-flow tests on the environment, not the code.
289+
$build = Join-Path $env:GITHUB_WORKSPACE "build\c"
290+
New-Item -ItemType Directory -Path $build -Force | Out-Null
291+
$bacl = [System.Security.AccessControl.DirectorySecurity]::new()
292+
$bacl.SetOwner($sid)
293+
$bacl.SetAccessRuleProtection($true, $false)
294+
$bacl.AddAccessRule($rule) | Out-Null
295+
Set-Acl -LiteralPath $build -AclObject $bacl
228296
229297
- name: Compiler cache (content-verified)
230-
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
298+
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
231299
with:
232300
path: ${{ github.workspace }}/.ccache
233301
key: ccache-test-${{ matrix.os }}-${{ matrix.msystem }}-${{ github.ref }}-${{ github.sha }}
234302
restore-keys: |
235303
ccache-test-${{ matrix.os }}-${{ matrix.msystem }}-${{ github.ref }}-
304+
ccache-test-${{ matrix.os }}-${{ matrix.msystem }}-
236305
237306
- name: Test
238307
shell: msys2 {0}
@@ -256,6 +325,8 @@ jobs:
256325
export TMPDIR="$(cygpath -u "$CBM_CI_TEMP_ROOT")"
257326
scripts/test.sh CC=clang CXX=clang++ ${{ matrix.os == 'windows-11-arm' && 'SANITIZE=-fsanitize=undefined -fsanitize-trap=undefined -fstack-protector-strong -fno-omit-frame-pointer' || '' }}
258327
env:
328+
CBM_TEST_SHARD: ${{ matrix.shard }}
329+
CBM_TEST_LEG: ${{ matrix.os }}-${{ matrix.msystem }}
259330
CBM_SKIP_PERF: ${{ inputs.skip_perf && '1' || '' }}
260331
CCACHE_DIR: ${{ github.workspace }}/.ccache
261332
CCACHE_MAXSIZE: 1500M
@@ -267,6 +338,82 @@ jobs:
267338
env:
268339
CCACHE_DIR: ${{ github.workspace }}/.ccache
269340

341+
- name: Shard manifest (completeness proof)
342+
if: always()
343+
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
344+
with:
345+
name: shard-manifest-${{ matrix.os }}-${{ matrix.msystem }}-${{ strategy.job-index }}
346+
path: build/c/test-logs/shard-manifest.txt
347+
if-no-files-found: error
348+
349+
# Shard 1 (or the unsharded job) saves the compiler cache; every shard
350+
# builds the identical runner, so one cache carries the full object set.
351+
- name: Compiler cache (save)
352+
if: always() && (matrix.shard == null || startsWith(matrix.shard, '1/'))
353+
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
354+
with:
355+
path: ${{ github.workspace }}/.ccache
356+
key: ccache-test-${{ matrix.os }}-${{ matrix.msystem }}-${{ github.ref }}-${{ github.sha }}
357+
358+
# Cross-shard completeness: re-proves at runtime that each leg's shards
359+
# agreed on the full suite list and that the union of their slices IS that
360+
# list. This is the guard the per-shard union checks cannot provide: a
361+
# mis-plumbed CBM_TEST_SHARD (two jobs running the same slice) passes every
362+
# per-shard guard while a slice runs nowhere — only a cross-shard view
363+
# catches it. Runs and gates on unsharded topologies too (every leg then
364+
# contributes one 1/1 manifest whose slice must equal the full list).
365+
shard-completeness:
366+
needs: [test-unix, test-windows]
367+
if: always()
368+
runs-on: ubuntu-latest
369+
timeout-minutes: 10
370+
steps:
371+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
372+
with:
373+
pattern: shard-manifest-*
374+
path: manifests
375+
376+
- name: Verify cross-shard union per leg
377+
run: |
378+
set -eu
379+
files=$(find manifests -name shard-manifest.txt | sort)
380+
if [ -z "$files" ]; then
381+
echo "FAIL: no shard manifests were uploaded" >&2
382+
exit 1
383+
fi
384+
rc=0
385+
for leg in $(grep -h '^leg=' $files | sort -u | sed 's/^leg=//'); do
386+
leg_files=$(grep -l "^leg=$leg\$" $files)
387+
n=$(grep -h '^shard=' $leg_files | sed 's|.*/||' | sort -u)
388+
if [ "$(printf '%s\n' "$n" | wc -l)" -ne 1 ]; then
389+
echo "FAIL: $leg shards disagree on shard count: $n" >&2
390+
rc=1
391+
continue
392+
fi
393+
indices=$(grep -h '^shard=' $leg_files | sed 's/^shard=//;s|/.*||' | sort -n)
394+
if [ "$indices" != "$(seq 1 "$n")" ]; then
395+
echo "FAIL: $leg shard indices [$indices] != 1..$n" >&2
396+
rc=1
397+
continue
398+
fi
399+
list_sha=$(grep -h '^list_sha256=' $leg_files | sed 's/^list_sha256=//' | sort -u)
400+
if [ "$(printf '%s\n' "$list_sha" | wc -l)" -ne 1 ]; then
401+
echo "FAIL: $leg shards saw different suite lists" >&2
402+
rc=1
403+
continue
404+
fi
405+
union_sha=$(for f in $leg_files; do
406+
sed -n '/^--- slice ---$/,$p' "$f" | tail -n +2
407+
done | sort | sha256sum | awk '{print $1}')
408+
if [ "$union_sha" != "$list_sha" ]; then
409+
echo "FAIL: $leg union of shard slices != full suite list (GATE-QUALITY LOSS)" >&2
410+
rc=1
411+
continue
412+
fi
413+
echo "OK: $leg — $n shard(s), union of slices == full suite list"
414+
done
415+
exit "$rc"
416+
270417
# Windows product-surface regression guards. Distinct from test-windows above
271418
# (the sanitizer C suite): these drive a real product binary + embedded HTTP UI
272419
# over stdio / CLI / HTTP and fail if a Windows bug already fixed on main comes
@@ -307,6 +454,7 @@ jobs:
307454
key: ccache-guards-${{ github.ref }}-${{ github.sha }}
308455
restore-keys: |
309456
ccache-guards-${{ github.ref }}-
457+
ccache-guards-
310458
311459
- name: Build product binary with embedded UI
312460
shell: msys2 {0}

‎.github/workflows/cache-warm.yml‎

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Nightly compiler-cache warming for main's scope. GitHub cache isolation
2+
# lets a PR restore only caches written by its own ref or the base branch —
3+
# and nothing used to write test caches in main's scope (the test workflow
4+
# runs on pull_request and workflow_dispatch only), so the cross-ref
5+
# fallback restore keys in _test.yml had no warm source and every fresh PR
6+
# built cold. This build-only job (no tests) keeps main-scoped caches at
7+
# most a day stale; CCACHE_COMPILERCHECK=content turns staleness into
8+
# partial misses, never wrong output. The build commands and ccache
9+
# environment mirror scripts/test.sh's build phase exactly — a warm entry
10+
# only hits when flags and compiler content match, so fidelity here is what
11+
# makes the cache useful.
12+
name: Cache warm
13+
14+
on:
15+
schedule:
16+
- cron: '17 2 * * *'
17+
workflow_dispatch: {}
18+
19+
permissions:
20+
contents: read
21+
22+
jobs:
23+
warm-unix:
24+
strategy:
25+
fail-fast: false
26+
matrix:
27+
include:
28+
- { os: ubuntu-latest, cc: gcc, cxx: g++ }
29+
- { os: ubuntu-24.04-arm, cc: gcc, cxx: g++ }
30+
- { os: macos-14, cc: cc, cxx: c++ }
31+
- { os: macos-15-intel, cc: cc, cxx: c++ }
32+
runs-on: ${{ matrix.os }}
33+
timeout-minutes: 60
34+
steps:
35+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
36+
37+
- name: Install deps (Ubuntu)
38+
if: startsWith(matrix.os, 'ubuntu')
39+
run: sudo apt-get update && sudo apt-get install -y zlib1g-dev ccache
40+
41+
- name: Install ccache (macOS)
42+
if: startsWith(matrix.os, 'macos')
43+
run: command -v ccache >/dev/null 2>&1 || brew install ccache
44+
45+
- name: Compiler cache (restore)
46+
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
47+
with:
48+
path: ${{ github.workspace }}/.ccache
49+
key: ccache-test-${{ matrix.os }}-${{ matrix.cc }}-${{ github.ref }}-${{ github.sha }}
50+
restore-keys: |
51+
ccache-test-${{ matrix.os }}-${{ matrix.cc }}-${{ github.ref }}-
52+
ccache-test-${{ matrix.os }}-${{ matrix.cc }}-
53+
54+
- name: Build (test runner + product, no tests)
55+
run: |
56+
source scripts/env.sh
57+
make -j"$(getconf _NPROCESSORS_ONLN)" -f Makefile.cbm \
58+
build/c/test-runner cbm CC=${{ matrix.cc }} CXX=${{ matrix.cxx }}
59+
env:
60+
CCACHE_DIR: ${{ github.workspace }}/.ccache
61+
CCACHE_MAXSIZE: 1500M
62+
63+
- name: ccache stats
64+
run: ccache -sv || ccache -s
65+
env:
66+
CCACHE_DIR: ${{ github.workspace }}/.ccache
67+
68+
- name: Compiler cache (save)
69+
if: always()
70+
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
71+
with:
72+
path: ${{ github.workspace }}/.ccache
73+
key: ccache-test-${{ matrix.os }}-${{ matrix.cc }}-${{ github.ref }}-${{ github.sha }}
74+
75+
warm-windows:
76+
runs-on: windows-latest
77+
timeout-minutes: 90
78+
defaults:
79+
run:
80+
shell: msys2 {0}
81+
steps:
82+
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
83+
84+
- uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2
85+
with:
86+
msystem: CLANG64
87+
path-type: inherit
88+
install: >-
89+
mingw-w64-clang-x86_64-clang
90+
mingw-w64-clang-x86_64-compiler-rt
91+
mingw-w64-clang-x86_64-zlib
92+
mingw-w64-clang-x86_64-ccache
93+
make
94+
git
95+
96+
- name: Compiler cache (restore)
97+
uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
98+
with:
99+
path: ${{ github.workspace }}/.ccache
100+
key: ccache-test-windows-latest-CLANG64-${{ github.ref }}-${{ github.sha }}
101+
restore-keys: |
102+
ccache-test-windows-latest-CLANG64-${{ github.ref }}-
103+
ccache-test-windows-latest-CLANG64-
104+
105+
- name: Build (test runner + product, no tests)
106+
run: |
107+
source scripts/env.sh
108+
make -j"$(nproc)" -f Makefile.cbm build/c/test-runner cbm CC=clang CXX=clang++
109+
env:
110+
CCACHE_DIR: ${{ github.workspace }}/.ccache
111+
CCACHE_MAXSIZE: 1500M
112+
113+
- name: ccache stats
114+
run: ccache -sv || ccache -s
115+
env:
116+
CCACHE_DIR: ${{ github.workspace }}/.ccache
117+
118+
- name: Compiler cache (save)
119+
if: always()
120+
uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
121+
with:
122+
path: ${{ github.workspace }}/.ccache
123+
key: ccache-test-windows-latest-CLANG64-${{ github.ref }}-${{ github.sha }}

0 commit comments

Comments
 (0)