CI Webhook Integration
The Project Tracker exposes a CI webhook endpoint that automatically updates CI_PIPELINE gate criteria based on pipeline results. This enables stage-gate workflows where stages cannot progress until CI passes.
Overview
| Aspect | Detail |
|---|---|
| Endpoint | POST /api/webhooks/ci |
| Auth | HMAC-SHA256 signature (x-hub-signature-256 header) |
| Scoping | x-ci-source-id header → project mapping via CI_SOURCE_MAP |
| Replay protection | Monotonic run_id per source/project |
| Audit | Durable auditCritical entry for tenanted projects; structured log for null-tenant (legacy) projects |
How it works
- A CI pipeline step posts build/test/coverage/lint results to the endpoint.
- The handler verifies the HMAC signature and resolves the project from the source ID.
- Stale payloads (where
run_id≤ the last accepted value) are rejected. - All
CI_PIPELINEgate criteria on pending gates for the project are evaluated against configurable thresholds. - Criteria are marked MET or NOT_MET. For AUTOMATIC gates, if all criteria are now met, the stage auto-progresses.
Environment variables
| Variable | Required | Description |
|---|---|---|
CI_SOURCE_MAP | Yes | JSON mapping source IDs to project UUIDs: {"my-repo-ci": "<uuid>"} |
CI_WEBHOOK_SECRETS | Yes (per source) | JSON mapping source IDs to HMAC secrets: {"my-repo-ci": "<secret>"}. Uses exact key matching — no case folding or character substitution. Every source that hits the endpoint must have its own entry; there is no shared fallback. |
CI_GATE_MIN_COVERAGE | No | Minimum coverage % (default: 80) |
CI_GATE_MAX_LINT_ERRORS | No | Maximum lint errors allowed (default: 0) |
Payload format
{
"run_id": 147001,
"build_status": "success",
"tests_passed": 42,
"tests_failed": 0,
"coverage_percent": 87.5,
"lint_errors": 0
}
| Field | Type | Required | Description |
|---|---|---|---|
run_id | integer | Yes | Monotonically increasing across runs and reruns. See below. |
build_status | "success" | "failure" | Yes | Overall build outcome |
tests_passed | integer | Yes | Passing test count |
tests_failed | integer | Yes | Failing test count |
coverage_percent | number | No | Code coverage % (evaluated against CI_GATE_MIN_COVERAGE) |
lint_errors | integer | No | Lint error count (evaluated against CI_GATE_MAX_LINT_ERRORS) |
run_id and reruns
The run_id must be strictly increasing so that a stale "success" payload cannot overwrite a newer "failure". GitHub Actions run_number does not increment on "Re-run failed jobs", so using it raw would cause a passing rerun to be rejected.
Compute run_id * 1000 + run_attempt in the shell step instead — github.run_id is unique per workflow run and github.run_attempt increments on each rerun. GitHub expressions don't support arithmetic, so the multiplication must happen in bash.
GitHub Actions example
- name: Report CI results to Project Tracker
if: always()
env:
CI_SECRET: ${{ secrets.PT_CI_WEBHOOK_SECRET }}
GH_RUN_ID: ${{ github.run_id }}
GH_RUN_ATTEMPT: ${{ github.run_attempt }}
GH_JOB_STATUS: ${{ job.status }}
run: |
# Monotonic across reruns: run_id * 1000 + attempt (supports up to 999 reruns).
# Computed in shell because GitHub expressions don't support arithmetic.
MONO_RUN_ID=$(( GH_RUN_ID * 1000 + GH_RUN_ATTEMPT ))
# Map job.status (success | failure | cancelled) to build_status (success | failure).
# Doing this in bash keeps STATUS as a raw token, so `jq --arg` handles all JSON quoting.
if [ "$GH_JOB_STATUS" = "success" ]; then STATUS=success; else STATUS=failure; fi
PAYLOAD=$(jq -n \
--argjson run_id "$MONO_RUN_ID" \
--arg status "$STATUS" \
'{run_id: $run_id, build_status: $status, tests_passed: 42, tests_failed: 0}')
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$CI_SECRET" | awk '{print $2}')
curl -X POST https://constellation.planetb2b.com/projects/api/webhooks/ci \
-H "Content-Type: application/json" \
-H "x-ci-source-id: my-repo-ci" \
-H "x-hub-signature-256: sha256=$SIGNATURE" \
-d "$PAYLOAD"
Creating CI_PIPELINE gate criteria
CI_PIPELINE criteria can only be added to AUTOMATIC gates. In the Project Tracker UI:
- Open a project → Stages settings.
- Select or create a stage with an AUTOMATIC gate.
- Add a criterion with type CI Pipeline.
- The criterion starts as NOT_MET and is updated by the next CI webhook.
Data model
The projects.ci_run_log table tracks the last accepted run per source/project:
| Column | Type | Description |
|---|---|---|
source_id | TEXT | CI source identifier (from x-ci-source-id header) |
project_id | UUID | FK to projects.projects |
tenant_id | UUID | Tenant isolation (standard RLS) |
last_run_id | BIGINT | Last accepted monotonic run ID |
updated_at | TIMESTAMPTZ | Timestamp of last update |
Threshold evaluation
A CI result is considered MET when all of the following hold:
- Build status is
success - Zero test failures
- Coverage ≥
CI_GATE_MIN_COVERAGE(if coverage is reported) - Lint errors ≤
CI_GATE_MAX_LINT_ERRORS(if lint count is reported)
If any condition fails, the criterion is marked NOT_MET and the failure reasons are returned in the response.