ci.yml 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438
  1. # Main quality gate (task 42).
  2. #
  3. # Per-PR legs (all must be green, except `coverage` which is report-only):
  4. # fmt, clippy, audit, deny, sqlx-prepare-check, nextest, doc-tests,
  5. # coverage (llvm-cov report upload over lib unit tests; non-blocking),
  6. # fuzz-build (nightly leg inside `fuzz/`, so the pinned stable
  7. # `rust-toolchain.toml` at the root is never violated).
  8. # Scheduled legs (not per-PR):
  9. # mutants (weekly budgeted mutation run over the pure security modules),
  10. # fuzz-run (bounded run of every fuzz target; the per-PR leg only builds),
  11. # miri (scoped interpreter run over the sync crypto/JOSE helpers;
  12. # allow-failure, never gates).
  13. #
  14. # Must NOT gate on miri for tokio/sqlx/redis; no lint suppressions anywhere.
  15. # The `miri` leg therefore runs only sync unit tests (`crypto::{ct, pkce,
  16. # tokens}` and the sync subset of `jose::jar` — the fetch-decode,
  17. # request-URI-membership, and alg-rejection tests, which short-circuit before
  18. # any crypto or I/O), excluding the argon2 KDF
  19. # (`crypto::secret`, cost prohibitive under the interpreter), the `ring`-backed
  20. # modules (`crypto::field`, `crypto::totp`, whose C/asm calls the interpreter
  21. # cannot execute), and every
  22. # `#[tokio::test]` / redis-backed test (DPoP replay store, async claim
  23. # paths, including the async `jose::jar::resolve` path itself — only its sync
  24. # unit tests are selected); the whole leg is `continue-on-error` so it can
  25. # never gate a PR. If a `#[tokio::test]` or a real-verification test is ever
  26. # added under `jose::jar`, narrow the filter to the explicit sync test paths.
  27. name: ci
  28. on:
  29. push:
  30. branches: ["master"]
  31. pull_request: {}
  32. schedule:
  33. # Weekly budgeted mutation run (Sunday 03:00 UTC).
  34. - cron: "0 3 * * 0"
  35. workflow_dispatch: {}
  36. permissions:
  37. contents: read
  38. env:
  39. CARGO_TERM_COLOR: always
  40. SQLX_OFFLINE: "true"
  41. # Shared compilation cache: sccache runs in each compiling job below (except
  42. # `miri`, whose cargo-miri overrides RUSTC_WRAPPER itself). It writes to a
  43. # local on-disk dir (SCCACHE_DIR) persisted by actions/cache -- NOT the GitHub
  44. # Actions cache backend, which stores one cache entry per compiled object
  45. # (thousands per run, per-ref duplicated); SCCACHE_GHA_ENABLED is deliberately
  46. # left unset. Incremental is useless on ephemeral runners and harms cross-run
  47. # cache hits.
  48. SCCACHE_DIR: ${{ github.workspace }}/.sccache
  49. CARGO_INCREMENTAL: "0"
  50. jobs:
  51. fmt:
  52. name: fmt
  53. runs-on: ubuntu-24.04
  54. steps:
  55. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
  56. - uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable
  57. with:
  58. components: rustfmt
  59. - name: Check formatting
  60. run: cargo fmt --check
  61. clippy:
  62. name: clippy
  63. runs-on: ubuntu-24.04
  64. env:
  65. RUSTC_WRAPPER: sccache
  66. steps:
  67. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
  68. - uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable
  69. with:
  70. components: clippy
  71. - name: Cache sccache directory
  72. uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
  73. with:
  74. path: ${{ env.SCCACHE_DIR }}
  75. key: sccache-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'fuzz/Cargo.lock') }}
  76. restore-keys: |
  77. sccache-${{ runner.os }}-
  78. - name: Start sccache (shared compilation cache)
  79. uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
  80. with:
  81. version: "v0.18.0"
  82. - name: Clippy (all targets, warnings denied)
  83. run: cargo clippy --all-targets -- -D warnings
  84. audit:
  85. name: audit
  86. runs-on: ubuntu-24.04
  87. steps:
  88. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
  89. - uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable
  90. - name: Install cargo-audit
  91. uses: taiki-e/install-action@ab6895351a88b908e96a4697860d4ffea5e57dbe # v2
  92. with:
  93. tool: cargo-audit
  94. - name: Audit advisories
  95. run: cargo audit
  96. deny:
  97. name: deny
  98. runs-on: ubuntu-24.04
  99. steps:
  100. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
  101. - uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable
  102. - name: Install cargo-deny
  103. uses: taiki-e/install-action@ab6895351a88b908e96a4697860d4ffea5e57dbe # v2
  104. with:
  105. tool: cargo-deny
  106. - name: Check supply chain (ring-only TLS enforced in deny.toml)
  107. run: cargo deny check
  108. sqlx-prepare-check:
  109. name: sqlx-prepare-check
  110. runs-on: ubuntu-24.04
  111. env:
  112. RUSTC_WRAPPER: sccache
  113. services:
  114. postgres:
  115. image: postgres:16
  116. env:
  117. POSTGRES_USER: postgres
  118. POSTGRES_PASSWORD: sqlxcheck
  119. POSTGRES_DB: postgres
  120. ports:
  121. - 5432:5432
  122. options: >-
  123. --health-cmd "pg_isready -U postgres"
  124. --health-interval 5s
  125. --health-timeout 5s
  126. --health-retries 10
  127. steps:
  128. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
  129. - uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable
  130. - name: Cache sccache directory
  131. uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
  132. with:
  133. path: ${{ env.SCCACHE_DIR }}
  134. key: sccache-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'fuzz/Cargo.lock') }}
  135. restore-keys: |
  136. sccache-${{ runner.os }}-
  137. - name: Start sccache (shared compilation cache)
  138. uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
  139. with:
  140. version: "v0.18.0"
  141. - name: Install sqlx-cli
  142. uses: taiki-e/install-action@ab6895351a88b908e96a4697860d4ffea5e57dbe # v2
  143. with:
  144. tool: sqlx-cli@0.9.0
  145. - name: Apply migrations to the check database
  146. env:
  147. DATABASE_URL: postgres://postgres:sqlxcheck@127.0.0.1:5432/postgres
  148. run: cargo sqlx migrate run
  149. - name: Fail if the committed .sqlx cache is stale
  150. env:
  151. DATABASE_URL: postgres://postgres:sqlxcheck@127.0.0.1:5432/postgres
  152. run: cargo sqlx prepare --check --workspace
  153. doc-links:
  154. name: doc-links
  155. runs-on: ubuntu-24.04
  156. steps:
  157. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
  158. - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
  159. with:
  160. python-version: "3.x"
  161. - name: Check documentation links (markdown-link-check equivalent)
  162. run: python3 scripts/check_doc_links.py
  163. nextest:
  164. name: nextest
  165. runs-on: ubuntu-24.04
  166. env:
  167. RUSTC_WRAPPER: sccache
  168. steps:
  169. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
  170. - uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable
  171. - name: Cache sccache directory
  172. uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
  173. with:
  174. path: ${{ env.SCCACHE_DIR }}
  175. key: sccache-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'fuzz/Cargo.lock') }}
  176. restore-keys: |
  177. sccache-${{ runner.os }}-
  178. - name: Start sccache (shared compilation cache)
  179. uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
  180. with:
  181. version: "v0.18.0"
  182. - name: Install cargo-nextest
  183. uses: taiki-e/install-action@ab6895351a88b908e96a4697860d4ffea5e57dbe # v2
  184. with:
  185. tool: nextest
  186. - name: Install redis-server (in-process Sentinel/Cluster fixtures)
  187. run: sudo apt-get update && sudo apt-get install -y redis-server
  188. - name: Start shared test services (Postgres + Redis)
  189. run: docker compose -f compose.test.yaml up -d
  190. - name: Full test suite (shared services; testcontainers for lifecycle)
  191. run: cargo nextest run --all-targets
  192. - name: Stop shared test services
  193. if: always()
  194. run: docker compose -f compose.test.yaml down -v
  195. doc-tests:
  196. name: doc-tests
  197. runs-on: ubuntu-24.04
  198. env:
  199. RUSTC_WRAPPER: sccache
  200. steps:
  201. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
  202. - uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable
  203. - name: Cache sccache directory
  204. uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
  205. with:
  206. path: ${{ env.SCCACHE_DIR }}
  207. key: sccache-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'fuzz/Cargo.lock') }}
  208. restore-keys: |
  209. sccache-${{ runner.os }}-
  210. - name: Start sccache (shared compilation cache)
  211. uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
  212. with:
  213. version: "v0.18.0"
  214. - name: Documentation tests
  215. run: cargo test --doc
  216. fuzz-build:
  217. name: fuzz-build
  218. runs-on: ubuntu-24.04
  219. env:
  220. RUSTC_WRAPPER: sccache
  221. defaults:
  222. run:
  223. working-directory: fuzz
  224. steps:
  225. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
  226. # The `fuzz/rust-toolchain.toml` in the working directory selects
  227. # nightly via rustup override; the root stable pin is untouched.
  228. - name: Install cargo-fuzz (nightly, per fuzz/rust-toolchain.toml)
  229. uses: taiki-e/install-action@ab6895351a88b908e96a4697860d4ffea5e57dbe # v2
  230. with:
  231. tool: cargo-fuzz
  232. - name: Cache sccache directory
  233. uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
  234. with:
  235. path: ${{ env.SCCACHE_DIR }}
  236. key: sccache-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'fuzz/Cargo.lock') }}
  237. restore-keys: |
  238. sccache-${{ runner.os }}-
  239. - name: Start sccache (shared compilation cache)
  240. uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
  241. with:
  242. version: "v0.18.0"
  243. # Explicit gnu target: the prebuilt cargo-fuzz binary is musl-static,
  244. # so its default target is musl (ASan-incompatible, and musl std is
  245. # not installed). Never drop this flag without checking the default.
  246. - name: Build all fuzz targets
  247. run: cargo fuzz build --target x86_64-unknown-linux-gnu
  248. fuzz-run:
  249. name: fuzz-run
  250. # Scheduled bounded fuzzing. `fuzz-build` (above) only compiles the
  251. # targets per PR; this leg actually executes them, so a crash is found
  252. # without a human starting it. It is bounded and scheduled, not 24/7:
  253. # continuous coverage is ClusterFuzzLite (`.clusterfuzzlite/`, docs in
  254. # `docs/CONTRIBUTING.md`); the remaining owner step is enabling it.
  255. if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
  256. runs-on: ubuntu-24.04
  257. env:
  258. RUSTC_WRAPPER: sccache
  259. timeout-minutes: 70
  260. strategy:
  261. fail-fast: false
  262. matrix:
  263. target: [token_form, dcr_json, jar_claims, dpop_proof, jar_verify, redirect_uri, scope_parse]
  264. defaults:
  265. run:
  266. working-directory: fuzz
  267. steps:
  268. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
  269. # `fuzz/rust-toolchain.toml` selects nightly; the root stable pin is
  270. # untouched, exactly as in `fuzz-build`.
  271. - name: Install cargo-fuzz (nightly)
  272. uses: taiki-e/install-action@ab6895351a88b908e96a4697860d4ffea5e57dbe # v2
  273. with:
  274. tool: cargo-fuzz
  275. - name: Cache sccache directory
  276. uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
  277. with:
  278. path: ${{ env.SCCACHE_DIR }}
  279. key: sccache-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'fuzz/Cargo.lock') }}
  280. restore-keys: |
  281. sccache-${{ runner.os }}-
  282. - name: Start sccache (shared compilation cache)
  283. uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
  284. with:
  285. version: "v0.18.0"
  286. # `uses` steps resolve paths from the workspace root, not the job's
  287. # `working-directory`; keep the fuzz/ prefix explicit.
  288. - name: Restore corpus for ${{ matrix.target }}
  289. uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
  290. with:
  291. path: fuzz/corpus/${{ matrix.target }}
  292. key: fuzz-corpus-${{ matrix.target }}-${{ github.run_id }}
  293. restore-keys: |
  294. fuzz-corpus-${{ matrix.target }}-
  295. # Same explicit target as `fuzz-build`: cargo-fuzz defaults to the
  296. # installer binary's own platform (musl), not the runner's (gnu).
  297. - name: Fuzz ${{ matrix.target }} (bounded)
  298. run: cargo fuzz run --target x86_64-unknown-linux-gnu ${{ matrix.target }} -- -max_total_time=600 -rss_limit_mb=2048
  299. # Save only on success; a per-run key grows the corpus while the prefix
  300. # `restore-keys` always pick the newest entry.
  301. - name: Save corpus for ${{ matrix.target }}
  302. if: success()
  303. uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
  304. with:
  305. path: fuzz/corpus/${{ matrix.target }}
  306. key: fuzz-corpus-${{ matrix.target }}-${{ github.run_id }}
  307. - name: Upload crash artifacts
  308. if: failure()
  309. uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
  310. with:
  311. name: fuzz-crash-${{ matrix.target }}
  312. path: fuzz/artifacts/${{ matrix.target }}
  313. if-no-files-found: ignore
  314. mutants:
  315. name: mutants
  316. # Scheduled budgeted job only: never blocks a PR.
  317. if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
  318. runs-on: ubuntu-24.04
  319. env:
  320. RUSTC_WRAPPER: sccache
  321. timeout-minutes: 60
  322. steps:
  323. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
  324. - uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable
  325. - name: Cache sccache directory
  326. uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
  327. with:
  328. path: ${{ env.SCCACHE_DIR }}
  329. key: sccache-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'fuzz/Cargo.lock') }}
  330. restore-keys: |
  331. sccache-${{ runner.os }}-
  332. - name: Start sccache (shared compilation cache)
  333. uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
  334. with:
  335. version: "v0.18.0"
  336. - name: Install cargo-mutants
  337. uses: taiki-e/install-action@ab6895351a88b908e96a4697860d4ffea5e57dbe # v2
  338. with:
  339. tool: cargo-mutants
  340. - name: Budgeted mutation run (see .cargo/mutants.toml)
  341. run: cargo mutants --timeout 1800 --test-tool nextest
  342. coverage:
  343. name: coverage
  344. # Report-only coverage signal (T4-4). Non-blocking by design: the upload
  345. # is the deliverable; there is no `--fail-under-*` threshold yet.
  346. # Services mirror `nextest` (shared Postgres/Redis plus redis-server),
  347. # because several lib unit tests are testcontainers-backed.
  348. # Follow-up: add a threshold and extend beyond `--lib` once
  349. # the first reports land.
  350. runs-on: ubuntu-24.04
  351. env:
  352. RUSTC_WRAPPER: sccache
  353. continue-on-error: true
  354. steps:
  355. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
  356. - uses: dtolnay/rust-toolchain@686976e191b89faba57d3206551f0f330d8cb249 # stable
  357. with:
  358. components: llvm-tools-preview
  359. - name: Cache sccache directory
  360. uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
  361. with:
  362. path: ${{ env.SCCACHE_DIR }}
  363. key: sccache-${{ runner.os }}-${{ hashFiles('Cargo.lock', 'fuzz/Cargo.lock') }}
  364. restore-keys: |
  365. sccache-${{ runner.os }}-
  366. - name: Start sccache (shared compilation cache)
  367. uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
  368. with:
  369. version: "v0.18.0"
  370. - name: Install cargo-llvm-cov
  371. uses: taiki-e/install-action@ab6895351a88b908e96a4697860d4ffea5e57dbe # v2
  372. with:
  373. tool: cargo-llvm-cov
  374. - name: Install redis-server (in-process Sentinel/Cluster fixtures)
  375. run: sudo apt-get update && sudo apt-get install -y redis-server
  376. - name: Start shared test services (Postgres + Redis)
  377. run: docker compose -f compose.test.yaml up -d
  378. - name: Coverage report (lib unit tests only; integration tests stay in `nextest`)
  379. run: cargo llvm-cov --lib --lcov --output-path lcov.info
  380. - name: Stop shared test services
  381. if: always()
  382. run: docker compose -f compose.test.yaml down -v
  383. - name: Upload coverage report
  384. if: always()
  385. uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
  386. with:
  387. name: coverage-lcov
  388. path: lcov.info
  389. if-no-files-found: error
  390. miri:
  391. name: miri
  392. # Scoped miri leg (T4-4). Scheduled only: the interpreter is slow and
  393. # nightly-only, so this never runs per-PR and never gates
  394. # (`continue-on-error`). Nightly is confined to this job; the root
  395. # `rust-toolchain.toml` stable pin is untouched.
  396. # Scope is the sync unit tests of the dependency-free helpers, plus the
  397. # sync subset of `jose::jar` (fetch-decode, request-URI-membership, and
  398. # alg-rejection tests only — never the async `resolve` path).
  399. # Explicitly excluded, per the header constraint: tokio (`#[tokio::test]`), sqlx, and redis
  400. # (DPoP replay store, async claim paths), plus the argon2 KDF
  401. # (`crypto::secret`, cost prohibitive under the interpreter) and the
  402. # `ring`-backed modules (`crypto::field`, `crypto::totp`, whose C/asm
  403. # calls Miri cannot interpret).
  404. if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
  405. runs-on: ubuntu-24.04
  406. timeout-minutes: 60
  407. continue-on-error: true
  408. steps:
  409. - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
  410. - uses: dtolnay/rust-toolchain@f0d5300ba8908d942c808f4d31c08471d7798ba3 # nightly
  411. with:
  412. components: miri
  413. - name: Scoped miri run (sync crypto/JOSE helpers only)
  414. env:
  415. # Relaxed isolation for the first scheduled runs (wall-clock/env
  416. # access); tighten in a follow-up once green.
  417. MIRIFLAGS: -Zmiri-disable-isolation
  418. run: >
  419. cargo miri test --lib --
  420. crypto::ct crypto::pkce crypto::tokens
  421. jose::jar