Any CI Pipeline
DriftWise is a single HTTPS endpoint. Any runner that can execute terraform show -json and POST JSON works — Jenkins, CircleCI, Buildkite, Azure Pipelines, Drone, Harness, Tekton, Argo Workflows, Bitbucket Pipelines, TeamCity, self-hosted shell scripts. No plugin, no sidecar, no agent.
The Contract
| Endpoint | POST https://api.driftwise.ai/api/v2/orgs/{ORG_ID}/analyze |
| Auth | x-api-key: dw2_... header |
| Body | {"plan_json": "<terraform show -json output, as a string>", "ci": {...}} |
| Response | 202 {"job_id", "scan_run_id", "status": "pending"} |
| Poll | GET /api/v2/orgs/{ORG_ID}/llm-jobs/{job_id} → {"status", "result"}; on status: "done", result carries risk_level, narrative, changes, summary, scan_run, plan_noise |
| Timing | Enqueue returns immediately; analysis typically completes in 15–90s; poll every 5s |
plan_json is a string, not a nested objectThe API expects plan_json to be the raw Terraform JSON plan, encoded as a JSON string. Shell callers use jq -Rs . to turn a file into an escaped string. Language SDKs that serialize via JSON.stringify / json.dumps do this automatically — pass a string.
Step 1 — Produce a Plan JSON
Run this in any pipeline before the analysis step:
terraform plan -out=tfplan
terraform show -json tfplan > plan.json
Step 2 — Call the API
curl (any shell runner)
The analyze endpoint enqueues the job and returns immediately — poll
GET /llm-jobs/{job_id} until status is done (or error):
JOB_ID=$(curl -sf -X POST "https://api.driftwise.ai/api/v2/orgs/${DRIFTWISE_ORG_ID}/analyze" \
-H "x-api-key: ${DRIFTWISE_API_KEY}" -H "Content-Type: application/json" \
-d "{\"plan_json\": $(jq -Rs . < plan.json)}" | jq -r '.job_id')
for i in $(seq 1 36); do
BODY=$(curl -sf "https://api.driftwise.ai/api/v2/orgs/${DRIFTWISE_ORG_ID}/llm-jobs/$JOB_ID" \
-H "x-api-key: ${DRIFTWISE_API_KEY}")
STATUS=$(echo "$BODY" | jq -r '.status')
if [ "$STATUS" = "done" ]; then RESULT=$(echo "$BODY" | jq '.result'); break; fi
if [ "$STATUS" = "error" ]; then echo "analysis failed: $(echo "$BODY" | jq -r '.error')" >&2; exit 1; fi
sleep 5
done
[ "$STATUS" = "done" ] || { echo "timed out waiting for analysis" >&2; exit 1; }
RISK=$(echo "$RESULT" | jq -r '.risk_level')
echo "risk_level=$RISK"
# Gate the pipeline on risk: fail the step on high/critical.
if [ "$RISK" = "high" ] || [ "$RISK" = "critical" ]; then
echo "blocking merge: risk_level=$RISK" >&2
exit 1
fi
-f/-sf makes curl exit non-zero on HTTP errors so the pipeline step fails. The loop above polls every 5s for up to 3 minutes (36 × 5s) — adjust the iteration count for slower models or larger plans. To pass ci metadata, add a ci object to the enqueue call's -d payload (see CI Metadata below).
CI Metadata
Every ci field is optional. Populate whatever your runner exposes — DriftWise uses it to link the analysis back to the source (PR comments, drill-down in the UI).
| Field | Description |
|---|---|
repo_owner | Org/user (e.g. acme-corp) |
repo_name | Repository name |
repo_url | Full URL to the repo |
pr_number | Pull/merge request number |
branch | Source branch |
commit_sha | Full commit SHA |
Runner-Specific Env Var Reference
Map your runner's built-ins to the metadata fields:
| Runner | repo_owner | repo_name | branch | commit_sha | pr_number |
|---|---|---|---|---|---|
| Jenkins | $CHANGE_AUTHOR / parse $GIT_URL | parse $GIT_URL | $BRANCH_NAME | $GIT_COMMIT | $CHANGE_ID |
| CircleCI | $CIRCLE_PROJECT_USERNAME | $CIRCLE_PROJECT_REPONAME | $CIRCLE_BRANCH | $CIRCLE_SHA1 | parse $CIRCLE_PULL_REQUEST |
| Buildkite | $BUILDKITE_ORGANIZATION_SLUG | $BUILDKITE_PIPELINE_SLUG | $BUILDKITE_BRANCH | $BUILDKITE_COMMIT | $BUILDKITE_PULL_REQUEST |
| Azure Pipelines | $(Build.Repository.Name) owner | $(Build.Repository.Name) name | $(Build.SourceBranchName) | $(Build.SourceVersion) | $(System.PullRequest.PullRequestNumber) |
| Bitbucket Pipelines | $BITBUCKET_WORKSPACE | $BITBUCKET_REPO_SLUG | $BITBUCKET_BRANCH | $BITBUCKET_COMMIT | $BITBUCKET_PR_ID |
| Drone | $DRONE_REPO_OWNER | $DRONE_REPO_NAME | $DRONE_SOURCE_BRANCH | $DRONE_COMMIT_SHA | $DRONE_PULL_REQUEST |
| Tekton / Argo | from event payload | from event payload | from event payload | from event payload | from event payload |
Secrets Handling
Store DRIFTWISE_API_KEY in your runner's secret store (Jenkins Credentials, CircleCI Contexts, Azure Key Vault, etc.). DRIFTWISE_ORG_ID is not sensitive — treat it like a project ID.
Failing the Pipeline on Risk
The examples above exit non-zero on high or critical. Adjust to your policy:
- Strict: block on
mediumand above. - Advisory only: never fail, just log the narrative for reviewers.
- Policy-driven: combine with DriftWise custom policy rules to rewrite risk on your own signals, then gate on the rewritten level.
What's Returned
The analyze call itself only returns job_id, scan_run_id, and
status: "pending". Poll GET /llm-jobs/{job_id} until status is
done, then read result — the fields you'll typically consume in a
pipeline:
result.risk_level— one ofnone,low,medium,high,critical(a no-changes plan returnsnone).result.narrative— plain-English summary, safe to print in CI logs or post as a PR comment.result.scan_run.id— UUID of the recorded analysis; use to deep-link back to the DriftWise UI.result.plan_noise— counts of known-benign patterns vs. novel changes; useful for filtering noisy reviews.
If status is error, the job's error field carries the failure message instead.
Limits
- Request body: 5 MB. Large monorepo plans may need to be split per workspace.
- Job retention: completed and errored jobs are retained 7 days — poll before that window closes.
- Typical completion: seconds to 90s; poll every 5s. LLM generation dominates — slower models take longer.
- Rate limits: the platform-LLM quota depends on plan — Free: 5 analyses/week; Team: 20/hour (BYOK exempt); Enterprise: unlimited by default. Separately, the enqueue endpoint itself is capped at 30 requests/hour per org on every plan, BYOK included.