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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
name: CI
permissions:
contents: read
checks: write
id-token: write
on:
# Integration branches only: a branch that also has a PR would otherwise run
# the whole matrix twice for one commit.
push:
branches:
pull_request:
branches:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
# rust-cache is the only cache layer here. GHA-backed sccache used to sit in
# front of it and measured a 2-8% hit rate while holding 38% of the
# repository's 10GB Actions cache quota, so its main effect was evicting the
# rust-cache tarballs that do work and putting every job back on a cold
# build. Local development still uses sccache; this is CI-only.
# Incremental artifacts are per-machine rebuild state, useless to a fresh
# runner, and they bloat the cache that is restored.
CARGO_INCREMENTAL: 0
jobs:
# The developer's iteration signal is the first three steps of
# `Test / ubuntu-latest` — format, workspace clippy, workspace tests, in that
# order, cheapest failure first. A separate "Quick" job used to run exactly
# those three on a second runner off the same cache key; it reached them no
# sooner, because it had to compile the same graph to get there, so all it
# bought was a duplicate build on every push.
coverage:
name: Coverage
runs-on: ubuntu-latest
timeout-minutes: 90
# A full instrumented rebuild shares nothing with the ordinary builds, so it
# runs on the integration branches rather than on every pull request.
if: github.event_name == 'push'
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- uses: ./.github/actions/setup-linux-deps
# Its own key: an instrumented build shares no artifacts with the
# ordinary ones, so storing it under the shared key would just overwrite
# a useful tarball with one nothing else can restore. The target dir is
# deliberately NOT cached: the instrumented tarball measures 3-5GB, and
# together with the two Test tarballs that oversubscribes the 10GB cache
# quota — every run would then evict one of the three and put a Test leg
# back on a 60-minute cold build. Coverage recompiles each run (~42min,
# which it already did when it was being evicted 4 runs out of 4) but
# only stores the ~0.2GB dependency caches, so the Test legs stay warm.
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-coverage
# Only the integration branches write cache generations. A pull
# request reads them and saves nothing: every distinct fingerprint
# would otherwise mint a generation that is never reclaimed, and the
# stale ones evict the Test tarballs that decide whether a leg is
# warm (16 min) or cold (66 min).
save-if: ${{ github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' }}
cache-targets: false
cache-on-failure: true
- uses: taiki-e/install-action@cargo-llvm-cov
- uses: taiki-e/install-action@nextest
- name: Prepare coverage target dir
run: |
mkdir -p target/llvm-cov-target
printf 'Signature: 8a477f597d28d172789f06886806bc55\n' > target/llvm-cov-target/CACHEDIR.TAG
# NEXTEST_PROFILE rather than --profile: cargo-llvm-cov reads that flag as
# cargo's build profile. The ci profile matters here because it disables
# fail-fast — without it a single failing test ends the run and hides the
# state of every test after it (see #153, where one failure masked 632).
- run: cargo llvm-cov nextest --workspace --features waterui/all --lcov --output-path lcov.info
env:
NEXTEST_PROFILE: ci
- uses: codecov/codecov-action@v4
with:
use_oidc: true
fail_ci_if_error: true
typos:
name: Typos
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
# Pinned: on `@master` the tool upgrades itself, and a new dictionary
# turns a green job red with no change to the repository.
- uses: crate-ci/typos@v1.49.0
# Security audit, license check and unused-dependency check are each a cheap
# metadata pass over the dependency graph, but were previously three separate
# jobs that each paid full runner startup, checkout and cache-restore cost.
# They share nothing that isolates them from each other, so one job running
# them sequentially pays that fixed cost once instead of three times.
hygiene:
name: Hygiene
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
# Only the license check below needs a native dependency (nasm, for a
# build script) and it never runs a GUI/audio/video stack, so this job
# does not need the full setup-linux-deps package list.
- run: sudo apt-get update && sudo apt-get install -y nasm
# Restore-only: this job and Features share `ci-ubuntu`, and two jobs
# saving the same key race for the reservation, so one of them always
# loses and uploads nothing. Features owns the write because it builds
# the larger graph.
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-ubuntu
cache-on-failure: true
save-if: false
# The audit and deny checks run against the COMMITTED Cargo.lock — the
# dependency set the workspace actually builds. A `cargo generate-lockfile`
# step used to precede them; it silently re-resolved everything to the
# newest compatible versions first, so the checks audited a hypothetical
# future resolution instead of the real one. That is exactly backwards
# for supply-chain checks: the August 2026 `arrayref 0.3.10` compromise
# (see the [bans] section in deny.toml) would have been *pulled in* by
# that step while the committed lock stayed clean.
- name: Security audit
uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
- uses: taiki-e/install-action@cargo-deny
- name: License and ban check
run: cargo deny check licenses bans
# `--config` goes before the subcommand: the cargo-deny that
# install-action provides takes it as a global option, and rejects the
# `check --config` spelling that newer builds accept.
- name: License and ban check (kit)
if: ${{ hashFiles('kit/Cargo.toml') != '' }}
working-directory: kit
run: cargo deny --config ../deny.toml check licenses bans
- uses: taiki-e/install-action@cargo-machete
- name: Unused dependencies
run: |
printf 'kit\nutils/nami\n' > .ignore
cargo machete --with-metadata
feature-checks:
name: Features
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
- uses: ./.github/actions/setup-linux-deps
# The single writer of `ci-ubuntu` (see Hygiene above).
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-ubuntu
# Only the integration branches write cache generations. A pull
# request reads them and saves nothing: every distinct fingerprint
# would otherwise mint a generation that is never reclaimed, and the
# stale ones evict the Test tarballs that decide whether a leg is
# warm (16 min) or cold (66 min).
save-if: ${{ github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' }}
cache-on-failure: true
- uses: taiki-e/install-action@cargo-hack
# `--feature-powerset` is deliberately absent. The workspace has ~33_800
# feature combinations (waterui alone has 14 features = 16_384), so the
# sweep either ran for 105 minutes or was cancelled at ~27 minutes having
# proved nothing — it produced no signal in any recent run. `--each-feature`
# is linear and catches the failure that actually happens: a feature that
# does not build on its own.
- run: cargo hack check --each-feature --no-dev-deps
ffi-header:
name: FFI Header
# The header is platform-dependent: the Apple exports (Metal, CEF) only
# appear when cbindgen expands on an Apple target, and the committed header
# contains them. Regenerating anywhere else would delete them and break the
# Apple backend, so macOS is the canonical generation host.
runs-on: macos-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
# The only job that needs nightly: cbindgen expands the FFI macros through
# `cargo rustc -- -Zunpretty=expanded`, and the exported functions are
# macro-generated, so the expansion cannot be skipped.
- uses: dtolnay/rust-toolchain@nightly
# Dependency caches only: the full target tarball measured 0.8GB and, in
# a quota squeezed by the two Test tarballs, was evicted before every
# single restore — this job has never once hit it. Storing just the
# dependency caches keeps the quota pressure off the caches that do hit.
- uses: Swatinem/rust-cache@v2
with:
shared-key: ffi-header-macos
cache-targets: false
cache-on-failure: true
- name: Regenerate the header
run: cargo run --bin generate_header --features cbindgen --manifest-path ffi/Cargo.toml
- name: Verify the committed header matches
run: |
set -euo pipefail
status=0
check() {
if ! git -C "$1" diff --exit-code -- "$2"; then
echo "::error file=$2::$2 is stale; on macOS run 'cargo +nightly run --bin generate_header --features cbindgen --manifest-path ffi/Cargo.toml' and commit the result"
status=1
fi
}
check . ffi/waterui.h
check backends/apple Sources/CWaterUI/include/waterui.h
check backends/android runtime/src/main/cpp/waterui.h
exit "$status"
test:
name: Test
uses: ./.github/workflows/test.yml
test-report:
name: Test Report
needs: test
if: always()
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- run: mkdir -p artifacts
- uses: actions/download-artifact@v4
continue-on-error: true
with:
pattern: test-snapshots-*
path: artifacts/
- name: Generate Step Summary
run: python3 .github/scripts/generate_test_summary.py artifacts >> "$GITHUB_STEP_SUMMARY"