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
name: CI
on:
push:
branches:
pull_request:
branches:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
changes:
name: Detect changes
runs-on: ubuntu-latest
outputs:
rust: ${{ steps.filter.outputs.rust }}
dist: ${{ steps.filter.outputs.dist }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
rust:
- 'src/**'
- 'crates/**'
- 'tests/**'
- 'examples/**'
- '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'
toolchains:
name: Resolve toolchain range
needs: changes
if: ${{ needs.changes.outputs.rust == 'true' }}
runs-on: ubuntu-latest
outputs:
list: ${{ steps.range.outputs.list }}
steps:
# Multi-MSRV: the crate keeps rust-version = 1.85 and CI proves the build on
# every stable toolchain from that floor up to current stable, so broker
# crates can adopt any MSRV in the range without waiting for a core release.
# The list is resolved from the release manifest, so a new stable joins the
# matrix on its release day with no workflow edit.
- name: Build the 1.85..stable toolchain list
id: range
run: |
stable_minor=$(curl -sSf https://static.rust-lang.org/dist/channel-rust-stable.toml \
| grep -A1 '^\[pkg\.rust\]' | grep -m1 'version' | sed -E 's/.*"1\.([0-9]+)\..*/\1/')
versions=$(seq 85 "$((stable_minor - 1))" | sed 's/^/"1./; s/$/"/' | paste -sd, -)
echo "list=[\"stable\",${versions}]" >> "$GITHUB_OUTPUT"
rust:
name: Rust ${{ matrix.toolchain }}
needs:
if: ${{ needs.changes.outputs.rust == 'true' }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
toolchain: ${{ fromJSON(needs.toolchains.outputs.list) }}
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ matrix.toolchain }}
components: rustfmt, clippy
- name: Cache cargo
uses: Swatinem/rust-cache@v2
- name: cargo fmt
if: matrix.toolchain == 'stable'
run: cargo fmt --all -- --check
- name: cargo clippy
if: matrix.toolchain == 'stable'
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
- name: cargo check (no default features)
run: cargo check --workspace --no-default-features
- name: cargo check (all features)
run: cargo check --workspace --all-targets --all-features
# 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)
if: matrix.toolchain == 'stable'
run: |
cargo check --no-default-features --features cbor,memory,macros
cargo check --no-default-features --features msgpack,memory,macros
# The intermediate toolchains exist to prove the build (the two checks
# above); the full suite runs at the edges of the range only.
#
# The trybuild UI snapshots in tests/ui are rustc-version-sensitive, so they
# run on stable only: RUN_UI_TESTS=1 opts the `ui` test in here, every other
# run (the 1.85 job, the coverage job, a local `cargo test`) leaves it unset
# and the test skips itself.
- name: cargo test
if: matrix.toolchain == 'stable' || matrix.toolchain == '1.85'
env:
RUN_UI_TESTS: ${{ matrix.toolchain == 'stable' && '1' || '0' }}
run: cargo test --workspace --all-features
coverage:
name: Coverage
needs: changes
if: ${{ needs.changes.outputs.rust == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
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
with:
tool: cargo-llvm-cov
- name: Cache cargo
uses: Swatinem/rust-cache@v2
# Run the suite under instrumentation once, then format the collected data twice:
# a human-readable table into the run's job summary, and an lcov file as a downloadable
# artifact. `report` reuses the profile from `--no-report`, so neither re-runs the tests.
- name: Run tests under coverage
run: cargo llvm-cov --no-report --workspace --all-features
# Line-coverage gate, sitting at the 90% DoD target. Raise it here and in the justfile
# together if coverage climbs further. 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 90 | tee coverage.txt
rc=${PIPESTATUS[0]}
{
echo '## Coverage'
echo
echo '```'
cat coverage.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
exit "$rc"
- name: Export lcov report
if: always()
run: cargo llvm-cov report --lcov --output-path lcov.info
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage-lcov
path: lcov.info
package:
name: Package (publish dry-run)
needs: changes
if: ${{ needs.changes.outputs.dist == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: Swatinem/rust-cache@v2
# Package the published artifacts without uploading. Both packages in one invocation
# (multi-package packaging, stable since cargo 1.90): packaging strips the path from the
# lockstep `ruststream-macros` dependency, and between a version bump and the release that
# version does not exist on crates.io, so packaging ruststream alone cannot resolve it. The
# multi-package form resolves in-workspace dependencies through a local overlay instead.
#
# CARGO_HTTP_MULTIPLEXING=false works around recurring "[16] Error in the HTTP2 framing
# layer" curl failures on the runners when this job re-downloads registry deps.
- name: cargo publish dry-run (no upload)
env:
CARGO_HTTP_MULTIPLEXING: "false"
run: cargo publish --dry-run -p ruststream-macros -p ruststream --all-features
security:
name: Security scans
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
# Dependency-graph gate: RustSec advisories, license allow-list, duplicate
# versions, and registry sources, per the checked-in deny.toml. Not path-
# gated: advisories arrive over time, not only with manifest changes.
- name: cargo deny
uses: EmbarkStudios/cargo-deny-action@v2