Skip to main content

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

EndpointPOST https://api.driftwise.ai/api/v2/orgs/{ORG_ID}/analyze
Authx-api-key: dw2_... header
Body{"plan_json": "<terraform show -json output, as a string>", "ci": {...}}
Response202 {"job_id", "scan_run_id", "status": "pending"}
PollGET /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
TimingEnqueue returns immediately; analysis typically completes in 15–90s; poll every 5s
plan_json is a string, not a nested object

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

FieldDescription
repo_ownerOrg/user (e.g. acme-corp)
repo_nameRepository name
repo_urlFull URL to the repo
pr_numberPull/merge request number
branchSource branch
commit_shaFull commit SHA

Runner-Specific Env Var Reference

Map your runner's built-ins to the metadata fields:

Runnerrepo_ownerrepo_namebranchcommit_shapr_number
Jenkins$CHANGE_AUTHOR / parse $GIT_URLparse $GIT_URL$BRANCH_NAME$GIT_COMMIT$CHANGE_ID
CircleCI$CIRCLE_PROJECT_USERNAME$CIRCLE_PROJECT_REPONAME$CIRCLE_BRANCH$CIRCLE_SHA1parse $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 / Argofrom event payloadfrom event payloadfrom event payloadfrom event payloadfrom 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 medium and 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 of none, low, medium, high, critical (a no-changes plan returns none).
  • 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.