Skip to content

Merge pull request #859 from devgbmuyiwa/fix/381-environment-management #237

Merge pull request #859 from devgbmuyiwa/fix/381-environment-management

Merge pull request #859 from devgbmuyiwa/fix/381-environment-management #237

# Latency Budget Enforcement CI
#
# Runs the CLI cold-start and command-latency Criterion benchmarks on every
# push / pull request and checks the results against the project's latency
# budgets. Fails the workflow when a statistically meaningful regression is
# detected, helping to keep the CLI snappy for all users.
#
# See CLI_LATENCY_BUDGETS.md for budget definitions and environment overrides.
name: Latency Budget
on:
push:
branches: [master, main]
paths:
- '**.rs'
- 'Cargo.toml'
- 'Cargo.lock'
- 'benches/**'
- '.github/workflows/benchmark-latency.yml'
pull_request:
paths:
- '**.rs'
- 'Cargo.toml'
- 'Cargo.lock'
- 'benches/**'
- '.github/workflows/benchmark-latency.yml'
# Allow manual trigger for ad-hoc budget checks.
workflow_dispatch:
permissions:
contents: read
pull-requests: write
jobs:
latency-budget:
name: Latency Budget Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y libudev-dev
# Build the project first so benchmark compilation is faster.
- name: Build project
run: cargo build --locked
# Run only the latency-relevant benchmark groups. This keeps CI fast
# while still exercising cold-start and command-dispatch paths.
- name: Run latency benchmarks
run: |
mkdir -p target/criterion
# Criterion's harness takes a SINGLE positional filter, which it
# treats as a regex. Passing the three group names as separate
# arguments made it exit 2 with
# "unexpected argument 'cli_command_latency' found", so this step
# failed before any budget was ever measured. Use one alternation.
cargo bench --locked -- \
'cli_cold_start|cli_command_latency|latency_budget' \
2>&1 | tee target/criterion/latency-bench-output.txt
# Parse Criterion output and check against latency budgets.
# The check-latency-budgets.sh script extracts median values from the
# default Criterion stdout format and compares them against the budgets
# defined in the bash script (which mirror src/utils/latency_budget.rs).
- name: Parse and check latency budget report
id: budget-check
run: |
chmod +x scripts/check-latency-budgets.sh
# Capture stdout only. The script writes its JSON report to stdout and
# progress notes to stderr; folding stderr in with 2>&1 made `jq` fail
# with "Invalid numeric literal". Because the result was then compared
# only against the literal "false", an unparseable report silently
# counted as a pass -- this gate could not fail. Let stderr flow to the
# job log instead.
REPORT=$(bash scripts/check-latency-budgets.sh \
--input target/criterion/latency-bench-output.txt \
--report-path target/criterion/latency-budget-report.json) || EXIT_CODE=$?
echo "::group::Latency Budget Report"
echo "$REPORT"
echo "::endgroup::"
ALL_PASS=$(printf '%s' "$REPORT" | jq -r '.all_pass' 2>/dev/null || echo "unparseable")
echo "all_pass=$ALL_PASS" >> "$GITHUB_OUTPUT"
# Fail closed: only an explicit `true` is a pass. A missing or
# unparseable report is a failure, not a free pass.
if [ "$ALL_PASS" != "true" ]; then
echo "❌ Latency budget check did not pass (all_pass=$ALL_PASS)"
echo "failures=true" >> "$GITHUB_OUTPUT"
exit 1
fi
echo "✅ All latency budgets met."
- name: Upload Criterion report (artefact)
if: always()
uses: actions/upload-artifact@v4
with:
name: criterion-latency-report
path: |
target/criterion/cli_cold_start/
target/criterion/cli_command_latency/
target/criterion/latency_budget/
target/criterion/latency-bench-output.txt
target/criterion/latency-budget-report.json
# Post a PR comment with the budget check summary when run on a PR.
# Posting the summary is a courtesy, not the gate: the budget check step
# above is what fails the job. A `pull_request` from a fork gets a
# read-only GITHUB_TOKEN, so createComment returns 403
# ("Resource not accessible by integration"). That used to fail this job
# even when every budget passed.
- name: Comment PR with budget summary
if: github.event_name == 'pull_request' && always()
continue-on-error: true
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const reportPath = 'target/criterion/latency-budget-report.json';
let summary = '## ⏱ CLI Latency Budget Check\n\n';
if (fs.existsSync(reportPath)) {
const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
let details = '';
for (const check of report.checks) {
const icon = check.status === 'PASS' ? '✅' :
check.status === 'FAIL' ? '❌' :
check.status === 'NOISY' ? '⚠️' :
check.status === 'SKIPPED' ? '⏭️' : '❗';
summary += `| ${icon} ${check.budget} | ${check.budget_max_ms} ms | ${check.actual_median_ms} ms | ${check.status} |\n`;
}
if (report.any_fail) summary += '\n❌ **Some budgets were violated.**';
else summary += '\n✅ **All budgets met.**';
} else {
summary += '_No latency budget report was generated._';
}
try {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: summary
});
} catch (error) {
core.warning(
`Could not post the latency budget comment (this is expected ` +
`for pull requests from forks, which get a read-only token): ` +
`${error.message}`
);
core.summary.addRaw(summary).write();
}