1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
# Code coverage tracking
# Runs on pushes to main and PRs
#
# #386 — this lane died on main with NO LOGS AT ALL. Steps 1-4 `success`, then
# `Cache cargo`, `Generate coverage` and both post-steps `null`. The only
# surviving evidence was the check-run annotation:
#
# System.IO.IOException: No space left on device :
# '/home/runner/actions-runner/cached/2.336.0/_diag/Worker_20260830-091631-utc.log'
# at GitHub.Runner.Worker.Worker.RunAsync(String pipeIn, String pipeOut)
#
# The runner's own Worker process died because it could not write its own diag
# log — which is why the job has no logs: the process that uploads them is the
# one that was killed.
#
# `cargo llvm-cov` here builds 242 integration test binaries plus the lib and
# bin unit-test binaries, all instrumented. MEASURED with this repo's default
# profile: 70.70 GiB in 19,070 files, 66 GiB of it DWARF in debug/deps. That is
# already the size of a hosted runner's disk, so the build alone sat on the line.
#
# This file then asked to CACHE `target` — a second, compressed copy of that same
# tree, on the same filesystem. It never once fit:
#
# [command]/usr/bin/tar --posix -cf cache.tzst ... --use-compress-program zstdmt
# zstd: error 70 : Write error : cannot write block : No space left on device
# ##[warning]Failed to save: "/usr/bin/tar" failed with error: ... exit code 2
#
# and `actions/cache` downgrades a failed SAVE to a warning, so every green run
# of this lane had already filled the runner's disk and said so in a warning
# nobody reads. Because the save never completed the cache never existed: every
# run whose logs survive reports `Cache not found for input keys`, on
# consecutive runs sharing an identical Cargo.lock hash. Zero restores, ever —
# the step's entire measured contribution was to fill the disk. When the margin
# was tighter the ENOSPC landed mid-build instead and took the Worker with it.
#
# Three changes, none of which weaken the gate:
# 1. Cache the cargo REGISTRY, never the build directory — and do it BEFORE
# the `cargo install` it exists to accelerate (it used to run after, so
# even a working cache could not have helped).
# 2. Build with `debug = line-tables-only`. LLVM emits its coverage mapping
# into __llvm_covmap via -C instrument-coverage; it does not read DWARF, so
# this changes the tree size and NOTHING about the numbers. Proven: the
# TOTAL line is identical either way (see the PR).
# 3. Print the disk denominator before and after, and REFUSE to start without
# headroom — so the next time this runs out it is a legible red with a
# message, not a vaporised runner with no logs.
#
# Guarded by tests/falsification_hosted_jobs_do_not_cache_target.rs, which fails
# if any GitHub-hosted job in this repo names a Rust build directory in
# actions/cache. Self-hosted jobs are exempt: the clean-room runners have real
# disks, and sovereign-ci deliberately mounts a persistent per-PR target dir.
name: Coverage
on:
push:
branches:
pull_request:
branches:
concurrency:
group: coverage-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: true
jobs:
# PMAT-237: what can this change break? The release gate is untouched and
# still runs everything; this decides only what a PR pays for what it
# changed. The decision is scripts/ci/changed-class.sh, driven in both
# directions by tests/falsification_pr_lane_runs_what_the_change_can_break.rs.
# PMAT-237 gates the heavy jobs on whether a change can reach them.
# COVERAGE IS DELIBERATELY NOT AMONG THEM. The 95% line floor is a standing
# property of the repository, not a property of one diff, and the cheapest
# way to keep a floor is to measure it every time rather than to reason about
# when it could not have moved. Twelve minutes of the ~131 a PR used to cost.
coverage:
runs-on: ubuntu-latest
timeout-minutes: 45
env:
# See note 2 above. Scoped to this lane — the repo's profiles are untouched.
CARGO_PROFILE_DEV_DEBUG: line-tables-only
CARGO_PROFILE_TEST_DEBUG: line-tables-only
# The instrumented tree is write-once here; incremental state is pure
# overhead on a cold runner and was 4.3 GiB of the measured 70.70.
CARGO_INCREMENTAL: "0"
# The floor asserted before the build starts, in GiB. The measured
# line-tables-only tree is well under this; the margin is deliberate,
# because the failure mode when it is wrong destroys its own evidence.
MIN_FREE_GIB: "40"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Rust toolchain
run: rustup show
# BEFORE the install it accelerates. Registry only — never `target`.
- name: Cache cargo registry
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: |
~/.cargo/registry/index
~/.cargo/registry/cache
~/.cargo/git/db
key: coverage-cargo-${{ runner.os }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
coverage-cargo-${{ runner.os }}-
- name: Install cargo-llvm-cov
run: cargo install --locked cargo-llvm-cov
# PRINT THE DENOMINATOR, and refuse rather than die silently. An ENOSPC
# here kills the runner's Worker process and takes the logs with it, so a
# run that is going to fail for want of disk must say so while it still
# can. This is a hard failure on purpose: a coverage gate that cannot run
# is not a coverage gate that passed.
- name: Refuse to start without disk headroom
run: |
set -eu
df -h /
avail_kib=$(df -Pk / | awk 'NR==2 {print $4}')
avail_gib=$((avail_kib / 1024 / 1024))
echo "free on /: ${avail_gib} GiB; required: ${MIN_FREE_GIB} GiB"
if [ "${avail_gib}" -lt "${MIN_FREE_GIB}" ]; then
echo "::error::Only ${avail_gib} GiB free on / — this runner cannot" \
"hold the instrumented coverage build (#386). Refusing to start" \
"rather than exhausting the disk mid-build, which kills the" \
"runner Worker and destroys the logs that would explain it."
exit 1
fi
- name: Generate coverage
run: cargo llvm-cov --summary-only --fail-under-lines 95
# The measurement that makes the budget above auditable instead of a
# guess. Runs even when coverage fails, because a failure caused by disk
# and a failure caused by uncovered lines must not look alike.
- name: Disk and build-tree size after coverage
if: always()
run: |
set -u
df -h /
du -sh target/llvm-cov-target 2>/dev/null || echo "no llvm-cov-target"