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
# nanodock - CI Pipeline
# Enforces code quality on pull requests. All checks must pass before merging.
#
# Gates (ALL must pass):
# 1. cargo fmt --check -> consistent formatting
# 2. cargo clippy (deny) -> zero lint warnings (all+pedantic+nursery)
# 3. cargo test --locked -> all tests pass, Cargo.lock respected
# 4. cargo bench --no-run -> benchmarks compile without errors
# 5. cargo build -> library compiles
# 6. cargo doc -> documentation + doc-comment format valid
# 7. cargo deny check -> no vulnerable/banned dependencies
#
# Linux benchmark execution:
# - gungraun instruction-count regressions vs PR merge-base
name: CI
on:
push:
branches:
pull_request:
branches:
# Principle of least privilege - CI only reads source, never pushes.
permissions:
contents: read
# Cancel any in-progress run for the same PR branch to avoid wasted minutes.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: "-D warnings"
jobs:
quality-gate:
name: Quality Gate (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
os:
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # 1.94.1
with:
toolchain: stable
components: rustfmt, clippy
- name: Cache Rust dependencies and build artifacts
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
# Gate 1: Formatting - rejects inconsistent style
- name: "Gate 1: cargo fmt --check"
run: cargo fmt --all -- --check
# Gate 2: Clippy (all+pedantic+nursery at deny level) - rejects lint violations.
# Lint levels and per-lint overrides are configured in Cargo.toml [lints.clippy].
- name: "Gate 2: cargo clippy"
run: cargo clippy --locked --all-targets -- -D warnings
# Gate 3: Tests - rejects broken logic; --locked ensures Cargo.lock is honoured
- name: "Gate 3: cargo test"
run: |
cargo test --locked --lib --tests
cargo test --locked --doc
# Gate 4: Benchmarks - rejects benchmarks that do not compile
- name: "Gate 4: cargo bench --no-run"
run: cargo bench --locked --no-run
# Gate 5: Build - rejects code that does not compile
- name: "Gate 5: cargo build"
run: cargo build --locked
# Gate 6: Documentation & doc-comment format - rejects broken links, bare URLs,
# invalid code blocks, and missing crate-level docs.
- name: "Gate 6: cargo doc"
run: cargo doc --locked --no-deps
env:
RUSTDOCFLAGS: >-
-D warnings
-D rustdoc::bare_urls
-D rustdoc::invalid_rust_codeblocks
-D rustdoc::private_intra_doc_links
-D rustdoc::unescaped_backticks
# Linux-only instruction-count benchmarks with gungraun.
#
# Gungraun executes under valgrind, so benchmark execution is unavailable on
# Windows runners. Pull requests compare the PR head against a merge-base
# baseline from `main` and fail on instruction regressions above the chosen
# Ir limit. Pushes to `main` still run the benchmark suite as a smoke check.
benchmark-regression:
name: Benchmark Regression (Linux)
runs-on: ubuntu-latest
timeout-minutes: 30
env:
GUNGRAUN_SEPARATE_TARGETS: "true"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9 # 1.94.1
with:
toolchain: stable
- name: Cache Rust dependencies and build artifacts
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
cache-on-failure: true
key: bench
- name: Install Valgrind
run: |
sudo apt-get update
sudo apt-get install -y valgrind
- name: Install gungraun-runner
run: |
version=$(cargo metadata --format-version=1 | python -c "import json, sys; data = json.load(sys.stdin); print(next(pkg['version'] for pkg in data['packages'] if pkg['name'] == 'gungraun'))")
cargo install --locked gungraun-runner --version "$version"
- name: Resolve merge-base
id: base
if: github.event_name == 'pull_request'
run: |
base=$(git merge-base HEAD origin/${{ github.base_ref }})
echo "sha=$base" >> "$GITHUB_OUTPUT"
echo "Merge-base: $base"
- name: Run benchmarks on merge-base (establish baseline)
if: github.event_name == 'pull_request'
run: |
head_sha=$(git rev-parse HEAD)
git checkout --detach ${{ steps.base.outputs.sha }}
cargo bench --locked --bench benchmarks -- \
--save-baseline main \
--callgrind-metrics=ir \
--save-summary=json
git checkout --detach "$head_sha"
- name: Run PR benchmark comparison
if: github.event_name == 'pull_request'
run: |
cargo bench --locked --bench benchmarks -- \
--baseline main \
--callgrind-metrics=ir \
--callgrind-limits='ir=1.0%' \
--save-summary=json \
| tee bench_output.txt
- name: Run benchmark smoke check on main pushes
if: github.event_name != 'pull_request'
run: |
cargo bench --locked --bench benchmarks \
-- \
--callgrind-metrics=ir \
--save-summary=json \
| tee bench_output.txt
- name: Upload benchmark reports
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
with:
name: benchmark-reports-${{ github.sha }}
path: |
bench_output.txt
target/gungraun
retention-days: 7
if-no-files-found: warn
# Gate 7: Dependency audit - rejects vulnerable/banned/unlicensed dependencies.
# Uses EmbarkStudios/cargo-deny-action (Docker-based, pre-built binary - no
# manual install or caching needed). Advisories are split into a separate
# matrix entry with continue-on-error so a sudden new advisory announcement
# does not block unrelated PRs.
audit:
name: Dependency Audit (${{ matrix.checks }})
runs-on: ubuntu-latest
timeout-minutes: 10
strategy:
matrix:
checks:
- advisories
- bans licenses sources
# A new advisory should warn, not block unrelated work.
continue-on-error: ${{ matrix.checks == 'advisories' }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: "Gate 7: cargo deny check ${{ matrix.checks }}"
uses: EmbarkStudios/cargo-deny-action@82eb9f621fbc699dd0918f3ea06864c14cc84246 # v2
with:
command: check ${{ matrix.checks }}