Deploy Gate
How it works
Section titled “How it works”POST /evaluate/deploy evaluates a deploy event (target system, environment, image, rollout
strategy) against your organization’s Controls and returns a 4-state verdict: go,
conditional_go, no_go, or insufficient_context. The call is deterministic — Forgecroft
answers from Postgres traversal, no LLM inference, no variable cost, sub-second latency. Wire it
into the deploy job itself (after tests and lint pass, before the rollout step runs) so a change
that is green on every other check can still be stopped by a deploy or binding Control.
This is not the CI Gate (POST /evaluate, kind=pr_diff, gates
a pull request against a diff). The deploy gate evaluates a deploy event — it runs in the
deploy job, not the PR workflow, and it has no composite action or binary: one curl step is the
entire integration.
Prerequisites
Section titled “Prerequisites”- A Forgecroft API key with
evaluate:readscope, stored as a repository or organization secret (e.g.FORGECROFT_API_KEY). Pass it via environment variable only — never echo it to logs, never place it in command arguments. - The System you are deploying already exists in Forgecroft (
system_slugmatches your Project slug) and carries at least one deploy or binding Control — otherwise the gate has nothing to check and returnsinsufficient_contextor a scope-match-onlyconditional_go.
Quickstart
Section titled “Quickstart”Add this step to your deploy job, after your test/lint/build steps and before the step that actually rolls out the change:
name: Deploy
on: push: branches: - main
jobs: deploy: runs-on: ubuntu-latest steps: # ... your existing checkout / build / test / lint steps ...
- name: Forgecroft deploy gate id: deploy_gate env: FORGECROFT_API_KEY: ${{ secrets.FORGECROFT_API_KEY }} run: | set -euo pipefail
response=$(curl -sS -w '\n%{http_code}' -X POST \ "https://api.forgecroft.com/evaluate/deploy" \ -H "Authorization: Bearer ${FORGECROFT_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "system_slug": "my-service", "environment": "production", "image_ref": "'"${IMAGE_REF}"'", "deploy_strategy": "rolling" }')
http_code=$(echo "$response" | tail -n1) body=$(echo "$response" | sed '$d') echo "$body"
if [ "$http_code" -ge 400 ]; then echo "::error::forgecroft deploy gate: HTTP ${http_code} — ${body}" exit 1 fi
verdict=$(echo "$body" | jq -r '.decision.verdict') echo "verdict=${verdict}" >> "$GITHUB_OUTPUT" echo "Forgecroft deploy gate verdict: ${verdict}"
if [ "$verdict" != "go" ]; then echo "::error::forgecroft deploy gate blocked this rollout (verdict: ${verdict}). ${body}" exit 1 fi
- name: Roll out run: ./deploy.sh # only reached if the gate step above exited 0Replace my-service with your system_slug, and ${IMAGE_REF} with however your job derives the
image reference (a prior build step’s output, github.sha, etc).
Request body
Section titled “Request body”| Field | Required | Description |
|---|---|---|
system_slug | yes | Slug of the System (= Project slug) being deployed. |
environment | no | Target environment (e.g. production, staging). Checked against any deploy_environment_allowlist Control. |
image_ref | no | Container image reference being deployed. Surfaced in the response and the append-only Evidence ledger. |
deploy_strategy | no | Rollout strategy (e.g. rolling, blue_green, canary). Checked against any deploy_strategy_allowlist Control. |
exception_id | no | UUID of an active, non-expired Exception covering a matched Control. When valid, downgrades a conditional_go to go. Fail-closed — any validation failure (expired, wrong approver, coverage mismatch) leaves the verdict unchanged. |
The four verdict states
Section titled “The four verdict states”| Verdict | Meaning | Recommended CI exit code |
|---|---|---|
go | No blocking findings; at least one Control was matched and evaluated clean. Safe to proceed. | 0 — continue to rollout |
conditional_go | A Control matched but only produced concern-severity findings, or a matched Control declared no deploy checks (scope-match-only). Proceed with caution. | Exit non-zero to block by default; teams that want this non-blocking can special-case it, but the quickstart above treats anything other than go as a stop |
no_go | At least one matched Control produced a blocker-severity finding (e.g. the environment or rollout strategy is outside the declared allowlist). | Non-zero — block the rollout |
insufficient_context | No Controls matched this system at all. Forgecroft had nothing to say — this is not the same as a clean pass. | Non-zero in the quickstart above (fail-closed); teams comfortable deploying ungoverned systems can treat this as non-blocking, but that is an explicit choice, not the default |
The quickstart step above blocks on anything other than go. This is fail-closed by design: a
deploy pipeline should not treat “the gate had nothing to say” the same as “the gate said yes.”
Response shape
Section titled “Response shape”{ "kind": "deploy_event", "system_slug": "my-service", "system_id": "...", "environment": "production", "image_ref": "ghcr.io/acme/my-service:sha-abc123", "deploy_strategy": "rolling", "controls": [{"id": "...", "intent_text": "...", "severity_tier": "...", "framework_tags": [...]}], "decision": { "verdict": "no_go", "verdict_label": "blocked", "reasoning": "...", "confidence": 0.0, "blast_radius": { "provable_surface": 1.0, "known_consumers": 3, "unprovable_gap": 0, "recommendation": "..." } }, "findings": [ { "rule_id": "env-gate", "check_kind": "deploy_environment_allowlist", "severity": "blocker", "subject": "my-service", "control_id": "...", "message": "environment \"dev\" is not in the allowed list [staging production]" } ], "coverage_notes": []}blast_radius reflects the System’s Components in the graph (via contains edges) —
known_consumers is the count of Components the graph can account for; provable_surface is
how much of the downstream surface Forgecroft can actually see. It is 0.0 when no import graph
is available for this system yet — treat the verdict as advisory, not a green light, until it is.
Every deploy event that matches at least one Control appends one append-only Evidence row
(source=observation) per matched Control — the audit ledger for what was decided and why,
regardless of whether the verdict blocked the rollout.