Skip to content

Deploy Gate

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.

  • A Forgecroft API key with evaluate:read scope, 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_slug matches your Project slug) and carries at least one deploy or binding Control — otherwise the gate has nothing to check and returns insufficient_context or a scope-match-only conditional_go.

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 0

Replace 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).

FieldRequiredDescription
system_slugyesSlug of the System (= Project slug) being deployed.
environmentnoTarget environment (e.g. production, staging). Checked against any deploy_environment_allowlist Control.
image_refnoContainer image reference being deployed. Surfaced in the response and the append-only Evidence ledger.
deploy_strategynoRollout strategy (e.g. rolling, blue_green, canary). Checked against any deploy_strategy_allowlist Control.
exception_idnoUUID 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.
VerdictMeaningRecommended CI exit code
goNo blocking findings; at least one Control was matched and evaluated clean. Safe to proceed.0 — continue to rollout
conditional_goA 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_goAt 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_contextNo 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.”

{
"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.

  • CI Gate — the pull-request-time gate (kind=pr_diff)
  • API Keys — creating and scoping API keys