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
name: CI
on:
# Branch-filtered so a pull request from a branch in this repository does not
# trigger two identical runs.
push:
branches:
pull_request:
# A RustSec advisory is published against code that is already merged, so a
# push/PR-only trigger only ever finds it the next time somebody happens to
# open a pull request. The `supply-chain` job below runs on this schedule too;
# `test` skips it, having nothing to say without a change to test.
schedule:
- cron: "17 6 * * *"
# Least privilege: nothing in this workflow writes to the repository.
permissions:
contents: read
# A newer push to the same ref supersedes the run in progress.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
# Every action below is pinned to a commit SHA rather than a tag. A tag is
# mutable: whoever controls the action's repository can repoint it at new code,
# which then runs with this workflow's token and inside the build that produces
# a certificate authority. `taiki-e/install-action` is the sharpest case — it
# downloads and executes prebuilt binaries — but the rule is uniform so no
# reader has to decide which ones matter. The trailing comment is the tag the
# SHA stood for when it was pinned; update both together.
jobs:
test:
name: Test, lint & coverage
runs-on: ubuntu-latest
timeout-minutes: 20
# Nothing here depends on the clock; on the nightly advisory run only
# `supply-chain` has anything new to say.
if: github.event_name != 'schedule'
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
# Pinned by SHA, so the action can no longer read the toolchain out of its
# own ref name (`@stable`) and `toolchain:` has to say it explicitly.
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable
with:
toolchain: stable
components: rustfmt, clippy, llvm-tools-preview
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Install cargo-llvm-cov and cargo-nextest
uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 # v2
with:
tool: cargo-llvm-cov,cargo-nextest
- name: Format
run: cargo fmt --all --check
# `--locked` on every cargo invocation that builds: `Cargo.lock` is
# committed and `Containerfile` already builds against it, so without this
# CI was the one place a semver-compatible upstream release could enter a
# build — including one `cargo deny` (which reads the lockfile) never saw.
- name: Clippy
run: cargo clippy --locked --all-targets -- -D warnings
# Runs the unit + integration tests under instrumentation. `--no-report`
# keeps the raw profiles on disk instead of rendering one report and
# discarding them, so the three `cargo llvm-cov report` invocations below
# are three views of a single test run rather than three test runs.
#
# `nextest` here is not interchangeable with plain `cargo test`: the
# `custom` script tests in `signer`/`filter`/`notify` exec a file they
# just wrote, which intermittently fails `ETXTBSY` when tests share one
# process (see the note on `write_script` in `src/signer/custom.rs`).
- name: Test with coverage
run: cargo llvm-cov nextest --locked --no-report
# The reports themselves, none of which gates: the gate is the next step,
# kept separate so a run that lands below the floor still publishes the
# report saying which file lost the lines. `main.rs` is excluded from
# every view — it is socket/exit wiring, so counting it would move the
# metric without anyone being able to act on it — and the regex has to be
# repeated per invocation, since the filtering happens at report time.
#
# `lcov.info` is the machine-readable form (any coverage viewer, and the
# input a service would take if one is ever added); the HTML tree is what
# a human actually opens from the artifact; the summary goes to the job
# summary so the per-file table is on the run page itself, without
# downloading anything or unfolding a log group.
- name: Coverage reports
run: |
cargo llvm-cov report --lcov --output-path lcov.info \
--ignore-filename-regex 'src/main\.rs'
cargo llvm-cov report --html \
--ignore-filename-regex 'src/main\.rs'
{
echo '## Coverage'
echo
echo '```text'
cargo llvm-cov report --summary-only \
--ignore-filename-regex 'src/main\.rs'
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
# The ratchet, set just below the current line coverage (~97.3%).
#
# `--fail-under-lines` reads the **Lines** column, not the `Regions` one
# the summary prints first — they differ by more than a point here, and
# reading the wrong one makes this look far tighter than it is. A separate
# step from the rendering above so the failure names itself on the run
# page: "Coverage floor" red is a sentence, a `report` step red is a
# question.
- name: Coverage floor
run: >
cargo llvm-cov report --summary-only
--ignore-filename-regex 'src/main\.rs'
--fail-under-lines 97
# `always()`, because the run where the floor was missed is exactly the
# run whose report somebody wants to read. Retained for a fortnight
# rather than the 90-day default: this is a debugging aid tied to one
# commit, not a record.
- name: Upload the coverage report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: coverage-report
path: |
lcov.info
target/llvm-cov/html
retention-days: 14
if-no-files-found: ignore
# `cargo llvm-cov` does not run doc-tests, so the `rust,no_run` startup
# example in `src/lib.rs` is otherwise never compiled — and it calls
# `build_router`, whose signature changes.
- name: Doc tests
run: cargo test --locked --doc
# `cargo test --doc` compiles the *examples*; it says nothing about the
# intra-doc links, of which this crate has a great many — the doc comments
# are where the reasoning lives, and a link pointing at something renamed
# away rots silently otherwise. It caught three on the run that added it.
#
# `private_intra_doc_links` is allowed, and that is a judgement about what
# this crate is: the library exists so the tests and `main.rs` can reach
# it, not as a published API, and the doc comments are written for someone
# reading the source. A public item explaining itself by naming the
# private thing it delegates to is the good outcome; the alternative is
# either widening visibility for rustdoc's benefit or unlinking the name,
# and both are worse than a link a `--document-private-items` build
# resolves.
- name: Documentation
run: cargo doc --locked --no-deps --all-features
env:
RUSTDOCFLAGS: -D warnings -A rustdoc::private_intra_doc_links
# `Cargo.toml` declares `rust-version`, and until this job existed its own
# comment admitted CI did not check it: `test` builds on stable, so the floor
# was a claim rather than a fact. `cargo check` is enough — the point is
# whether the language and dependency features used still compile there, not
# whether the tests pass twice.
msrv:
name: Minimum supported Rust version
runs-on: ubuntu-latest
timeout-minutes: 15
if: github.event_name != 'schedule'
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
# Read out of the manifest rather than written here as well: a floor
# stated in two places is a floor that drifts, and the failure mode is
# silent — CI would go on proving a version nobody claims.
- name: Read rust-version from Cargo.toml
id: msrv
run: |
version=$(grep -m1 '^rust-version' Cargo.toml | cut -d'"' -f2)
test -n "$version" || { echo "Cargo.toml declares no rust-version"; exit 1; }
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable
with:
toolchain: ${{ steps.msrv.outputs.version }}
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Check
run: cargo check --locked --all-targets --all-features
# The `hsm` feature is off by default, so the job above neither builds nor
# lints `src/signer/local_ca/pkcs11.rs` — `--all-targets` does not enable
# features. Without this job the whole PKCS#11 path could rot warning-dirty
# or stop compiling and nobody would find out.
#
# Deliberately separate rather than folded into `test`: that job enforces
# `--fail-under-lines 97`, and a feature-gated file is not compiled at all
# with the feature off, so it stays outside the metric entirely. Folding it in
# would drag the token error-recovery paths — the ones SoftHSM2 cannot easily
# provoke — inside the ratchet and make the floor fight the feature.
hsm:
name: PKCS#11 (SoftHSM2)
runs-on: ubuntu-latest
timeout-minutes: 20
if: github.event_name != 'schedule'
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable
with:
toolchain: stable
components: clippy
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Install cargo-nextest
uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 # v2
with:
tool: cargo-nextest
# Only the module `.so` is needed: the tests create the token and
# generate the CA key through `cryptoki` itself rather than shelling out
# to `pkcs11-tool`, so `opensc` is not a prerequisite. Without softhsm2
# installed the PKCS#11 tests skip rather than fail, which is what keeps
# them runnable on a developer machine — this job is where they actually
# execute.
- name: Install SoftHSM2
run: sudo apt-get update && sudo apt-get install -y softhsm2
- name: Clippy
run: cargo clippy --locked --all-targets --features hsm -- -D warnings
- name: Test
run: cargo nextest run --locked --features hsm
# The book is deployed by `mdbook.yml`, which runs only on a push to `main`.
# Until this job existed, nothing built `doc/` on a pull request — and
# `create-missing = false` makes a `SUMMARY.md` entry with no file a *build
# failure*, so the first sign of a broken chapter was a failed deployment
# after merge. Cheap enough (no Rust toolchain, no cargo build) to run on
# every change rather than only on ones that touch `doc/`: the README and
# `config.toml.example` are checked against the book too.
docs:
name: Documentation
runs-on: ubuntu-latest
timeout-minutes: 10
if: github.event_name != 'schedule'
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
# Pinned, and installed as prebuilt binaries rather than `cargo install`
# from source: `mdbook-version: latest` in the deploy workflow means an
# upstream release can break the build with no change on this side.
- name: Install mdBook
uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 # v2
with:
tool: mdbook@0.5.4,mdbook-mermaid@0.17.0
# `create-missing = false` turns a SUMMARY entry with no file into an
# error here rather than a stub nobody notices.
- name: Build the book
run: mdbook build doc/
# The mechanical half of the house style: 80-column prose, no numbered
# headings, tagged code fences, links and anchors that resolve, and no
# configuration key documented in two files at once. The last one is the
# rule that actually rots — two copies of a default drift silently.
- name: Style and link check
run: python3 doc/lint.py
supply-chain:
name: Advisories, licenses & sources
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
# `deny.toml` is a hand-tuned policy — curated licence allowlist, ban and
# source rules — that nothing was enforcing. For a CA, an unnoticed
# advisory in the crypto or TLS tree is the most expensive kind of miss.
- name: cargo-deny
uses: EmbarkStudios/cargo-deny-action@c3bbe7e4e3f7baeee1a3dd9aec0a3b2aded580fb # v2.1.1
with:
command: check
# `tests/e2e/` drives real ACME clients — certbot, acme.sh, lego — against a
# real `acme-proxy` in containers, and until this job existed CI ran none of
# it: every test is `#[ignore]`d and nothing passed `--run-ignored`. That is
# the largest gap in the suite by surface area, and the lab has already caught
# at least one class of bug the unit tests structurally cannot (hickory
# reporting an empty NOERROR answer as `Err`, fixed in `src/dns.rs`).
#
# Nightly rather than per-push: it builds six container images and starts a
# DNS server plus three client containers per scenario, which is minutes, not
# seconds. A subset — the three challenge types, which is where a real client
# disagrees with this server if anything does.
e2e:
name: End-to-end (real ACME clients)
runs-on: ubuntu-latest
timeout-minutes: 45
if: github.event_name == 'schedule'
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@4cda84d5c5c54efe2404f9d843567869ab1699d4 # stable
with:
toolchain: stable
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Install cargo-nextest
uses: taiki-e/install-action@67729d5c413db75907f0ad1e39bb04b9c868ff60 # v2
with:
tool: cargo-nextest
# A filter expression, not a positional argument: nextest's positional
# filter matches test *names*, and none of these contain "e2e", so
# `cargo nextest run e2e` silently matches nothing and exits 0.
- name: Run the challenge scenarios
run: >
cargo nextest run --locked
-E 'binary(e2e) and (test(test_http_01_order) or test(test_dns_01_order) or test(test_tls_alpn_01_order))'
--run-ignored all