Defined in .github/workflows/platform-ci.yml. Runs on pushes to terraform/platform/**, terraform/lambda-src/**, and scripts/**.
push to main ──► plan (includes LLM review) ──► apply
PR ──► plan (includes LLM review) ──► PR comments (no apply)
Monday 06:00 ──► drift detection ──► Slack if drift found
Monday 08:00 ──► securityhub-summary Lambda ──► #javabin-infra-alerts
- OIDC →
javabin-ci-infra-planrole (read-only) terraform init,validate,fmt -check,plan- Uploads
tfplan+plan-output.txtto S3 with SHA256 hash - Posts plan output as PR comment on pull requests
- Runs
scripts/review-plan.pyinline — calls Bedrock (Claude Haiku 5.5) for risk analysis - The model gets the whole plan with refresh/read lines stripped; there is no practical input cap (Haiku 5.5 has a 1M-token context): the default ceiling is 2M chars, 2.4x the largest real plan. The prompt says the plan is complete, gives its change count and
Plan:line, and explains Terraform's own elision markers (# (N unchanged attributes hidden),(sensitive value),(known after apply), partialjsonencodediffs) so the model does not report them as truncation.REVIEW_MAX_PLAN_CHARSoverrides the ceiling. A plan above it gets a list of every resource change plus as much diff as fits, cut at a resource boundary, and the prompt says exactly which diffs are missing. A response withstopReason: max_tokensis discarded (FAILED / retried) rather than trusted. - The PR comment shows at most 60k chars of the plan (GitHub's comment limit) and says so when it cuts; this does not affect the review.
- Structured output via Bedrock Converse tool use:
{risk: LOW|MEDIUM|HIGH, summary: ..., findings: [...]} - Posts review as PR comment
- HIGH risk (or a FAILED review) on main → Slack alert to #platform-override-alerts with plan key, run ID and override button.
slack_post.pychecks Slack's reply; rejected blocks fall back to plain text, and an undelivered alert shows as a CI error annotation - Outputs:
has_changes,plan_key,plan_sha256,risk_level
- Only on
main(push,workflow_dispatch, or the registry'steams-changedrepository_dispatch) with changes - Checks risk level: blocks on HIGH/FAILED/UNKNOWN
- Downloads plan from S3, verifies SHA256 integrity
- Restores
.terraform.lock.hclfrom the plan file (Terraform stores it there; it is gitignored in the repo), soterraform initinstalls the provider versions the plan was made with terraform apply -auto-approve tfplan- Then queues (async, non-blocking) an app-auth-sync reconcile (
scripts/reconcile-app-auth.sh) and a team-provisionersync_team_accessrun (scripts/sync-team-access.sh), which grants a newly registered team its permission set and account assignment once this apply has created itsjavabin-team-{team}-guardrailspolicy - Uses
environment: production(GitHub environment protection) - OIDC →
javabin-ci-infra. The role's trust also pinsjob_workflow_reftoplatform-ci.yml@refs/heads/main, because the environment-basedsubclaim carries no branch. A workflow on another branch that declaresenvironment: productioncannot assume it.
- Weekly schedule (Monday 06:00 UTC)
- OIDC →
javabin-ci-infra-plan(read-only; drift never applies) - Runs
terraform plan -detailed-exitcode - If drift detected, posts to Slack with plan output
- LOW/MEDIUM → auto-apply
- HIGH → blocked, Slack notification sent, board member must run approve-override workflow
- FAILED → blocked (the review could not run; a service failure, not a risk finding). See Requesting an override
Plans are stored in S3 (javabin-ci-plan-artifacts-553637109631) with 24h lifecycle expiry.
The SHA256 hash is computed at plan time and verified before apply, preventing plan tampering.