fix: collection fails loud -- admin pre-check, per-stage error budget, non-zero exit

Problem

Collection could ship empty or broken data while exiting green.

  • A non-admin Personal Access Token (PAT) gets HTTP 200 with truncated data, not HTTP 403. A run "succeeds" and renders a believable-but-empty dashboard that silently undercounts.
  • The orchestrator checked the error budget only after the events stage. Merge-request, issue, and pipeline errors then accumulated unchecked, so a failing instance kept going.
  • A whole-stage merge-request or issue failure was swallowed: Run() returned nil, wrote a manifest, and the process exited 0. A stale or empty dashboard published green in continuous integration (CI).

Fix

  1. Admin pre-check. CheckAdmin fetches the authenticated user (GET /api/v4/user) early, before the expensive collection, and aborts loudly when the token user is not an instance admin. New --skip-admin-check flag (cmd -> collector config) is the escape hatch for intentional scoped runs.
  2. Per-stage error budget. The budget check now runs after the merge-request, issue, and pipeline stages too, not just events, so a failing instance aborts promptly.
  3. Fail loud on fatal failure. A whole-stage merge-request or issue collection failure is now fatal: Run() returns a non-nil error and writes no success-looking artifact for the failed stage. Tolerable partial failures (per-item errors below the budget) remain a success, recorded in the manifest. cmd/collect.go and cmd/run.go already propagate the Run() error, so the process exits non-zero.

Validation

  • make check passes (go vet + full test suite).
  • go test -race ./... passes.
  • New tests: admin pre-check abort on non-admin, proceed on admin, bypass with --skip-admin-check; per-stage budget abort on pipeline-stage errors; fatal-stage error return with no artifact written.
  • Fails-on-old proof (per-stage budget): with the post-pipeline budget check temporarily reverted, TestRun_PerStageBudget_PipelineErrorsAbort FAILS (Run() returns nil, collects through). With the check restored it PASSES (aborts with "too many errors"). Both observed locally.

🤖 Generated with Claude Code

Merge request reports

Loading