Skip to main content
The draftt-io/terraform-ci GitHub Action runs in your workflow after terraform plan. It scrubs the plan JSON on the runner and uploads the scrubbed copy to Draftt. The Draftt CI GitHub App publishes the result; the Action itself never writes to GitHub and needs no GitHub token.

Add the Action

1

Create an access key

In Draftt, go to Settings > Access Keys and create a key with the Developer role or above. In GitHub, store it as a repository or organization secret named DRAFTT_API_KEY.
2

Add the step to your workflow

Add the Draftt step after the one that writes your plan as JSON. For example:
Check out the pull request head commit, as in the example, so Draftt can annotate the changed files. Set terraform_wrapper: false, or the plan file is not valid JSON. Write the plan inside the workspace. For OpenTofu, use tofu plan and tofu show -json.
Use draftt-io/terraform-ci@v1. We recommend it: v1 follows the latest 1.x release, which brings fixes and new optional inputs and never a breaking change. A breaking change ships as v2, with migration notes in its release.
3

Open a pull request

Open or update a pull request that changes Terraform files. The Draftt Terraform Policy Scan check fills in with the result when the step uploads the plan. If it stays at Waiting for a Terraform plan, open the workflow run and expand the Draftt step: a scan that cannot complete leaves the step green with a warning that says why. See Troubleshooting.
Terraform 1.0 or later, or OpenTofu 1.6 or later. Runners need Node 24 and git; the standard ubuntu-latest image has both.

Inputs

Outputs

Give the step an id to read the outputs in later steps. When scan-status is skipped or error, every count is 0 and the URL is empty, so read scan-status first. Outputs contain counts and the check URL only, never plan data or credentials.

Several root modules in one repository

Scan each root module with its own Draftt step, or a matrix job, and set terraform-root to the module’s directory. Each root gets its own check, named Draftt Terraform Policy Scan (root), and its own comment.
  • terraform-root is a workspace-relative path, . for the repository root. It can contain letters, digits, ., _, /, and -, up to 200 characters, with no .. segments.
  • A commit takes at most 50 roots.
  • Two scans in the same workflow run attempt must use different roots; a second upload for the same root is answered with the first result and not published. One root planned several times for different environments, with -var-file or workspaces, is therefore not supported yet.

What is sent to Draftt

The raw plan never leaves the runner. The Action sends one HTTPS request with:
  • The planned changes: the resource changes of managed resources, and the aws_caller_identity data source, with every value Terraform marks sensitive removed from both sides of each change. Unchanged resources are included with their current values, scrubbed the same way. A removed value is marked unknown, the same way Terraform marks a value known only after apply, so a policy that depends on it is reported as not evaluated instead of evaluated against a default.
  • A reduced configuration: the configuration of the changed resources, including those inside modules, with sensitive values removed. Provider configuration is cut down to the region and the assumed role ARN, and the variables that set them.
  • Where each resource is defined: file path and line per resource address, so the App can annotate the changed files.
  • Where the scan came from: repository and owner IDs, pull request number, head commit SHA, the terraform-root, and the workflow run ID, attempt, and URL.
Never sent: the raw plan, the state file, planned values, outputs, Terraform source files, or any GitHub token. If a recognized credential format is still present after scrubbing, an AWS access key, a PEM private key, a GitHub or GitLab token, a JWT, or a URL with embedded credentials, the Action uploads nothing and reports a scan error that names the kind of credential and where it is. The Action masks the API key before it reads the plan. The job log and summary never show the key, plan values, or Draftt response bodies.

Events and limits

  • Pull request events only: Draftt CI publishes results on pull_request events. Pull requests from external forks are skipped, because forks have no access to your secrets, and scan-status is skipped.
  • Dependabot pull requests: GitHub does not give workflows triggered by Dependabot access to Actions secrets, so the scan ends in an error on them. Skip the job with if: github.actor != 'dependabot[bot]', or add the key as a Dependabot secret too.
  • pull_request_target: Don’t use it. The planned code would run with access to the Draftt key.
  • Plan size: The raw plan is limited to 50 MiB, the scrubbed plan to 49 MiB, and the whole request to 50 MiB. Requests over 1 MiB are compressed.
  • Request time: The upload is retried on 502, 503, and 504 answers and on network failures, up to 3 attempts within 8 minutes in total.
  • Source map: The Action sends a map from resource addresses to files and lines so the App can annotate the changed files. It drops the map when the request would exceed the size limit, when the pull request head isn’t checked out, when Terraform files differ from the commit, when GITHUB_WORKSPACE isn’t the repository root, or when Git can’t be inspected. The map is capped at 100,000 resources. Without it, findings stay in the check summary and the comment.

Troubleshooting

Open the workflow run on the pull request and expand the Draftt step. On success, the log shows the api-url host, the scan key, the counts, and the check URL. The job summary shows the same counts. A scan that cannot complete does not fail the step by default: the step turns green with a warning, the outputs report scan-status: error, and the check stays at Waiting for a Terraform plan until it times out. To fail the step instead, set fail-on-scan-error: "true". Messages in the step log:
  • Input required and not supplied: api-key: The secret is not set for this repository, or the run has no secrets, as on a Dependabot pull request.
  • Plan JSON path must stay inside the workspace, Plan JSON path is not a file: Write the plan inside the workspace and pass its path relative to the workspace root, for example infra/plan.json.
  • Plan JSON is not valid JSON: Usually the Terraform wrapper added lines to the output. Set terraform_wrapper: false on hashicorp/setup-terraform.
  • Terraform plan has an unsupported format_version: The file is not the output of terraform show -json for a saved plan.
  • terraform-root must be an existing directory inside the repository: The directory doesn’t exist in the checked-out commit, or the path points outside the workspace.
  • Scrubbed plan still contains a recognized <kind> at <path>: A credential is in the plan in a value Terraform doesn’t mark sensitive. Nothing was uploaded. Mark the value sensitive in Terraform, or move it out of the plan.
  • Draftt authentication failed: The access key is invalid or revoked.
  • Draftt answered 403: owner_mismatch: The repository doesn’t belong to the GitHub organization bound to the Draftt organization that owns the access key.
  • Draftt rejected the scan request as too large: The scrubbed plan is over the size limit. Scan a smaller root module, or split the plan.
  • Draftt could not complete the scan: A server error. Re-run the job, and contact support if it persists.
  • Draftt answered <status>: <message>: Any other 4xx or 502 answer; the message says why. github_unavailable means Draftt could not reach GitHub, and a re-run usually clears it.
Problems that appear after the scan completed, when the App publishes the result, are warnings with scan-status: completed and an empty check-run-url; fail-on-scan-error does not fail the step for them. See the App troubleshooting. No annotations on the changed files, while the check and the comment show the findings: the step log carries a warning that names the cause. Check that actions/checkout checks out the pull request head, that the workspace is the repository root, that terraform-root is the directory with the root module’s .tf files, and that no generated .tf files or .terraform/ directory are untracked in the working tree.

Support

Questions and problems: support@draftt.io. Report a security issue to security@draftt.io, not in a public issue. The Action is open source under Apache-2.0.