PR #11 (GitHub OIDC deploy role + workflow) failed the Terraform / validate check.
Cause
PR content and checks were out of sync while the role policy was still being tightened (privilege-escalation review in progress).
Remediation
Fixed the failing check in the same PR series; re-ran CI until green before merge. Do not merge deploy-role changes while validate is red.
Deploy role could escalate past its own deny
Symptom
Copilot / review found the draft invdash-terraform-deploy role could create another invdash-* role with arbitrary permissions and use it to bypass “no self-modification” denials.
Cause
IAM allow on creating project roles without a permissions boundary, and no hard block on attaching unrestricted policies to those roles.
Remediation
Attach invdash-workload-boundary to every invdash-* role CI creates; deny workarounds that strip or skip the boundary; keep an explicit deny on the deploy role modifying itself / the OIDC provider / the boundary. Re-review any PR that touches module.github_deploy.
First Terraform plan: Invalid for_each
Symptom
Early terraform plan failed with an Invalid for_each argument (unknown or sensitive value at plan time).
Cause
Module / resource for_each keyed on a value not known until apply (common with computed maps or empty optional sets on first plan).
Remediation
Infra PR fixed the for_each sources so keys are known at plan time; re-plan against merged main before apply. Prefer static or locally computed maps for for_each keys.
Stacked PR merged into the wrong base
Symptom
ETF-price PR #4 landed on PR #3’s branch instead of main.
Cause
Stacked PR workflow: GitHub targets the open base branch of the stack, not main.
Remediation
Re-landed via PR #5 onto main. Later feature PRs target main directly (no long stacks) unless intentionally stacked and rebased.
Workflow bootstrap check: state list \| grep -q under pipefail
Symptom
Deploy workflow could mis-detect “jobs already exist” and re-enter the empty-jobs_image_uri bootstrap apply.
Cause
Piping terraform state list into grep -q with set -o pipefail: an early grep match SIGPIPEs state list, flips an if ! condition, and treats the check as failed.
Remediation
Fixed in PR #15: load terraform state list into a bash array first, then test membership without a fragile pipe. See docs/operations.md (“Jobs image URI and empty-image applies”).
Image / ECR notes (build side)
Symptom
Jobs need an immutable ECR tag; empty URI disables the whole module.jobs set.
Cause
enabled_jobs = {} when jobs_image_uri is empty by design.
Remediation
CI builds and pushes invdash/jobs:<gitsha>-N, then applies once with that URI. Never pass an empty URI after jobs exist (see Deployment issues).