What's new in v0.9.17
v0.9.17 gives the batch subcommands a distinct exit code for a tooling failure, so a CI pipeline can tell "perf-sentinel could not run" apart from "the quality gate was breached". Until now both exited 1, so a missing trace file, a malformed config, or a broken pipe on the report write was indistinguishable from a genuine regression a gate is meant to block. A pipeline had to infer the difference from file existence or step ordering, and a flaky input could read as a hard quality failure. This release closes that gap with a single, stable code.
A distinct exit code for tooling failures
The batch subcommands (analyze, report, diff, tempo, jaeger-query, pg-stat, mysql-stat, calibrate, explain, bench, demo) now exit 75 for a runtime tooling or internal failure that reaches perf-sentinel's own code: a missing or unreadable input, config, acknowledgments, or baseline file, malformed trace, config, or acknowledgments data, a tempo or jaeger-query fetch failure, an explain trace that is not found, or a failure writing the SARIF, JSON, or HTML output.
Exit 1 is now reserved for one thing, a genuine quality-gate breach on the three gated invocations, analyze --ci, tempo --ci, and jaeger-query --ci. When a real regression and a report-write failure happen at once, the gate breach wins, so a regression on a broken pipe or a full disk still exits 1 rather than the tolerable 75, never masked as a passing run. Invalid arguments and unsupported flag combinations exit 2, matching clap's own usage-error code.
The value 75 is a fixed sentinel matching the || exit 75 the GitLab CI template already used at the shell level (its origin is EX_TEMPFAIL in sysexits.h). perf-sentinel emits it for permanent failures too, so it is not a request to retry: read it as "perf-sentinel could not run", not "try again later". The three official CI templates and docs/CI.md document how each maps the code.
Upgrade impact
One contract changes. A pipeline that specifically checks exit code == 1 to mean "quality gate breached" must now also treat 75 as a distinct, non-gate failure, otherwise a tooling error reads as a gate breach. A pipeline that only checks for a non-zero exit code needs no change, 75 is still an error. See docs/CI.md "Exit codes".
Why this is a patch and not a minor
No subcommand, flag, output shape, or file format changes. The only behavior change is that failures which already failed with 1 now fail with 75 unless they are a real gate breach or a usage error, a finer signal on the same non-zero outcome, with one documented migration step for pipelines that branch on the exact code.
Validation
The exit-code contract is exercised end to end by the CLI test suite, which spawns the binary and asserts the exact code on each path: a gate breach exits 1, a malformed or missing input, a failed tempo and jaeger-query fetch, an explain trace-not-found, and a malformed pg-stat and mysql-stat source each exit 75, and an unsupported flag combination exits 2. The full Rust suite passes green, clippy is clean at -D warnings on Rust 1.97.1, and the default and --no-default-features builds both pass.
Verifying this release
# Binary integrity via SLSA Build L3 attestation
gh attestation verify perf-sentinel-linux-amd64 \
--repo robintra/perf-sentinel
# A periodic disclosure produced by this binary
perf-sentinel verify-hash --report perf-sentinel-report.json \
--expected-identity "https://github.com/robintra/perf-sentinel/.github/workflows/release.yml@refs/tags/v0.9.17" \
--expected-issuer "https://token.actions.githubusercontent.com" \
--verify-binary ./perf-sentinel-linux-amd64gh CLI 2.49 or newer required for gh attestation verify.
Full Changelog: v0.9.15...v0.9.17