A reusable GitHub Actions workflow that reviews Terraform pull requests with a LangGraph multi-agent system and posts a single, severity-ranked sticky comment.
Three specialists run in parallel over the PR's changed Terraform files:
| Agent | Scanners | Looks for |
|---|---|---|
| π Security | tfsec + checkov |
misconfigurations, insecure defaults, exposed resources |
| π° Cost | infracost diff |
monthly cost deltas vs. the base branch |
| π¨ Style | tflint + terraform fmt -check |
lint findings and formatting drift |
Scanners own detection and severity; an LLM only rewords each finding into a concise, actionable sentence β so the set of findings is deterministic run to run. Results are merged, de-duplicated, severity-ranked, and upserted as one comment (edited in place on every push) rather than stacking up.
Everything runs inside a prebuilt container (ghcr.io/infiniumtek/terraform-review-agent)
that bundles pinned terraform, tfsec, tflint, infracost, and checkov
binaries, so there are no per-run tool installs.
Add a workflow to your repo that calls the reusable workflow. A complete,
commented sample lives in examples/example-caller.yml;
the minimal version:
# .github/workflows/terraform-review.yml
name: terraform-review
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review]
paths:
- "**/*.tf"
- "**/*.tfvars"
- "**/*.tf.json"
- "**/*.tfvars.json"
jobs:
terraform-review:
uses: infiniumtek/terraform-review-agent/.github/workflows/terraform-review.yml@v1
permissions:
contents: read # checkout
pull-requests: write # post/edit the sticky comment
with:
llm-provider: anthropic
llm-model: claude-sonnet-4-5
fail-on-severity: high # fail the check on any high/critical finding
secrets:
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
infracost-api-key: ${{ secrets.INFRACOST_API_KEY }} # optional; enables cost agentPin @v1 (the major float) or a specific release tag such as @v1.2.3. The
paths filter on the trigger decides whether the job spins up at all; the agent
additionally early-exits if no Terraform files actually changed.
Manual re-runs:
workflow_dispatchevents carry no PR context, so pass apr-numberinput when triggering manually. See the example caller for thegithub.event.pull_request.number || inputs.pr-numberpattern.
| Input | Default | Description |
|---|---|---|
llm-provider |
openai |
openai | anthropic | google. |
llm-model |
gpt-4o |
Model id β must match the provider. The default suits openai; set this when choosing another provider (e.g. claude-sonnet-4-5). |
fail-on-severity |
none |
Gate CI when a finding meets/exceeds this floor: critical | high | medium | low | info | none. The comment is always posted first; none never fails the check. |
pr-number |
"" |
PR to review. Defaults to the triggering pull_request event; required for workflow_dispatch runs. |
| Secret | Required | Description |
|---|---|---|
openai-api-key / anthropic-api-key / google-api-key |
one, matching llm-provider |
LLM credentials. |
infracost-api-key |
optional | Enables the π° cost agent. When unset, cost review is skipped (security + style still run). Get a free key at infracost.io. |
github-token |
optional | Defaults to the caller's ${{ github.token }}. Override only if you need broader scope. |
The calling job needs:
permissions:
contents: read # checkout the PR merge ref
pull-requests: write # create/edit the sticky comment5 findings in 3 files β 1 critical, 2 high, 1 medium, 1 low
By agent: π Security 2 Β· π° Cost 1 Β· π¨ Style 2
π° Infracost estimate: $520.50/mo total Β· +$120.00/mo from this PR
Severity Issue Location π΄ π S3 bucket has no server-side encryption configured.
π‘ Add anaws_s3_bucket_server_side_encryption_configurationblock.
tfsec:aws-s3-enable-bucket-encryptionmodules/s3/main.tf:12
Severity Issue Location π π° Estimated monthly cost change for aws_instance.web: +$120.00
π‘ Consider a smaller instance type or autoscaling.
infracost:resource-delta.π π S3 bucket access logging is not enabled.
π‘ Enable access logging to an audit bucket.
checkov:CKV_AWS_18modules/s3/main.tf:12
Severity Issue Location π‘ π¨ variable "region" is declared but never used.
π‘ Remove the unused variable.
tflint:terraform_unused_declarationsmain.tf:9
Critical / high / medium findings show inline; low and info collapse into a
<details> block so the comment stays scannable. Each location links to the
exact file and line at the PR head. On the next push, this same comment is
edited in place.
GitHub PR event
βββΊ reusable workflow (terraform-review.yml)
βββΊ container: ghcr.io/infiniumtek/terraform-review-agent:v1
βββΊ python -m terraform_review_agent.entrypoint
βββΊ LangGraph:
start ββΊ [security β₯ cost β₯ style] ββΊ aggregator ββΊ post_comment
- start filters the PR to Terraform files and early-exits if none changed.
- security / cost / style run their scanners, then an LLM rewords the findings (it cannot change severity, file, line, or rule).
- aggregator dedupes by
(file, rule, line), severity-ranks, and renders the markdown. - post_comment upserts the sticky comment via a hidden HTML marker.
Scanner versions are pinned in the container image β bumping one is a rebuild-image PR in this repo, not an edit to your workflow file.
Requires Python 3.13 + uv. Scanners only run inside the container; the host test suite mocks them.
make install # create .venv and sync pinned deps
make fmt lint type test # format, lint, mypy --strict, pytestSee CLAUDE.md for the full project contract and layout.
make run executes the CLI inside the container, which bundles every
scanner (terraform / tfsec / tflint / infracost / checkov) β your host
.venv does not. For an external repo the entrypoint clones the PR's merge ref
into a scratch dir, scans it, and upserts the sticky comment on that PR, exactly
as the reusable workflow does in CI.
-
Configure
.env(copy.env.example). For an end-to-end run you need:GITHUB_TOKEN=ghp_... # read access to the repo + write access to its PRs DEFAULT_LLM_PROVIDER=anthropic # openai | anthropic | google DEFAULT_LLM_MODEL=claude-sonnet-4-5 ANTHROPIC_API_KEY=sk-ant-... # the key matching DEFAULT_LLM_PROVIDER INFRACOST_API_KEY=ico-... # optional β enables the cost agent
-
Build the image (bundles the pinned scanners). Re-run only after a dependency or scanner-version bump;
./srcis bind-mounted, so code edits need no rebuild:make docker-build
-
Review a PR. Point
--repository/--pr-numberat any repo your token can reach. Using the sample Cloud Run service repospanosg131/gcp-test-cloudrun-serviceβ open (or reuse) a PR there that touches a.tffile, then:make run ARGS="--repository spanosg131/gcp-test-cloudrun-service --pr-number 2"The agent fetches the PR, runs the three specialists, and posts/edits the sticky comment on PR #2. (You can also set
GITHUB_REPOSITORY/GITHUB_PR_NUMBERin.envand runmake runwith noARGS.)
Notes
- The token needs
pull-requests: writeto post the comment and read access to clone the PR; withoutINFRACOST_API_KEYthe π° cost agent is skipped.- To inspect output without posting, point at a PR in a throwaway repo, or review the structured logs the run prints to stderr.
MIT