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.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.
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 setterraform-root to the module’s directory. Each root gets its own check, named Draftt Terraform Policy Scan (root), and its own comment.
terraform-rootis 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-fileor 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_identitydata 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.
Events and limits
- Pull request events only: Draftt CI publishes results on
pull_requestevents. Pull requests from external forks are skipped, because forks have no access to your secrets, andscan-statusisskipped. - 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, and504answers 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_WORKSPACEisn’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 theapi-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 exampleinfra/plan.json.Plan JSON is not valid JSON: Usually the Terraform wrapper added lines to the output. Setterraform_wrapper: falseonhashicorp/setup-terraform.Terraform plan has an unsupported format_version: The file is not the output ofterraform show -jsonfor 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 other4xxor502answer; the message says why.github_unavailablemeans Draftt could not reach GitHub, and a re-run usually clears it.
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.