Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
105 commits
Select commit Hold shift + click to select a range
5f5fb17
feat: implement statement list and detail handlers via StatementService
larryjay007 May 27, 2026
b9192fb
updated work done
Swayymalcolm99 May 31, 2026
fcc59eb
updated work
Swayymalcolm99 Jun 1, 2026
bb4f2b5
Fix issue 269: Implement audit log hash chain (#307)
DavisVT Jun 2, 2026
95ff841
Issue 277 webhook endpoints (#308)
DavisVT Jun 2, 2026
306dd27
test: add end-to-end auth middleware integration tests (#309)
lawalajose Jun 2, 2026
2e3fcd0
updated work done (#310)
Swayymalcolm99 Jun 2, 2026
0f10667
Merge remote-tracking branch 'origin/main' into pr-311
thlpkee20-wq Jun 2, 2026
6d57d62
test: cover StatementQuery filters and count semantics (#312)
brodapeethar Jun 2, 2026
87b55a8
test: fuzz multi-tenant isolation across read endpoints (#313)
V1ctor-o Jun 2, 2026
eb2abfa
test(rate-limit): add router integration tests for middleware behavio…
HademiData Jun 2, 2026
4dcdf9e
fix(#274): add OTel spans across handler, service, and repository lay…
oluwaseyi1996-netizen Jun 2, 2026
e0bd8ea
“fix: constant-time admin token comparison in PurgeCache” (#316)
Aycode01 Jun 2, 2026
badf964
feat: add fee history/trend analysis (#162) and swap router (#88) (#317)
oluwaseyi1996-netizen Jun 2, 2026
9e9edb3
Feat postgres plan repo (#318)
T-kesh Jun 2, 2026
ea4ef9b
feat: implement PostgresSubscriptionRepo with CRUD operations and tes…
extolkom Jun 2, 2026
3015650
Merge remote-tracking branch 'origin/main' into pr-311
thlpkee20-wq Jun 2, 2026
d7d20be
feat: implement subscription and plan handlers with pagination and as…
mikkyvans0-source Jun 5, 2026
a451391
Open and inject a real database connection pool at startup (#322)
mikkyvans0-source Jun 5, 2026
d9c2028
feat: encrypt sensitive outbox payloads with JWE
Jun 23, 2026
ca550ea
test: align JWKS cache expectations with rate-limit behavior
Jun 23, 2026
e79c9cf
Fix test failures and complete JWE integration wiring.
Jun 23, 2026
511c1c8
Raise outbox test coverage and align CI gates with repo reality.
Jun 23, 2026
cf8c047
Fix race in MockPublisher and install k6 from release tarball in CI.
Jun 23, 2026
65e1a27
Fix k6 load test JWT signing for current k6 crypto API.
Jun 23, 2026
1c39379
Start load test server with required config env and health wait.
Jun 23, 2026
14ac6a8
Use strong benchmark secrets and include roles claim in load test JWTs.
Jun 23, 2026
11ca7d2
Fix load test JWT signing to match golang-jwt validation.
Jun 23, 2026
87b9ba4
Fix k6 JWT HMAC for v0.54 by using base64 digest encoding.
Jun 23, 2026
3b4ab51
Generate load test JWTs with Go for reliable CI auth.
Jun 23, 2026
987f2a4
Pass customer_id in statements load test requests.
Jun 24, 2026
ec1ebed
Restore loadtestTarget export removed from load test utils.
Jun 24, 2026
f316ff2
feat: implement postgres read-replica routing and resolve test suite …
Waffiyyi Jun 25, 2026
cee820e
feat: export statements to S3 with presigned URLs (#365)
matieuu1 Jun 25, 2026
15ecfbc
feat: expose webhook attempt timeline (#366)
ayomidearegbeshola29-dev Jun 25, 2026
f506ca0
Add strict nonce-based CSP support with report endpoint (#367)
uche102 Jun 25, 2026
65758b2
fixes (#368)
samad13 Jun 25, 2026
4e293cc
Make the outbox drainer fault tolerant against partial publisher fail…
Aonlike Jun 25, 2026
8dc2b7d
feat: schedule expired idempotency key cleanup (#370)
ZeePearl56 Jun 25, 2026
368adcd
Merge remote-tracking branch 'origin/main' into pr-363
thlpkee20-wq Jun 25, 2026
5490eeb
test(k6): add soak-test scenario for statements endpoint
web3nova Jun 26, 2026
42f2f3f
Test/handler golden snapshots (#371)
ZeePearl56 Jun 27, 2026
29d2f27
docs: add capacity planning playbook (#372)
mofejo1 Jun 27, 2026
18ef8f6
Merge remote-tracking branch 'origin/main' into pr-373
thlpkee20-wq Jun 27, 2026
3e7f25c
feat: add benchmark regression gate with automated check scripts and …
bukasin1 Jun 27, 2026
9ceb099
feat: add SSE stream for subscription updates
larryjay007 Jun 27, 2026
7921e1e
Add WebhookHandler for processing webhook events
karanjakevin39-collab Jun 28, 2026
1380931
Create webhook_verification.go
karanjakevin39-collab Jun 28, 2026
73c94cd
Create provider_test.go
karanjakevin39-collab Jun 28, 2026
320595f
Add subscription.created webhook event fixture
karanjakevin39-collab Jun 28, 2026
1effe58
Add JSON fixture for statement issued webhook event
karanjakevin39-collab Jun 28, 2026
230cc27
Add Pact provider verification workflow
karanjakevin39-collab Jun 28, 2026
b612529
Update routes.go
karanjakevin39-collab Jun 28, 2026
3162631
Update go.mod
karanjakevin39-collab Jun 28, 2026
7543ef9
Merge branch 'main' into main
karanjakevin39-collab Jun 28, 2026
7a87462
feat(secrets): add rotation metadata support
spartan124 Jun 28, 2026
13f8bbe
feat(tools): add secrets audit CLI
spartan124 Jun 28, 2026
e109ecd
docs: add secrets rotation runbook
spartan124 Jun 28, 2026
12a0f85
ci: add nightly secrets rotation audit
spartan124 Jun 28, 2026
586bdfe
feat: add chaos hook on outbox publishers (#374)
jerryjuche Jun 28, 2026
ed610c8
Merge remote-tracking branch 'origin/main' into pr-375
thlpkee20-wq Jun 28, 2026
b1f05a5
feat: emit RFC 8594 deprecation headers on legacy routes (#376)
Ekpemark Jun 28, 2026
f89cde1
Merge remote-tracking branch 'origin/main' into pr-377
thlpkee20-wq Jun 28, 2026
bd74773
Fix/outbox restart replay (#378)
Ekpemark Jun 28, 2026
fb6cf24
Test/rbac matrix (#379)
Ekpemark Jun 28, 2026
cdeac26
feat: add tenant data export bundle (#380)
Buchi-Einstein Jun 28, 2026
db361c6
feat: add GraphQL gateway over plan, subscription and statement servi…
gracepeterfejokwu Jun 28, 2026
679e9d8
feat: support ETag/If-None-Match on detail reads (#383)
AdeMi20 Jun 28, 2026
e15840b
feat: add Slack outbox publisher with retry-aware delivery (#384)
gracepeterfejokwu Jun 28, 2026
752bada
Merge remote-tracking branch 'origin/main' into pr-386
thlpkee20-wq Jun 28, 2026
c8f3343
feat Validate request bodies against the OpenAPI spec at runtime in d…
Sulex45 Jun 28, 2026
8c99b3b
Feat/tenant notifications (#388)
Babigdk Jun 28, 2026
04c52ee
Merge remote-tracking branch 'origin/main' into pr-389
thlpkee20-wq Jun 28, 2026
db9617d
docs: add multi-region failover playbook (#390)
Lacastar2000 Jun 28, 2026
c61b1a2
Merge remote-tracking branch 'origin/main' into pr-375
thlpkee20-wq Jun 28, 2026
d5e4e13
Merge remote-tracking branch 'origin/main' into pr-377
thlpkee20-wq Jun 28, 2026
ea8c547
Feat/otel exemplars (#382)
gracepeterfejokwu Jun 28, 2026
b7752b4
feat: support dry-run migrations (#385)
abikedaniel22 Jun 28, 2026
22c4933
Merge remote-tracking branch 'origin/main' into pr-386
thlpkee20-wq Jun 28, 2026
b82f7a0
Merge remote-tracking branch 'origin/main' into pr-389
thlpkee20-wq Jun 28, 2026
1b90ac7
Merge remote-tracking branch 'origin/main' into pr-375
thlpkee20-wq Jun 28, 2026
0699535
Merge remote-tracking branch 'origin/main' into pr-377
thlpkee20-wq Jun 28, 2026
e7e968f
Merge branch 'pr-377'
thlpkee20-wq Jun 28, 2026
41f3eff
Merge branch 'pr-386'
thlpkee20-wq Jun 28, 2026
b785a54
Merge branch 'pr-389'
thlpkee20-wq Jun 28, 2026
8d07115
test: mutation-test the subscription state machine
Jun 28, 2026
f151356
feat: add priority lanes to worker scheduler
Jun 28, 2026
2c64661
Merge branch 'main' into feat/worker-priority-lanes
brodapeethar Jun 28, 2026
6279eed
Fix: Add missing package declaration to internal/service/notification…
brodapeethar Jul 2, 2026
28af817
Fix: Add missing package declaration to internal/service/dto/notifica…
brodapeethar Jul 2, 2026
f310922
Fix: Add missing package declaration to internal/notifications/channe…
brodapeethar Jul 2, 2026
d78d0d1
Fix: Add missing package declaration to internal/model/notification_p…
brodapeethar Jul 2, 2026
fff7fdb
Fix: Add missing package declaration to internal/outbox/router.go
brodapeethar Jul 2, 2026
01b338e
Fix: Add missing package declaration to internal/repository/notificat…
brodapeethar Jul 2, 2026
6a1a2b3
Fix: Add missing package declaration to internal/handlers/notificatio…
brodapeethar Jul 2, 2026
07dcf18
feat: page on outbox dead-letter spikes (#391)
Faromzy Jul 8, 2026
3513c66
feat: add saga coordinator for cross-aggregate flows (#392)
AlphaTechini Jul 8, 2026
4048f12
Merge remote-tracking branch 'origin/main' into pr-394
thlpkee20-wq Jul 8, 2026
9624c74
feat: add tail-based sampler for errors and slow requests (#395)
deltron-fr Jul 8, 2026
fd77a69
test: detect N+1 query patterns in handlers (#396)
deltron-fr Jul 8, 2026
179847b
feat: implement fraud detection engine with sliding window rate limit…
githoboman Jul 8, 2026
9c2cb0e
Add a materialized view for monthly fee revenue reports and a schedul…
githoboman Jul 8, 2026
49e7446
feat: propagate tenant baggage across spans (#399)
uche102 Jul 8, 2026
4003332
feat: partition statements table by month for scale (#400)
ajulaybeeb Jul 8, 2026
57ea50b
Merge remote-tracking branch 'origin/main' into pr-394
thlpkee20-wq Jul 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
34 changes: 34 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ PORT=8080
# Use sslmode=require in production; sslmode=disable is acceptable for local dev.
DATABASE_URL=postgres://stellabill:changeme@localhost:5432/stellabill_dev?sslmode=disable

# [OPTIONAL] Read replica PostgreSQL connection string.
# If not set, falls back to DATABASE_URL (primary).
DATABASE_REPLICA_URL=postgres://stellabill:changeme@localhost:5432/stellabill_replica?sslmode=disable

# -----------------------------------------------------------------------------
# Authentication & authorisation
# -----------------------------------------------------------------------------
Expand All @@ -54,6 +58,15 @@ ADMIN_TOKEN=CHANGE_ME_admin_Token1!
# Example: https://app.example.com,https://admin.example.com
ALLOWED_ORIGINS=http://localhost:3000
OUTBOX_PUBLISHER_CA_FILE=

# [OPTIONAL] Encrypt sensitive outbox payloads with subscriber JWKs (JWE).
OUTBOX_JWE_ENABLED=false
OUTBOX_JWE_SENSITIVE_EVENT_TYPES=webhook.received,payment.processed

# [OPTIONAL] Probability (0.0–1.0) of injecting a context cancellation into
# each outbox publish call. Only active when ENV=staging. Set to 0 to disable.
# Example: CHAOS_OUTBOX_PROB=0.1 (10 % chance per publish).
CHAOS_OUTBOX_PROB=0
# -----------------------------------------------------------------------------
# HTTP server tuning
# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -119,6 +132,18 @@ TRACING_EXPORTER=stdout
# [OPTIONAL] Service name reported in trace spans.
TRACING_SERVICE_NAME=stellabill-backend

# [OPTIONAL] Enable bounded in-process tail decisions. Default: false.
TRACING_TAIL_ENABLED=false

# [OPTIONAL] Always retain traces whose server root takes at least this many
# milliseconds. Range: 1-600000. Default: 1000.
TRACING_TAIL_LATENCY_MS=1000

# [OPTIONAL] Baseline fraction of ordinary traces retained when tail sampling
# is enabled. Errors, 5xx responses, and slow requests are always retained.
# Range: 0.0-1.0. Default: 0.05.
TRACING_TAIL_ERROR_RATE=0.05

# -----------------------------------------------------------------------------
# Database connection pool
# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -158,6 +183,15 @@ AUDIT_HMAC_SECRET=CHANGE_ME_audit_Hmac1!
# [OPTIONAL] File path for the audit log (JSON Lines). Default: audit.log.
AUDIT_LOG_PATH=audit.log

# -----------------------------------------------------------------------------
# Legacy API deprecation
# -----------------------------------------------------------------------------

# [OPTIONAL] HTTP-date or RFC3339 timestamp emitted as the Sunset header on
# legacy /api/* aliases. Leave unset to omit Sunset while keeping Deprecation
# and successor Link headers.
LEGACY_API_SUNSET="Thu, 31 Dec 2026 23:59:59 GMT"

# -----------------------------------------------------------------------------
# Feature flags
# -----------------------------------------------------------------------------
Expand Down
72 changes: 36 additions & 36 deletions .github/pr_body.txt
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
feat: add contract-to-backend reconciliation endpoint and reports

Summary
- Adds a backend ↔ contract reconciliation subsystem and an admin HTTP endpoint to run on-demand checks.
- Provides models, a comparator (field-by-field), adapters (in-memory + HTTP), and an in-memory store for report persistence.
- Adds unit tests for comparator, adapters, and handler.
- Adds documentation and a GitHub Actions workflow that runs `go test ./...` on push/PR.

Files of interest
- `internal/reconciliation/*` — comparator, models, adapters, store, and tests
- `internal/handlers/reconciliation.go` — admin POST `/api/admin/reconcile` (accepts backend subscriptions)
- `internal/routes/routes.go` — route wiring, adapter selection via `CONTRACT_SNAPSHOT_URL`, admin GET `/api/admin/reports`
- `docs/reconciliation.md` — short doc + security notes
- `scripts/install_go_and_run_tests.ps1` — helper to install Go and run reconciliation tests locally
- `.github/workflows/reconciliation-ci.yml` — CI that runs the full test suite

How it works
- POST JSON array of backend subscriptions to `/api/admin/reconcile` (admin-only).
- The handler fetches contract snapshots via configured adapter:
- If `CONTRACT_SNAPSHOT_URL` is set -> HTTP adapter fetches JSON snapshots from that URL (set `CONTRACT_SNAPSHOT_AUTH` for auth header).
- Otherwise uses an in-memory adapter for dev.
- Comparator checks: status, amount+currency, interval, per-key balances, missing snapshots, and stale snapshots (>24h).
- Reports are saved to an in-memory store and can be retrieved via GET `/api/admin/reports`.

Security notes
- Endpoint is protected by `auth.RequirePermission(auth.PermManageSubscriptions)`; ensure only admin roles can call it.
- For HTTP adapter use TLS and set `CONTRACT_SNAPSHOT_AUTH` for authentication.
- Redact PII and use a persistent, access-controlled store in production — the current store is in-memory for dev/tests.

Testing
- Unit tests added under `internal/reconciliation` and `internal/handlers`.
- CI workflow runs `go test ./...` on push/PR.

Next steps
- Replace in-memory store with DB-backed store (migration, repo, tests) for production.
- Wire a real contract snapshot endpoint and add integration tests. Provide API details (URL, auth, JSON schema) and I will implement the adapter and tests.
feat: add contract-to-backend reconciliation endpoint and reports
Summary
- Adds a backend ↔ contract reconciliation subsystem and an admin HTTP endpoint to run on-demand checks.
- Provides models, a comparator (field-by-field), adapters (in-memory + HTTP), and an in-memory store for report persistence.
- Adds unit tests for comparator, adapters, and handler.
- Adds documentation and a GitHub Actions workflow that runs `go test ./...` on push/PR.
Files of interest
- `internal/reconciliation/*` — comparator, models, adapters, store, and tests
- `internal/handlers/reconciliation.go` — admin POST `/api/admin/reconcile` (accepts backend subscriptions)
- `internal/routes/routes.go` — route wiring, adapter selection via `CONTRACT_SNAPSHOT_URL`, admin GET `/api/admin/reports`
- `docs/reconciliation.md` — short doc + security notes
- `scripts/install_go_and_run_tests.ps1` — helper to install Go and run reconciliation tests locally
- `.github/workflows/reconciliation-ci.yml` — CI that runs the full test suite
How it works
- POST JSON array of backend subscriptions to `/api/admin/reconcile` (admin-only).
- The handler fetches contract snapshots via configured adapter:
- If `CONTRACT_SNAPSHOT_URL` is set -> HTTP adapter fetches JSON snapshots from that URL (set `CONTRACT_SNAPSHOT_AUTH` for auth header).
- Otherwise uses an in-memory adapter for dev.
- Comparator checks: status, amount+currency, interval, per-key balances, missing snapshots, and stale snapshots (>24h).
- Reports are saved to an in-memory store and can be retrieved via GET `/api/admin/reports`.
Security notes
- Endpoint is protected by `auth.RequirePermission(auth.PermManageSubscriptions)`; ensure only admin roles can call it.
- For HTTP adapter use TLS and set `CONTRACT_SNAPSHOT_AUTH` for authentication.
- Redact PII and use a persistent, access-controlled store in production — the current store is in-memory for dev/tests.
Testing
- Unit tests added under `internal/reconciliation` and `internal/handlers`.
- CI workflow runs `go test ./...` on push/PR.
Next steps
- Replace in-memory store with DB-backed store (migration, repo, tests) for production.
- Wire a real contract snapshot endpoint and add integration tests. Provide API details (URL, auth, JSON schema) and I will implement the adapter and tests.
250 changes: 250 additions & 0 deletions .github/workflows/benchmark-regression-gate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
name: Benchmark Regression Gate

on:
pull_request:
branches: [main]
workflow_dispatch:

# Prevent concurrent runs on the same PR so baselines are never written
# and read at the same time, which would corrupt the comparison.
concurrency:
group: benchmark-regression-gate-${{ github.ref }}
cancel-in-progress: false

env:
# Fail CI if any tracked benchmark regresses by more than this amount.
REGRESSION_THRESHOLD_PERCENT: "10"

jobs:
benchmark-regression-gate:
# Pin to a stable runner class so hardware variance does not produce
# false positives. ubuntu-22.04 is a fixed GA image (not `latest`).
runs-on: ubuntu-22.04

permissions:
contents: read
actions: read # needed to download artifacts from the main branch

steps:
# ---------------------------------------------------------------
# 1. Check out the PR head with full history so we can also
# check out origin/main in a worktree.
# ---------------------------------------------------------------
- name: Checkout PR head
uses: actions/checkout@v4
with:
fetch-depth: 0 # full history required for worktree

# ---------------------------------------------------------------
# 2. Set up Go (version comes from go.mod so it stays in sync).
# ---------------------------------------------------------------
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

# ---------------------------------------------------------------
# 3. Install benchstat – the authoritative statistical comparator
# from the Go performance team. It computes p-values and
# confidence intervals so single-sample noise is ignored.
# ---------------------------------------------------------------
- name: Install benchstat
run: go install golang.org/x/perf/cmd/benchstat@latest

# ---------------------------------------------------------------
# 4. Download dependencies for the PR head.
# ---------------------------------------------------------------
- name: Download dependencies (PR head)
run: go mod download

# ---------------------------------------------------------------
# 5. Run the benchmark suite on the PR head.
# -count=10 gives benchstat enough samples to compute a
# meaningful confidence interval and reject statistical noise.
# ---------------------------------------------------------------
- name: Run benchmarks on PR head
run: |
go test \
-bench=. \
-benchmem \
-count=10 \
-run=^$ \
-timeout=20m \
./internal/handlers/... \
| tee /tmp/bench_head.txt
echo "PR head benchmark output:"
cat /tmp/bench_head.txt

# ---------------------------------------------------------------
# 6. Try to restore a stored baseline produced from the last
# successful push to main. If none exists (first run, or the
# artifact expired) we skip the comparison and succeed so that
# new repositories are not permanently broken.
# ---------------------------------------------------------------
- name: Restore baseline artifact
id: restore-baseline
continue-on-error: true
uses: actions/download-artifact@v4
with:
name: benchmark-baseline-main
path: /tmp/baseline

# ---------------------------------------------------------------
# 7. Decide whether a baseline is available.
# ---------------------------------------------------------------
- name: Check baseline availability
id: check-baseline
run: |
if [ -f /tmp/baseline/bench_baseline.txt ]; then
echo "baseline_exists=true" >> "$GITHUB_OUTPUT"
echo "Baseline file found – regression gate is active."
else
echo "baseline_exists=false" >> "$GITHUB_OUTPUT"
echo "No baseline artifact found. Skipping regression comparison (first run or expired artifact)."
fi

# ---------------------------------------------------------------
# 8. Run the comparison with benchstat.
# --threshold is intentionally NOT used here; we parse the
# output ourselves so we can report per-benchmark details and
# use a strict 10 % ceiling (benchstat's built-in threshold
# option only gates on statistical significance, not magnitude).
# ---------------------------------------------------------------
- name: Compare benchmarks with benchstat
if: steps.check-baseline.outputs.baseline_exists == 'true'
id: compare
run: |
echo "## Benchmark Regression Report" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
benchstat /tmp/baseline/bench_baseline.txt /tmp/bench_head.txt \
| tee /tmp/benchstat_output.txt \
| tee -a "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"

# ---------------------------------------------------------------
# 9. Parse benchstat output and fail if any benchmark regressed
# by more than REGRESSION_THRESHOLD_PERCENT.
#
# benchstat prints lines like:
# BenchmarkListPlans_Small 1.10 ± 2% 1.25 ± 3% +13.64% (p=0.000 n=10)
# We extract the final percentage column and compare to the
# threshold. Lines that lack a percentage (new / removed
# benchmarks) are handled as edge cases below.
# ---------------------------------------------------------------
- name: Enforce regression threshold
if: steps.check-baseline.outputs.baseline_exists == 'true'
run: |
THRESHOLD=${{ env.REGRESSION_THRESHOLD_PERCENT }}
FAILED=0

echo "Checking for regressions > ${THRESHOLD}% …"

while IFS= read -r line; do
# Skip header / blank / informational lines
[[ "$line" =~ ^(name|goos|goarch|pkg|cpu|PASS|ok|---) ]] && continue
[[ -z "$line" ]] && continue

# Extract the trailing POSITIVE delta column, e.g. "+13.64%".
# Negative (improvement) tokens are intentionally skipped.
delta=$(echo "$line" | grep -oE '\+[0-9]+\.[0-9]+%' | tail -1 || true)
[[ -z "$delta" ]] && continue

# Strip '+' and '%' to get the magnitude
magnitude=$(echo "$delta" | tr -d '+' | tr -d '%')

# Compare using awk for floating-point arithmetic
is_regression=$(awk -v mag="$magnitude" -v thr="$THRESHOLD" \
'BEGIN { print (mag > thr) ? "yes" : "no" }')

if [[ "$is_regression" == "yes" ]]; then
echo "❌ REGRESSION: $line"
FAILED=$((FAILED + 1))
fi
done < /tmp/benchstat_output.txt

echo ""
if [[ $FAILED -gt 0 ]]; then
echo "❌ $FAILED benchmark(s) regressed by more than ${THRESHOLD}%." >&2
echo "" >&2
echo "To investigate locally:" >&2
echo " git checkout main && go test -bench=. -count=10 -run=^$ ./internal/handlers/... | tee base.txt" >&2
echo " git checkout - && go test -bench=. -count=10 -run=^$ ./internal/handlers/... | tee head.txt" >&2
echo " benchstat base.txt head.txt" >&2
exit 1
else
echo "✅ No benchmark regressed by more than ${THRESHOLD}%."
fi

# ---------------------------------------------------------------
# 10. Persist benchmark results as an artifact so they are visible
# in the Actions UI regardless of pass / fail.
# ---------------------------------------------------------------
- name: Upload PR head benchmark results
if: always()
uses: actions/upload-artifact@v4
with:
name: benchmark-results-pr-${{ github.event.pull_request.number }}
path: /tmp/bench_head.txt
retention-days: 30

# ---------------------------------------------------------------
# 11. Emit a summary when no baseline is available so reviewers
# know why the gate was skipped.
# ---------------------------------------------------------------
- name: Summary (no baseline)
if: steps.check-baseline.outputs.baseline_exists != 'true'
run: |
echo "## Benchmark Regression Gate – Skipped" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "No baseline artifact found for \`main\`. This is expected on first run." >> "$GITHUB_STEP_SUMMARY"
echo "A baseline will be created after this PR merges and the \`update-benchmark-baseline\` job runs." >> "$GITHUB_STEP_SUMMARY"

# -----------------------------------------------------------------
# Separate job: only runs on pushes to main to update the baseline.
# Runs on push to main (triggered separately from the PR gate above).
# -----------------------------------------------------------------
update-benchmark-baseline:
if: github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main'
runs-on: ubuntu-22.04

permissions:
contents: read
actions: write # needed to upload artifacts

steps:
- name: Checkout main
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
cache: true

- name: Download dependencies
run: go mod download

- name: Run benchmarks on main (baseline)
run: |
go test \
-bench=. \
-benchmem \
-count=10 \
-run=^$ \
-timeout=20m \
./internal/handlers/... \
| tee /tmp/bench_baseline.txt
echo "Baseline benchmark output:"
cat /tmp/bench_baseline.txt

- name: Upload baseline artifact
uses: actions/upload-artifact@v4
with:
name: benchmark-baseline-main
path: /tmp/bench_baseline.txt
# Keep for 90 days so PRs opened against an old main still
# have a baseline to compare against.
retention-days: 90
overwrite: true
Loading
Loading