4646 CARGO_TERM_COLOR : always # Enable colored output in CI logs
4747 CARGO_REGISTRIES_CRATES_IO_PROTOCOL : sparse # Use sparse registry protocol
4848 RUST_BACKTRACE : short # Provide backtraces without excessive noise
49- RUSTFLAGS : " -D warnings" # Treat warnings as errors
5049
5150permissions :
5251 contents : read
53- pull-requests : write
5452 actions : read
55- checks : write
5653
5754jobs :
5855 # Fast-fail checks run before the expensive test matrix
5956 quick-check :
6057 name : Quick Checks
6158 runs-on : ubuntu-latest
59+ timeout-minutes : 15
6260 steps :
6361 - name : Checkout repository
6462 uses : actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -89,11 +87,26 @@ jobs:
8987 env :
9088 RUSTDOCFLAGS : " -D warnings"
9189
90+ - name : Validate changelog format
91+ run : |
92+ if [ ! -f "CHANGELOG.md" ]; then
93+ echo "⚠️ CHANGELOG.md not found (will be created by release-plz)"
94+ exit 0
95+ fi
96+
97+ if ! grep -q "^# Changelog" CHANGELOG.md; then
98+ echo "❌ Invalid changelog format - missing '# Changelog' header"
99+ exit 1
100+ fi
101+
102+ echo "✅ Changelog format is valid"
103+
92104 # Security audit runs in parallel with quick-check
93105 # Continues on error since advisory warnings shouldn't block PRs
94106 security :
95107 name : Security Audit
96108 runs-on : ubuntu-latest
109+ timeout-minutes : 20
97110 continue-on-error : true # Advisory warnings are informational only
98111 steps :
99112 - name : Checkout repository
@@ -130,12 +143,13 @@ jobs:
130143 with :
131144 name : security-audit
132145 path : audit.json
133- retention-days : 30
146+ retention-days : 90 # Long-term retention for compliance/security artifacts
134147
135148 # Unit tests run across multiple platforms
136149 unit-tests :
137150 name : Unit Tests ${{ matrix.name }}
138151 needs : [quick-check]
152+ timeout-minutes : 30
139153 strategy :
140154 fail-fast : false
141155 matrix :
@@ -187,14 +201,18 @@ jobs:
187201 sudo apt-get install -y musl-tools
188202
189203 - name : Run unit tests
190- run : cargo test --locked --target ${{ matrix.target }} ${{ matrix.test_args }} -- --test-threads=1
204+ run : cargo test --locked --target ${{ matrix.target }} ${{ matrix.test_args }}
191205
192206 # Generates code coverage report using tarpaulin
193207 # Uploads to Codecov and posts PR comment with coverage summary
194208 coverage :
195209 name : Code Coverage
196210 needs : [unit-tests]
197211 runs-on : ubuntu-latest
212+ timeout-minutes : 45
213+ permissions :
214+ contents : read
215+ pull-requests : write # Required to post coverage comments on PRs
198216 steps :
199217 - name : Checkout repository
200218 uses : actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
@@ -214,6 +232,7 @@ jobs:
214232 uses : cargo-bins/cargo-binstall@38e8f5e4c386b611d51e8aa997b9a06a3c8eb67a # v1.15.6
215233
216234 - name : Install and run tarpaulin
235+ id : tarpaulin
217236 run : |
218237 cargo binstall --force --no-confirm --locked cargo-tarpaulin || cargo install --locked cargo-tarpaulin
219238
@@ -222,14 +241,25 @@ jobs:
222241 cargo install --locked cargo-tarpaulin
223242 fi
224243
225- # Run tarpaulin and capture exit code
226- # Use --no-fail-fast to ensure XML is generated even if coverage is low
227- cargo tarpaulin --timeout 120 --avoid-cfg-tarpaulin || {
228- EXIT_CODE=$?
229- echo "⚠️ Tarpaulin exited with code $EXIT_CODE (possibly due to coverage threshold)"
230- echo "Continuing to upload coverage data..."
231- exit 0 # Don't fail the step
232- }
244+ # Run tarpaulin - differentiate between test failures and coverage threshold
245+ set +e # Don't exit script on error
246+ cargo tarpaulin --timeout 120 --avoid-cfg-tarpaulin
247+ EXIT_CODE=$?
248+ set -e
249+
250+ # Store exit code for later evaluation
251+ echo "exit_code=$EXIT_CODE" >> "$GITHUB_OUTPUT"
252+
253+ if [ $EXIT_CODE -eq 0 ]; then
254+ echo "✅ Coverage passed (above threshold)"
255+ elif [ $EXIT_CODE -eq 2 ]; then
256+ echo "⚠️ Coverage below threshold - will fail after uploading report"
257+ echo "Uploading report for analysis..."
258+ else
259+ echo "❌ Tarpaulin failed (exit code: $EXIT_CODE)"
260+ echo "This indicates test failures or compilation errors"
261+ exit $EXIT_CODE # Fail immediately on real errors
262+ fi
233263
234264 - name : Upload coverage to Codecov
235265 if : always()
@@ -240,18 +270,35 @@ jobs:
240270 name : codecov-umbrella
241271 fail_ci_if_error : false
242272
273+ - name : Determine if PR comment should be posted
274+ id : check-pr
275+ if : always()
276+ run : |
277+ SHOULD_COMMENT="false"
278+
279+ # Check all required conditions
280+ if [[ "${{ github.event_name }}" == "pull_request" ]] && \
281+ [[ "${{ github.event.pull_request.head.repo.fork }}" == "false" ]] && \
282+ [[ -f "target/tarpaulin/cobertura.xml" ]]; then
283+ SHOULD_COMMENT="true"
284+ fi
285+
286+ echo "should_comment=${SHOULD_COMMENT}" >> "$GITHUB_OUTPUT"
287+ echo "PR comment needed: ${SHOULD_COMMENT}"
288+
243289 - name : Install coverage parser
244- if : always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && hashFiles('target/tarpaulin/cobertura.xml') != ''
245- run : npm install fast-xml-parser@5.2.5 dedent@1.7.0 --no-save
290+ if : always() && steps.check-pr.outputs.should_comment == 'true'
291+ working-directory : .github/scripts/coverage-comment
292+ run : npm ci
246293
247294 - name : Comment coverage on PR
248- if : always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && hashFiles('target/tarpaulin/cobertura.xml') != ' '
295+ if : always() && steps.check-pr.outputs.should_comment == 'true '
249296 uses : actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
250297 with :
251298 script : |
252299 const fs = require('fs');
253- const { XMLParser } = require('fast-xml-parser');
254- const dedent = require('dedent');
300+ const { XMLParser } = require('.github/scripts/coverage-comment/node_modules/ fast-xml-parser');
301+ const dedent = require('.github/scripts/coverage-comment/node_modules/ dedent');
255302
256303 const parser = new XMLParser({
257304 ignoreAttributes: false,
@@ -342,23 +389,33 @@ jobs:
342389 body
343390 });
344391
392+ - name : Enforce coverage threshold
393+ if : always() && steps.tarpaulin.outputs.exit_code == '2'
394+ run : |
395+ echo "❌ Coverage is below the required threshold (70%)"
396+ echo "Review the coverage report uploaded to Codecov for details"
397+ exit 1
398+
345399 # Final status check required by branch protection
346400 ci-success :
347401 name : CI Success
348- if : always()
402+ # Use GitHub's native conditional syntax for clearer intent
403+ # Job runs only if all critical jobs pass and security doesn't fail
404+ if : |
405+ always() &&
406+ needs.quick-check.result == 'success' &&
407+ needs.unit-tests.result == 'success' &&
408+ needs.coverage.result == 'success' &&
409+ (needs.security.result == 'success' || needs.security.result == 'skipped')
349410 needs : [quick-check, security, unit-tests, coverage]
350411 runs-on : ubuntu-latest
412+ timeout-minutes : 5
413+ permissions :
414+ contents : read
415+ checks : write # Needed for merge queue status checks
351416 steps :
352- - name : Check status
353- run : |
354- # Check all job results
355- if [[ "${{ needs.quick-check.result }}" != "success" ]] ||
356- [[ "${{ needs.unit-tests.result }}" != "success" ]] ||
357- [[ "${{ needs.coverage.result }}" != "success" ]]; then
358- echo "❌ CI failed"
359- exit 1
360- fi
361- echo "✅ All CI checks passed"
417+ - name : Report success
418+ run : echo "✅ All CI checks passed"
362419
363420 - name : Set merge queue status
364421 if : github.event_name == 'merge_group'
0 commit comments