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
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
name: CI
on:
push:
branches:
pull_request:
# A pull request runs on every base. A draft runs nothing until it is marked ready for
# review, which is what `ready_for_review` is here for: an agent opens its work as a draft,
# and the maintainer decides when CI spends its time on it. A first pull request from
# outside the organisation waits for the approval GitHub asks for, which is a repository
# setting.
types:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
changes:
name: Detect changes
# Every other job needs this one, so the draft gate lives here: a skipped gate skips them all.
if: >-
github.event_name != 'pull_request' || !github.event.pull_request.draft
runs-on: ubuntu-latest
outputs:
rust: ${{ steps.filter.outputs.rust }}
dist: ${{ steps.filter.outputs.dist }}
templates: ${{ steps.filter.outputs.templates }}
docs: ${{ steps.filter.outputs.docs }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: dorny/paths-filter@v4
id: filter
with:
filters: |
rust:
- 'src/**'
- 'crates/**'
- 'tests/**'
- 'examples/**'
- 'benches/**'
- 'Cargo.toml'
- 'Cargo.lock'
- 'rust-toolchain.toml'
- 'rustfmt.toml'
- 'clippy.toml'
- '.github/workflows/ci.yml'
dist:
- 'src/**'
- 'crates/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/ci.yml'
docs:
- 'docs/**'
- 'website/**'
- 'examples/**'
# API links on the pages resolve against the crate's rustdoc, so a renamed item
# rebuilds the site and fails it on a link that no longer resolves
- 'src/**'
- 'crates/**'
- 'Cargo.toml'
- 'Cargo.lock'
# the testing and conformance pages embed their snippets from these test files
- 'tests/doc_*.rs'
- 'tests/conformance_self.rs'
- 'scripts/lint_doc_fences.py'
- 'scripts/check_doc_i18n.py'
- 'scripts/doc_glossary.toml'
- '.github/workflows/ci.yml'
templates:
- 'templates/**'
- 'src/**'
- 'Cargo.toml'
- '.github/workflows/ci.yml'
lint:
name: Lint and checks
needs: changes
# One runner for every check outside the build job: formatting, lints, the dependency
# graph, the public API surface, the publish dry-run, the scaffold, each feature alone and
# minimal versions. Not path-gated as a whole: advisories arrive over time, not only with
# manifest changes, so `cargo deny` runs on every pull request, and each other step runs
# when its own sources changed. Each check runs whatever the one before it did, so a red
# formatting step hides no later verdict. Minimal versions rewrites the lock file and the
# manifests for good, so it comes last.
runs-on: ubuntu-latest
env:
RUST_CHANGED: ${{ needs.changes.outputs.rust }}
TEMPLATES_CHANGED: ${{ needs.changes.outputs.templates }}
DIST_CHANGED: ${{ needs.changes.outputs.dist }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
# Dependency-graph gate: RustSec advisories, license allow-list, duplicate versions, and
# registry sources, per the checked-in deny.toml.
- name: cargo deny
uses: EmbarkStudios/cargo-deny-action@v2
- name: Install Rust toolchain
if: >-
!cancelled()
&& (env.RUST_CHANGED == 'true' || env.TEMPLATES_CHANGED == 'true'
|| env.DIST_CHANGED == 'true')
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
# The rest are installed beside stable, which `rust-toolchain.toml` keeps the default: the
# floor for minimal versions, a nightly for its resolver, and the nightly the rustdoc JSON
# tool was built for (move that date and the tool version together).
- name: Install the extra toolchains
if: "!cancelled() && env.RUST_CHANGED == 'true'"
run: rustup toolchain install --profile minimal 1.95 nightly nightly-2026-03-20
- name: Cache cargo
if: >-
!cancelled()
&& (env.RUST_CHANGED == 'true' || env.TEMPLATES_CHANGED == 'true'
|| env.DIST_CHANGED == 'true')
uses: Swatinem/rust-cache@v2
- name: Install cargo-hack
if: "!cancelled() && env.RUST_CHANGED == 'true'"
uses: taiki-e/install-action@v2.87.28
with:
tool: cargo-hack
- name: Install cargo-check-external-types
if: "!cancelled() && env.RUST_CHANGED == 'true'"
run: cargo install --locked cargo-check-external-types@0.5.0
- name: Install cargo-generate
if: "!cancelled() && env.TEMPLATES_CHANGED == 'true'"
uses: taiki-e/install-action@v2.87.28
with:
tool: cargo-generate
- name: cargo fmt
if: "!cancelled() && env.RUST_CHANGED == 'true'"
run: cargo fmt --all -- --check
# The benchmarks are not in the all-features lints: they are built with the production
# feature set, and the harness feature is a compile error in them. CI only keeps them
# compiling; `just bench` runs them. A rustc warning fails their build, through cargo's
# `build.warnings` as in the justfile, which leaves the build fingerprint alone.
- name: cargo clippy
if: "!cancelled() && env.RUST_CHANGED == 'true'"
env:
CARGO_BUILD_WARNINGS: deny
run: |
cargo clippy --locked --workspace --lib --bins --tests --examples --all-features \
-- -D warnings
cargo check --locked --benches --no-default-features --features memory,macros,json
# Allow-list in Cargo.toml. Every entry makes a version bump of that dependency breaking
# for every broker crate downstream, so a new leak is added deliberately or not at all.
- name: cargo check-external-types
if: "!cancelled() && env.RUST_CHANGED == 'true'"
run: cargo +nightly-2026-03-20 check-external-types --all-features
# Both packages in one invocation: packaging strips the path from the lockstep
# `ruststream-macros` dependency, and between a version bump and the release that version
# is not on crates.io yet, so packaging ruststream alone cannot resolve it.
# CARGO_HTTP_MULTIPLEXING=false works around HTTP2 framing failures on the runners.
- name: cargo publish dry-run (no upload)
if: "!cancelled() && env.DIST_CHANGED == 'true'"
env:
CARGO_HTTP_MULTIPLEXING: "false"
run: cargo publish --locked --dry-run -p ruststream-macros -p ruststream --all-features
# The dry-run verifies each package by building it under `target/package/<crate>`, whose
# inner trees the cache action cannot read as cargo profiles either (same annotation as
# the trybuild scratch above). Verification output is per-run; nothing here is reusable.
- name: Drop the packaging scratch tree
if: always() && env.DIST_CHANGED == 'true'
run: rm -rf target/package
# Renders the scaffold and compiles it, so template drift fails here and not on a user's
# first build. The rendered project is patched to this checkout. No --locked: a fresh
# scaffold has no lock file, which is the situation a user starts from.
- name: Render and check templates/memory
if: "!cancelled() && env.TEMPLATES_CHANGED == 'true'"
run: |
cargo generate --path templates/memory --name smoke --destination "$RUNNER_TEMP" --vcs none
# The scaffold manifest is checked in as `Cargo.toml.liquid` so that cargo's git-source
# package discovery never parses its placeholder name. Assert that cargo-generate did
# drop the suffix, otherwise the append below would quietly write a manifest of its own.
test -f "$RUNNER_TEMP/smoke/Cargo.toml"
# `[patch]` redirects where a crate comes from, not what the manifest asks for, and
# Cargo keeps pre-releases out of a range that does not name one. So while the
# workspace is on a pre-release the scaffold's own `0.7` would reject this checkout.
# Point the rendered copy at the version actually being built; the template keeps the
# requirement a user writes for the release it targets.
VERSION=$(cargo metadata --format-version 1 --no-deps \
| jq -r '.packages[] | select(.name == "ruststream") | .version')
sed -i "s|^ruststream = { version = \"[^\"]*\"|ruststream = { version = \"$VERSION\"|" \
"$RUNNER_TEMP/smoke/Cargo.toml"
grep -q "version = \"$VERSION\"" "$RUNNER_TEMP/smoke/Cargo.toml"
{
echo ""
echo "[patch.crates-io]"
echo "ruststream = { path = \"$GITHUB_WORKSPACE\" }"
} >> "$RUNNER_TEMP/smoke/Cargo.toml"
cargo check --manifest-path "$RUNNER_TEMP/smoke/Cargo.toml"
# --no-dev-deps: a dev-dependency in scope masks a missing feature gate in the library.
# It also rewrites the manifests while it runs, which is why this is the last step and the
# one without --locked. A feature that leaves code unreached on its own fails on the
# warning.
- name: cargo hack check (each feature)
if: "!cancelled() && env.RUST_CHANGED == 'true'"
env:
CARGO_BUILD_WARNINGS: deny
run: cargo hack check --workspace --each-feature --no-dev-deps
# Minimal versions is one chain: each step runs only when the one before it succeeded, since
# a step on a graph that failed to resolve checks nothing.
# The full flag rather than -Z direct-minimal-versions: the narrow one leaves the assembled
# graph unverified, and that is what a user builds. It costs the pinning list below.
- name: Resolve minimal versions
id: minimal-resolve
if: "!cancelled() && env.RUST_CHANGED == 'true'"
run: cargo +nightly update -Z minimal-versions
# trybuild 1.0.0 declares `termcolor = "1.0"` but needs `Color: Copy`, added in 1.0.4.
# A too-loose bound in a third-party manifest; drop this when trybuild's floor moves.
- name: Pin dependencies whose declared bounds are too loose
id: minimal-pin
if: "!cancelled() && steps.minimal-resolve.outcome == 'success'"
run: cargo update -p termcolor --precise 1.0.4
# Minimum dependencies on the minimum compiler: the worst case a user can assemble.
# Resolution above needs nightly, so only this step drops to the floor.
- name: Build and test against minimal versions
id: minimal-test
if: "!cancelled() && steps.minimal-pin.outcome == 'success'"
env:
RUSTUP_TOOLCHAIN: "1.95"
run: |
cargo check --locked --workspace --lib --bins --tests --examples --all-features
cargo test --locked --workspace --all-features --no-fail-fast
# Everything above still sees dev-dependencies, which raise shared floors before the library
# is built: the run proves the workspace resolves, not that the manifest is honest for a
# consumer. Drop them and resolve again, so the graph is the one a dependent gets. Runs last
# because it rewrites the manifests in place.
- name: Resolve minimal versions without dev-dependencies
id: minimal-resolve-lib
if: "!cancelled() && steps.minimal-test.outcome == 'success'"
run: |
cargo hack --remove-dev-deps
cargo +nightly update -Z minimal-versions
# tonic 0.14 declares `tower = "0.5"`, but `ServiceBuilder::new` only became const-callable
# in 0.5.1. Another too-loose third-party bound, masked above by a higher tower.
- name: Pin dependencies whose declared bounds are too loose (no dev-dependencies)
id: minimal-pin-lib
if: "!cancelled() && steps.minimal-resolve-lib.outcome == 'success'"
run: cargo update -p tower@0.5.0 --precise 0.5.1
# The library as a dependency, on the floor toolchain. No --locked: cargo-hack edits the
# manifests it checks.
- name: Check the library against its own declared floors
id: minimal-check-lib
if: "!cancelled() && steps.minimal-pin-lib.outcome == 'success'"
env:
RUSTUP_TOOLCHAIN: "1.95"
run: cargo hack check --workspace --no-dev-deps --all-features
rust:
name: Build and test
needs: changes
if: ${{ needs.changes.outputs.rust == 'true' }}
# Stable first, then the early warning and the floor on the same runner, and those two only
# once stable passed as a whole: a break stable already reports is not worth two more
# builds. Releases between the floor and stable add nothing: what fails on a later one fails
# on the floor too. Stable is the default through `rust-toolchain.toml`; the other two steps
# name theirs through RUSTUP_TOOLCHAIN, which outranks the file.
runs-on: ubuntu-latest
env:
# The line-coverage floor, at the 95% DoD target. Raise it here and in the justfile
# together if coverage climbs further.
COVERAGE_GATE: "95"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@v2.87.28
with:
tool: cargo-llvm-cov
- name: Install beta and the floor
run: |
rustup toolchain install --profile minimal --component clippy beta
rustup toolchain install --profile minimal 1.95
- name: Cache cargo
uses: Swatinem/rust-cache@v2
# The stable builds and tests below fail on a rustc warning, through cargo's
# `build.warnings` as in the justfile. The instrumented run is left out: clippy lints its
# all-features build.
- name: cargo check (no default features)
env:
CARGO_BUILD_WARNINGS: deny
run: cargo check --locked --workspace --no-default-features
- name: cargo check (all features)
env:
CARGO_BUILD_WARNINGS: deny
run: |
cargo check --locked --workspace --lib --bins --tests --examples --all-features
cargo check --locked --benches --no-default-features --features memory,macros,json
# The `DefaultCodec` fallback aliases (cbor, then msgpack) are cfg'd out whenever `json` is
# on, so neither the all-features build nor the all-features tests ever compile them. Build
# each single-codec configuration so the alias and its default-codec call sites stay valid.
- name: cargo check (single-codec default fallbacks)
env:
CARGO_BUILD_WARNINGS: deny
run: |
cargo check --locked --no-default-features --features cbor,memory,macros
cargo check --locked --no-default-features --features msgpack,memory,macros
# Stale instrumented artifacts from the cache double-count files and crater the total.
- name: Clean stale coverage artifacts
run: cargo llvm-cov clean --workspace
# Stable runs its suite once, under instrumentation: the coverage run is the test run, and
# the two report steps below reuse its profile without re-running anything. The UI snapshots record rustc's exact wording, which is stable's, so they run here and
# nowhere else. REQUIRE_UI_TESTS makes a skip a failure, so losing the opt-in turns this
# job red instead of leaving a green run in which the compile-fail cases checked nothing.
# --no-fail-fast: a failing target must not hide failures in the targets behind it.
- name: Run tests under coverage
env:
RUN_UI_TESTS: '1'
REQUIRE_UI_TESTS: '1'
run: cargo llvm-cov --locked --no-report --workspace --all-features --no-fail-fast
# The summary is written before the gate's exit code propagates, so it shows up even on a
# failure.
- name: Coverage summary and gate
run: |
set +e
cargo llvm-cov report --summary-only --fail-under-lines "$COVERAGE_GATE" | tee coverage.txt
rc=${PIPESTATUS[0]}
{
echo '## Coverage'
echo
echo '```'
cat coverage.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
exit "$rc"
- name: Export lcov report
if: "!cancelled()"
run: cargo llvm-cov report --lcov --output-path lcov.info
- name: Upload coverage report
if: "!cancelled()"
uses: actions/upload-artifact@v7
with:
name: coverage-lcov
path: lcov.info
# Uploaded even when the gate fails, so the badge shows reality. Advisory: a Coveralls
# outage must not fail the pipeline.
- name: Upload to Coveralls
if: "!cancelled()"
uses: coverallsapp/github-action@v2
with:
file: lcov.info
format: lcov
fail-on-error: false
# The tests themselves on a reduced feature set. Everything above either builds the tests
# with every feature on, or builds the library alone without them - so a test that reached
# for a gated item, or a mount that demanded a codec the self-carrying lanes never use,
# compiled nowhere and rotted unnoticed. Those lanes are what must hold with no codec at
# all: a `Deserialized` input in, a `Serialized` value out, through the same typed publish
# entry a service always writes. `ui_codec_free` pins the errors the other side of that
# line produces, which exist only where no codec resolves. `lane_traits_without_macros`
# builds with `macros` off, where the derives are gone and the prelude alone has to carry
# every name the hand-written lane impls spell.
- name: cargo test (reduced features)
env:
RUN_UI_TESTS: '1'
REQUIRE_UI_TESTS: '1'
CARGO_BUILD_WARNINGS: deny
run: |
cargo test --locked --no-default-features --lib
cargo check --locked --workspace --no-default-features --features testing,memory,macros
cargo test --locked --no-default-features --features macros,memory,testing --test raw_subscriber
cargo test --locked --no-default-features --features macros,memory,testing --test codec_free_lanes
cargo test --locked --no-default-features --features macros,memory,testing --test ui_codec_free
cargo test --locked --no-default-features --features memory,testing --test lane_traits_without_macros
cargo test --locked --no-default-features --features testing,memory,json --examples
# Plain `cargo test`, the first command a contributor runs. Every target that needs a
# non-default feature gates itself (a crate-level `cfg` or `required-features`), and only
# this run notices one that does not: the legs above all pick their features explicitly.
- name: cargo test (default features)
env:
CARGO_BUILD_WARNINGS: deny
run: cargo test --locked --workspace --no-fail-fast
# All-features hides a doc example that names a feature-gated item without gating itself.
# Both ends of the range catch it.
- name: cargo test --doc (feature edges)
env:
CARGO_BUILD_WARNINGS: deny
run: |
cargo test --locked --workspace --doc
cargo test --locked --workspace --doc --no-default-features
# The crate-root missing_docs deny covers item docs; this gate catches what rustc
# cannot: broken intra-doc links and redundant targets only rustdoc resolves.
- name: cargo doc
env:
RUSTDOCFLAGS: -D warnings
run: cargo doc --locked --workspace --all-features --no-deps
# The gate the later toolchains wait on: it succeeds only when every stable step did.
- name: Stable passed
id: stable
run: echo "stable passed"
# Advisory, not a gate: surfaces a lint added by the next clippy release six weeks
# before it turns the build red.
- name: cargo clippy (beta, advisory)
if: "!cancelled() && steps.stable.outcome == 'success'"
continue-on-error: true
env:
RUSTUP_TOOLCHAIN: beta
run: |
cargo clippy --locked --workspace --lib --bins --tests --examples --all-features \
-- -D warnings
- name: cargo check (no default features, beta)
if: "!cancelled() && steps.stable.outcome == 'success'"
env:
RUSTUP_TOOLCHAIN: "beta"
run: cargo check --locked --workspace --no-default-features
- name: cargo check (all features, beta)
if: "!cancelled() && steps.stable.outcome == 'success'"
env:
RUSTUP_TOOLCHAIN: "beta"
run: |
cargo check --locked --workspace --lib --bins --tests --examples --all-features
cargo check --locked --benches --no-default-features --features memory,macros,json
- name: cargo test (beta)
if: "!cancelled() && steps.stable.outcome == 'success'"
env:
RUSTUP_TOOLCHAIN: "beta"
run: cargo test --locked --workspace --all-features --no-fail-fast
- name: cargo check (no default features, 1.95)
if: "!cancelled() && steps.stable.outcome == 'success'"
env:
RUSTUP_TOOLCHAIN: "1.95"
run: cargo check --locked --workspace --no-default-features
- name: cargo check (all features, 1.95)
if: "!cancelled() && steps.stable.outcome == 'success'"
env:
RUSTUP_TOOLCHAIN: "1.95"
run: |
cargo check --locked --workspace --lib --bins --tests --examples --all-features
cargo check --locked --benches --no-default-features --features memory,macros,json
- name: cargo test (1.95)
if: "!cancelled() && steps.stable.outcome == 'success'"
env:
RUSTUP_TOOLCHAIN: "1.95"
run: cargo test --locked --workspace --all-features --no-fail-fast
# trybuild compiles each UI case in a scratch crate under `target/tests`, which is not a
# cargo profile directory. The cache action walks `target` before it saves, cannot read
# that tree as one, and reports the miss as a job annotation - a red mark on a green run.
# The scratch is rebuilt per run anyway, so there is nothing to keep. Runs last and
# unconditionally: a failing test must not leave the tree behind either.
- name: Drop the trybuild scratch tree
if: always()
run: rm -rf target/tests
docs:
name: Build site
needs:
# After the lint job, and only when it passed: a site built from code that fails its own
# gates is not worth the runner.
if: >-
needs.changes.outputs.docs == 'true'
&& needs.lint.result == 'success'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v7
with:
node-version: "22"
cache: npm
cache-dependency-path: website/package-lock.json
- name: Install site toolchain
working-directory: website
run: npm ci
- name: Lint doc code fences
run: python3 scripts/lint_doc_fences.py
- name: Check translated pages against their originals
run: python3 scripts/check_doc_i18n.py
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: Swatinem/rust-cache@v2
# The API links on the pages resolve against this rustdoc, the one docs.rs renders for the
# same code; an item it lacks fails the build below.
- name: Build the rustdoc the API links resolve against
run: cargo doc --locked --all-features --no-deps -p ruststream
# Strict: a broken link, anchor, Markdown link or API link fails the build.
- name: Build site (strict)
working-directory: website
run: npm run build
- name: Type-check and test site components
working-directory: website
run: |
npm run typecheck
npm run test
codeql:
name: CodeQL (${{ matrix.language }})
needs: lint
# After the lint job, and only when it passed. The scan lives here rather than in the
# default setup, which cannot be ordered after anything. None of the three languages needs a
# build: Rust and Python are extracted from source, and `actions` reads the workflows.
if: needs.lint.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language:
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: none
- uses: github/codeql-action/analyze@v4
with:
category: /language:${{ matrix.language }}
result:
name: CI result
# The one check the `main` ruleset requires: it speaks for every job above, so a job that
# comes, goes or is renamed never touches the ruleset. Skipped counts as passed, anything else fails.
# A draft skips it with the rest, and the run that marking it ready starts reports again.
needs:
- changes
- rust
- lint
- docs
- codeql
if: >-
always()
&& (github.event_name != 'pull_request' || !github.event.pull_request.draft)
runs-on: ubuntu-latest
steps:
- name: Every job passed or was skipped
env:
RESULTS: ${{ join(needs.*.result, ' ') }}
run: |
echo "job results: $RESULTS"
for result in $RESULTS; do
case "$result" in
success | skipped) ;;
*) exit 1 ;;
esac
done