Skip to main content

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

AspectDetail
EndpointPOST /api/webhooks/ci
AuthHMAC-SHA256 signature (x-hub-signature-256 header)
Scopingx-ci-source-id header → project mapping via CI_SOURCE_MAP
Replay protectionMonotonic run_id per source/project
AuditDurable auditCritical entry for tenanted projects; structured log for null-tenant (legacy) projects

How it works

  1. A CI pipeline step posts build/test/coverage/lint results to the endpoint.
  2. The handler verifies the HMAC signature and resolves the project from the source ID.
  3. Stale payloads (where run_id ≤ the last accepted value) are rejected.
  4. All CI_PIPELINE gate criteria on pending gates for the project are evaluated against configurable thresholds.
  5. Criteria are marked MET or NOT_MET. For AUTOMATIC gates, if all criteria are now met, the stage auto-progresses.

Environment variables

VariableRequiredDescription
CI_SOURCE_MAPYesJSON mapping source IDs to project UUIDs: {"my-repo-ci": "<uuid>"}
CI_WEBHOOK_SECRETSYes (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_COVERAGENoMinimum coverage % (default: 80)
CI_GATE_MAX_LINT_ERRORSNoMaximum 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
}
FieldTypeRequiredDescription
run_idintegerYesMonotonically increasing across runs and reruns. See below.
build_status"success" | "failure"YesOverall build outcome
tests_passedintegerYesPassing test count
tests_failedintegerYesFailing test count
coverage_percentnumberNoCode coverage % (evaluated against CI_GATE_MIN_COVERAGE)
lint_errorsintegerNoLint 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:

  1. Open a project → Stages settings.
  2. Select or create a stage with an AUTOMATIC gate.
  3. Add a criterion with type CI Pipeline.
  4. 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:

ColumnTypeDescription
source_idTEXTCI source identifier (from x-ci-source-id header)
project_idUUIDFK to projects.projects
tenant_idUUIDTenant isolation (standard RLS)
last_run_idBIGINTLast accepted monotonic run ID
updated_atTIMESTAMPTZTimestamp of last update

Threshold evaluation

A CI result is considered MET when all of the following hold:

  1. Build status is success
  2. Zero test failures
  3. Coverage ≥ CI_GATE_MIN_COVERAGE (if coverage is reported)
  4. 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.