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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
name: Rust CI
on:
push:
branches:
# No base-branch filter on pull_request: stacked PRs (base = another feature
# branch) must get the same gate as PRs to main — the Windows jobs
# especially, since a local Linux gate can't catch cfg(windows) breakage.
pull_request:
# Beta and nightly run here rather than on every PR (see the `rust` matrix).
schedule:
- cron: '17 6 * * *'
workflow_dispatch:
# One in-flight run per ref. Pushing a fixup used to leave the superseded run
# compiling to completion on every leg, producing a verdict on a commit nobody
# would read it for — while holding the runners the new run then queues behind.
concurrency:
# `event_name` is in the key so the nightly schedule and a push to main do not
# share a group and serialize behind each other.
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
# Never cancel a main-branch run: those are the ones a release is cut from.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
# ~90% of a CI leg is compilation, not testing: the Windows leg spent ~295s
# compiling and 35s running all 2046 tests. So the compile knobs below are
# where the wall clock is. Measured together on a cold target dir, building
# exactly what CI builds: compile 172s -> 144s (-16%), and the target dir
# 8473 MB -> 2616 MB (-69%), which is what rust-cache has to restore.
#
# Incremental compilation is pure overhead here: it exists to make the SECOND
# build in a working tree fast, and CI never gets one. It costs time and is
# most of the target-dir bloat.
CARGO_INCREMENTAL: 0
# Full debuginfo (cargo's default, `debug = 2`) is the expensive half of a
# debug build, and on Windows the PDB write dominates linking — 12
# integration-test binaries each link the whole lib. `line-tables-only` keeps
# what a CI failure is actually read for: backtraces with file:line, paired
# with the RUST_BACKTRACE above. What it drops is variable inspection, which
# needs an interactive debugger nobody attaches to a runner.
CARGO_PROFILE_DEV_DEBUG: line-tables-only
CARGO_PROFILE_TEST_DEBUG: line-tables-only
jobs:
# Format checking
fmt:
name: Rustfmt
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: stable
components: rustfmt
- name: Check formatting
run: cargo fmt -- --check
# Linting with Clippy — `-D warnings` makes lint regressions fail CI.
clippy:
name: Clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: stable
components: clippy
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Run clippy
# `--workspace` so the `mermaid-runtime` crate (safety classifier,
# approvals store) is linted too — a bare `cargo clippy` only covers the
# root package and silently skipped it.
run: cargo clippy --workspace --all-targets -- -D warnings
# Build and test on multiple Rust versions and OS
test:
name: Test Suite
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
# A PR gets all three platforms on stable — those are the required
# checks, and the merge waits on the slowest of them.
#
# Beta and nightly answer a different question ("will a future toolchain
# break us?"), and the answer does not change between one PR and the
# next. Running them per-PR spent 4 of 7 test jobs re-asking it, so they
# run on the nightly schedule and on every push to main instead: the
# same signal, arriving within a day, off the critical path.
rust: ${{ fromJSON(github.event_name == 'pull_request' && '["stable"]' || '["stable", "beta", "nightly"]') }}
# Nightly stays Linux-only — enough to catch a future-toolchain break
# without tripling the cost of finding it. On a PR the list above has no
# nightly to exclude and these are no-ops.
exclude:
- os: macos-latest
rust: nightly
- os: windows-latest
rust: nightly
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: ${{ matrix.rust }}
# Cache dependencies for faster builds
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
# Cache key includes OS and Rust version
key: ${{ matrix.os }}-${{ matrix.rust }}
- name: Install cargo-nextest
uses: taiki-e/install-action@6c6fd71fe4fb72c3697d269963d0e15df8adedad # v2.85.10
with:
tool: nextest
# No separate `cargo build --workspace` step before this one. It looked
# free — surely nextest reuses the artifacts — but it measured 144s cold
# against 113s for nextest alone, because the two invocations do not want
# the same lib: `build` produces a plain rlib and the bins, nextest wants
# the lib again under `cfg(test)`. The integration tests reach the bins
# through `env!("CARGO_BIN_EXE_mermaid")`, which makes cargo build them
# here anyway, so nothing was lost by dropping the step — a compile error
# still fails this step, just with nextest's name on it.
- name: Build and run tests
# nextest runs the same test binaries as `cargo test`, but applies the
# `.config/nextest.toml` retry policy that auto-heals the documented
# Windows cancellation flake. `--workspace` covers the mermaid-runtime
# crate (safety classifier, approvals store). No doctests exist, so
# nextest skipping them loses nothing.
run: cargo nextest run --workspace
# The insta snapshot suite now runs on every matrix leg (#296), so a
# mismatch is reproducible locally. Still publish the `.snap.new` siblings
# insta writes on failure: the frame a leg you don't have rendered is what
# makes a platform-specific diff reviewable (and appliable) at all.
- name: Upload pending snapshots
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: pending-snapshots-${{ matrix.os }}-${{ matrix.rust }}
path: '**/*.snap.new'
if-no-files-found: ignore
retention-days: 3
# Only check documentation on stable Linux
- name: Check documentation
if: matrix.rust == 'stable' && matrix.os == 'ubuntu-latest'
run: cargo doc --no-deps --document-private-items
# Security audit
security:
name: Security Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# `rustsec/audit-check` compiled cargo-audit from source on every run:
# 221s in a single step, on a job whose actual work is reading
# Cargo.lock against an advisory database. That made it 94% as long as
# the Windows leg — the critical path — for no reason a prebuilt binary
# doesn't solve. `taiki-e/install-action` downloads one (the same action
# already used for nextest), and `cargo audit` is the same tool reading
# the same RustSec database, so the verdict is unchanged.
- uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: stable
- uses: taiki-e/install-action@6c6fd71fe4fb72c3697d269963d0e15df8adedad # v2.85.10
with:
tool: cargo-audit
# Plain `cargo audit`, not `--deny warnings`: the action failed on
# vulnerabilities and only annotated unmaintained/unsound advisories, and
# this job is meant to run exactly as strict as it did before, not one
# notch stricter.
- name: Audit dependencies
run: cargo audit
# Every publishable crate must build ALONE, on Linux.
#
# `cargo build --workspace` unifies features across members, so the root
# enabling `dbus-secret-service/vendored` made every crate's keyring build —
# including one whose own manifest never declared it. Nothing caught that
# until `cargo publish` verified the packaged tarball in isolation, mid-release,
# after `mermaid-runtime` had already gone to crates.io and could not be taken
# back (v0.21.0). A local `--dry-run` could not have caught it either: on
# Windows keyring uses `windows-native` and never touches dbus.
#
# `cargo package` + building the extracted tarball reproduces exactly what
# publish verifies, without needing the version to exist on the index — so
# this runs on every PR instead of once per release.
isolated-crate-build:
name: Crates build standalone (Linux)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: isolated-crate-build
- name: Build each crate outside the workspace
run: |
set -euo pipefail
# Copy the crates somewhere with no workspace root above them. Each
# then builds as its own root — no feature unification with
# `mermaid-cli` — which is exactly the isolation `cargo publish`
# applies, and exactly what hid the missing dep.
#
# Copying rather than `cargo package`: packaging resolves deps against
# the crates.io index, so a crate depending on an unreleased sibling
# version cannot be packaged before that sibling is published. That
# made the first version of this job fail on its own fix PR. Sibling
# path deps resolve fine here because the siblings are copied too.
ISO="$RUNNER_TEMP/isolated"
mkdir -p "$ISO"
cp -r crates/. "$ISO/"
for crate in mermaid-runtime mermaid-model; do
echo "::group::$crate (standalone)"
( cd "$ISO/$crate" && cargo build --all-targets )
echo "::endgroup::"
done
# Dependency-free source guards: no emoji in user-facing output, and the pure
# `src/domain` MVU core stays free of I/O + the wall clock.
guards:
name: Source Guards
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: No emoji in source
run: python3 .github/scripts/check_no_emoji.py
- name: domain/ purity (no I/O, no wall clock)
run: python3 .github/scripts/check_domain_purity.py
# Heavier integration coverage that CI's default suite skips: the daemon
# `#[ignore]`d tests (spawn a real mermaidd) and the deterministic, model-free
# `mermaid self-test`. Linux-only — enough to catch daemon/self-test breakage.
integration:
name: Daemon + Self-Test (Linux)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Build binaries
run: cargo build --workspace
- name: Daemon integration tests (ignored)
run: cargo test --test daemon_integration -- --ignored
- name: Sandbox network kill-switch (ignored, Linux)
run: cargo test --test sandbox_network -- --ignored
- name: Sandbox filesystem confinement (ignored, Linux)
run: cargo test --test sandbox_fs -- --ignored
- name: Self-test (deterministic, model-free)
run: cargo run --bin mermaid -- self-test --format json
# The managed-search test downloads the pinned bundle, starts the real
# Granian/SearXNG process, performs a JSON search, and verifies shutdown. Run
# it on every supported managed-search OS; the normal Windows matrix covers
# the actionable unsupported-backend path.
managed-search-integration:
name: Managed SearXNG (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: managed-searxng-${{ matrix.os }}
- name: Managed SearXNG end-to-end (ignored)
run: cargo test --lib searxng::tests::managed_searxng_end_to_end -- --ignored --exact
# OS-sandbox enforcement on the non-Linux platforms with a real backend
# (the Linux `integration` job above already runs these tests). macOS-only
# for now; the Windows AppContainer port adds windows-latest. The
# `#[ignore]`d tests include the Seatbelt profile-compile canary, so an SBPL
# grammar change in a new macOS release fails this job loudly.
# Real macOS verification of the clipboard FILE-REFERENCE paste path.
#
# This path (Finder 'Copy' on an image file, rather than copying the image
# itself) was written against documented `public.file-url` behavior and could
# not be exercised by its author, who has no Mac. The Linux equivalents are
# verified by hand; a CI runner is the only Apple hardware this project has,
# so the check lives here rather than in a paragraph nobody re-reads.
clipboard-macos:
name: Clipboard file-reference paste (macOS)
runs-on: macos-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: clipboard-macos
- name: Arm the clipboard with a file reference, then probe it
run: |
set -uo pipefail
DIR="$RUNNER_TEMP/mermaid-fileref"
mkdir -p "$DIR"
# A space in the name so the URI carries a percent-escape.
echo 'not-a-real-png-but-the-extension-is-what-matters' > "$DIR/My Shot.png"
# Exactly what Finder's Copy puts on the pasteboard: a file URL.
if ! osascript -e "set the clipboard to POSIX file \"$DIR/My Shot.png\""; then
echo "::notice::this runner cannot drive the pasteboard; skipping"
exit 0
fi
INFO=$(osascript -e 'clipboard info' || true)
echo "clipboard info: $INFO"
case "$INFO" in
*furl*) ;;
*)
echo "::notice::pasteboard did not take a file reference; skipping"
exit 0
;;
esac
# Only now is the assertion meaningful: the pasteboard really holds a
# file reference, so a failure here is mermaid's probe, not the runner.
cargo test --lib manual_file_reference_paste -- --ignored --nocapture
sandbox-integration:
name: OS Sandbox
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: sandbox-${{ matrix.os }}
- name: Build binaries
run: cargo build --workspace
- name: Sandbox network denial (ignored)
run: cargo test --test sandbox_network -- --ignored
- name: Sandbox filesystem confinement (ignored)
run: cargo test --test sandbox_fs -- --ignored
- name: Self-test (real sandbox availability probes)
run: cargo run --bin mermaid -- self-test --format json