Skip to content

fix(infra): enforce prod deployment approval gates#296

Open
tataihono wants to merge 1 commit intomainfrom
fix/67-prod-approval-gate
Open

fix(infra): enforce prod deployment approval gates#296
tataihono wants to merge 1 commit intomainfrom
fix/67-prod-approval-gate

Conversation

@tataihono
Copy link
Contributor

@tataihono tataihono commented Mar 9, 2026

Summary

  • enforce Terraform-managed approval gates on aws-prod and cms-prod GitHub environments
  • require explicit reviewer approval, prevent self-review/admin bypass, and limit deployments to protected branches
  • codify this in infra/github/environments.tf so prod gating is no longer manual drift

Contracts Changed

  • yes
  • no

Regeneration Required

  • yes
  • no

Validation

  • Contracts validated
  • Generated code verified (no manual edits)
  • Tests and build passed
  • Terraform plan reviewed (if infra change)

Resolves #67

Made with Cursor

Summary by CodeRabbit

  • Chores
    • Enhanced production deployment safeguards by implementing mandatory reviewer approval requirements, protecting deployment branches, and preventing self-review and admin override on AWS and CMS production environments.

Manage prod deployment protections in Terraform for aws and cms
environments so approval gating is codified and no longer
manual in GitHub UI.

Made-with: Cursor
@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

Walkthrough

This change adds deployment protection controls to GitHub repository production environments by introducing a data source to fetch a designated reviewer and configuring deployment branch policies, approval requirements, and self-review prevention for the aws_prod and cms_prod environments.

Changes

Cohort / File(s) Summary
GitHub Environment Configuration
infra/github/environments.tf
Added data.github_user data source to fetch prod environment reviewer. Updated aws_prod and cms_prod environments with deployment protection: can_admins_bypass = false, prevent_self_review = true, deployment_branch_policy (protected branches only), and reviewers block referencing the fetched user.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~8 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically describes the main change: enforcing production deployment approval gates via Terraform, which aligns perfectly with the changeset's additions of approval gate configurations.
Linked Issues check ✅ Passed The pull request addresses the pending acceptance criterion 'Terraform plan/apply runs via GitHub Actions; prod apply is approval-gated' [#67] by implementing Terraform-managed deployment approval gates on production environments.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing production approval gates in environments.tf; no unrelated modifications to other infrastructure or systems are present.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/67-prod-approval-gate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Mar 9, 2026

🟨 Terraform plan (github/prod) — Changes detected

Changes: +0 ~2 -0
Time:

TZ Time
UTC 2026-03-09, 12:44:02 a.m.
NZ 2026-03-09, 1:44:02 p.m.
PT 2026-03-08, 5:44:02 p.m.
ET 2026-03-08, 8:44:02 p.m.

Run: https://github.com/JesusFilm/forge/actions/runs/22833734972

data.aws_ssm_parameter.terraform_vercel_role_apply_arn: Reading...
data.aws_ssm_parameter.terraform_vercel_role_plan_arn: Reading...
data.aws_ssm_parameter.github_app_pem: Reading...
data.aws_ssm_parameter.github_installation_id: Reading...
data.aws_ssm_parameter.github_app_id: Reading...
data.aws_ssm_parameter.terraform_aws_role_apply_arn_stage: Reading...
data.aws_ssm_parameter.terraform_aws_role_apply_arn_prod: Reading...
data.aws_ssm_parameter.cms_deploy_role_arn_stage: Reading...
data.aws_ssm_parameter.terraform_aws_role_plan_arn_stage: Reading...
data.aws_ssm_parameter.cms_deploy_role_arn_prod: Reading...
data.aws_ssm_parameter.terraform_aws_role_apply_arn_prod: Read complete after 0s [id=/forge/github/terraform_aws_role_apply_prod_arn]
data.aws_ssm_parameter.cms_deploy_role_arn_prod: Read complete after 0s [id=/forge/github/cms_deploy_role_arn_prod]
data.aws_ssm_parameter.terraform_vercel_role_apply_arn: Read complete after 0s [id=/forge/github/terraform_vercel_role_apply_arn]
data.aws_ssm_parameter.cms_deploy_role_arn_stage: Read complete after 0s [id=/forge/github/cms_deploy_role_arn_stage]
data.aws_ssm_parameter.terraform_vercel_role_plan_arn: Read complete after 0s [id=/forge/github/terraform_vercel_role_plan_arn]
data.aws_ssm_parameter.terraform_github_role_plan_arn: Reading...
data.aws_ssm_parameter.terraform_github_role_apply_arn: Reading...
data.aws_ssm_parameter.terraform_aws_role_plan_arn_prod: Reading...
data.aws_ssm_parameter.terraform_aws_role_apply_arn_stage: Read complete after 0s [id=/forge/github/terraform_aws_role_apply_stage_arn]
data.aws_ssm_parameter.terraform_aws_role_plan_arn_stage: Read complete after 0s [id=/forge/github/terraform_aws_role_plan_stage_arn]
data.aws_ssm_parameter.github_installation_id: Read complete after 0s [id=/forge/github/installation_id]
data.aws_ssm_parameter.github_app_pem: Read complete after 0s [id=/forge/github/app_private_key]
data.aws_ssm_parameter.github_app_id: Read complete after 0s [id=/forge/github/app_id]
data.aws_ssm_parameter.terraform_github_role_plan_arn: Read complete after 0s [id=/forge/github/terraform_github_role_plan_arn]
data.aws_ssm_parameter.terraform_aws_role_plan_arn_prod: Read complete after 0s [id=/forge/github/terraform_aws_role_plan_prod_arn]
data.aws_ssm_parameter.terraform_github_role_apply_arn: Read complete after 0s [id=/forge/github/terraform_github_role_apply_arn]
data.github_user.prod_environment_reviewer: Reading...
github_repository.forge: Refreshing state... [id=forge]
github_branch_default.forge: Refreshing state... [id=forge]
github_repository_environment.aws_plan_stage: Refreshing state... [id=forge:aws-plan-stage]
github_repository_environment.aws_plan_prod: Refreshing state... [id=forge:aws-plan-prod]
github_repository_environment.vercel_prod: Refreshing state... [id=forge:vercel-prod]
github_actions_variable.aws_region: Refreshing state... [id=forge:AWS_REGION]
github_repository_environment.aws_stage: Refreshing state... [id=forge:aws-stage]
github_repository_environment.vercel_plan: Refreshing state... [id=forge:vercel-plan]
github_repository_environment.github_plan: Refreshing state... [id=forge:github-plan]
github_repository_environment.github_prod: Refreshing state... [id=forge:github-prod]
data.github_user.prod_environment_reviewer: Read complete after 1s [id=802117]
github_repository_environment.cms_stage: Refreshing state... [id=forge:cms-stage]
github_repository_environment.cms_prod: Refreshing state... [id=forge:cms-prod]
github_repository_environment.aws_prod: Refreshing state... [id=forge:aws-prod]
github_actions_environment_secret.aws_plan_role_prod: Refreshing state... [id=forge:aws-plan-prod:TERRAFORM_ROLE_ARN]
github_actions_environment_secret.aws_plan_role_stage: Refreshing state... [id=forge:aws-plan-stage:TERRAFORM_ROLE_ARN]
github_actions_environment_secret.vercel_terraform_role_plan: Refreshing state... [id=forge:vercel-plan:TERRAFORM_ROLE_ARN]
github_actions_environment_secret.vercel_terraform_role_apply: Refreshing state... [id=forge:vercel-prod:TERRAFORM_ROLE_ARN]
github_actions_environment_secret.terraform_apply_role_stage: Refreshing state... [id=forge:aws-stage:TERRAFORM_APPLY_ROLE_ARN]
github_actions_environment_secret.github_terraform_role_plan: Refreshing state... [id=forge:github-plan:TERRAFORM_ROLE_ARN]
github_actions_environment_secret.github_terraform_role_apply: Refreshing state... [id=forge:github-prod:TERRAFORM_ROLE_ARN]
github_actions_environment_secret.cms_deploy_role_stage: Refreshing state... [id=forge:cms-stage:CMS_DEPLOY_ROLE_ARN]
github_actions_environment_secret.cms_deploy_role_prod: Refreshing state... [id=forge:cms-prod:CMS_DEPLOY_ROLE_ARN]
github_actions_environment_secret.terraform_apply_role_prod: Refreshing state... [id=forge:aws-prod:TERRAFORM_APPLY_ROLE_ARN]

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # github_repository_environment.aws_prod will be updated in-place
  ~ resource "github_repository_environment" "aws_prod" {
      ~ can_admins_bypass   = true -> false
        id                  = "forge:aws-prod"
      + prevent_self_review = true
        # (3 unchanged attributes hidden)

      + deployment_branch_policy {
          + custom_branch_policies = false
          + protected_branches     = true
        }

      + reviewers {
          + users = [
              + 802117,
            ]
        }
    }

  # github_repository_environment.cms_prod will be updated in-place
  ~ resource "github_repository_environment" "cms_prod" {
      ~ can_admins_bypass   = true -> false
        id                  = "forge:cms-prod"
      ~ prevent_self_review = false -> true
        # (3 unchanged attributes hidden)

      + deployment_branch_policy {
          + custom_branch_policies = false
          + protected_branches     = true
        }

      + reviewers {
          + users = [
              + 802117,
            ]
        }
    }

Plan: 0 to add, 2 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
infra/github/environments.tf (1)

44-58: Consistent configuration with aws_prod — consider extracting common settings.

The protection settings mirror aws_prod exactly, which is good for consistency. As the number of prod environments grows, you might extract these common settings into a local or module to reduce duplication.

♻️ Optional: Extract common prod environment settings
locals {
  prod_env_protection = {
    can_admins_bypass   = false
    prevent_self_review = true
    reviewer_ids        = [for u in data.github_user.prod_environment_reviewer : u.id]
  }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infra/github/environments.tf` around lines 44 - 58, The cms_prod Github
environment resource duplicates the same protection settings used elsewhere
(e.g. aws_prod); extract the shared settings into a local (or small module) and
reference it from github_repository_environment.cms_prod to remove duplication:
create a local like prod_env_protection containing can_admins_bypass,
prevent_self_review and reviewer_ids (map reviewer data user IDs), optionally
also factor out deployment_branch_policy settings, then update the cms_prod
resource to read those values (use locals.prod_env_protection.can_admins_bypass,
.prevent_self_review, and .reviewer_ids for the reviewers block and reference
the shared deployment_branch_policy configuration) so both environments reuse
the same configuration.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@infra/github/environments.tf`:
- Around line 24-26: The reviewers block currently assigns a single user
(data.github_user.prod_environment_reviewer.id) while prevent_self_review =
true, which can deadlock deployments if that user (tataihono) triggers the
change; update the environment configuration to add at least one additional
reviewer or replace the single user with a team (e.g.,
data.github_team.prod_environment_reviewers.id) so there are multiple approvers,
and reconcile this with the approval-bypass in
.github/workflows/terraform-plan.yml to avoid conflicting permissions; locate
and update the reviewers { users = [...] } block in infra/github/environments.tf
and/or add a reviewers { teams = [...] } entry to ensure a second approver
exists.
- Around line 4-6: Replace the hardcoded github_user lookup with a configurable
approach: change the data resource data.github_user.prod_environment_reviewer to
accept a variable (e.g., var.prod_reviewer_usernames) or replace it with
multiple lookups (or a github_team data resource) so reviewers are not a single
hardcoded user; update the environment reviewers block that references
prod_environment_reviewer to iterate over the variable list (or include team via
the teams attribute) and keep prevent_self_review = true; ensure you add a
corresponding variable (e.g., prod_reviewer_usernames) with a sensible default
or validation so Terraform won’t break if a username changes or a reviewer is
unavailable.
- Around line 19-22: The deployment_branch_policy sets protected_branches = true
but there is no Terraform-managed branch protection; add a
github_branch_protection resource to manage the protected branch(es) referenced
by deployment_branch_policy: create a resource "github_branch_protection" with
the repository (repo) name and the branch name(s) you want protected, configure
required_status_checks (contexts and strict), required_pull_request_reviews
(dismissal_restrictions, required_approving_review_count), enforce_admins, and
restrictions as appropriate so Terraform fully owns branch protection and
satisfies protected_branches = true. Ensure the resource name and attributes
align with the existing environment configuration and any branch names
referenced by deployment_branch_policy.

---

Nitpick comments:
In `@infra/github/environments.tf`:
- Around line 44-58: The cms_prod Github environment resource duplicates the
same protection settings used elsewhere (e.g. aws_prod); extract the shared
settings into a local (or small module) and reference it from
github_repository_environment.cms_prod to remove duplication: create a local
like prod_env_protection containing can_admins_bypass, prevent_self_review and
reviewer_ids (map reviewer data user IDs), optionally also factor out
deployment_branch_policy settings, then update the cms_prod resource to read
those values (use locals.prod_env_protection.can_admins_bypass,
.prevent_self_review, and .reviewer_ids for the reviewers block and reference
the shared deployment_branch_policy configuration) so both environments reuse
the same configuration.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 03657d58-d0d3-442e-9991-36469c6b213b

📥 Commits

Reviewing files that changed from the base of the PR and between 604f01d and 663252f.

📒 Files selected for processing (1)
  • infra/github/environments.tf

Comment on lines +4 to +6
data "github_user" "prod_environment_reviewer" {
username = "tataihono"
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Hardcoded username and single-reviewer bottleneck.

Using a single hardcoded username creates two risks:

  1. Single point of failure: If "tataihono" is unavailable, no prod deployments can proceed. Combined with prevent_self_review = true, if they trigger the deployment themselves, there's no one to approve it.

  2. Fragility: If the username changes or the user leaves, Terraform apply will fail.

Consider using a variable for the username and/or adding multiple reviewers (or a team via the teams attribute).

🛠️ Suggested improvement
+variable "prod_environment_reviewers" {
+  description = "GitHub usernames authorized to approve prod deployments"
+  type        = list(string)
+  default     = ["tataihono"]
+}
+
 data "github_user" "prod_environment_reviewer" {
-  username = "tataihono"
+  for_each = toset(var.prod_environment_reviewers)
+  username = each.value
 }

Then update the reviewers blocks:

   reviewers {
-    users = [data.github_user.prod_environment_reviewer.id]
+    users = [for u in data.github_user.prod_environment_reviewer : u.id]
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infra/github/environments.tf` around lines 4 - 6, Replace the hardcoded
github_user lookup with a configurable approach: change the data resource
data.github_user.prod_environment_reviewer to accept a variable (e.g.,
var.prod_reviewer_usernames) or replace it with multiple lookups (or a
github_team data resource) so reviewers are not a single hardcoded user; update
the environment reviewers block that references prod_environment_reviewer to
iterate over the variable list (or include team via the teams attribute) and
keep prevent_self_review = true; ensure you add a corresponding variable (e.g.,
prod_reviewer_usernames) with a sensible default or validation so Terraform
won’t break if a username changes or a reviewer is unavailable.

Comment on lines +19 to +22
deployment_branch_policy {
protected_branches = true
custom_branch_policies = false
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if branch protection resources exist elsewhere in the infra directory
echo "=== Searching for github_branch_protection resources ==="
rg -n 'github_branch_protection' infra/ || echo "No branch protection resources found in Terraform"

echo ""
echo "=== Checking repository.tf for any protection-related resources ==="
cat infra/github/repository.tf

Repository: JesusFilm/forge

Length of output: 1236


protected_branches = true requires branch protection rules defined in Terraform.

The deployment_branch_policy with protected_branches = true is set in the environment configuration, but no github_branch_protection resource exists in the Terraform code. This violates the Terraform-only principle: either branch protection was configured manually (against guidelines), or it doesn't exist and the policy won't function as intended.

Add a github_branch_protection resource to infra/github/ to manage branch protection rules via Terraform.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infra/github/environments.tf` around lines 19 - 22, The
deployment_branch_policy sets protected_branches = true but there is no
Terraform-managed branch protection; add a github_branch_protection resource to
manage the protected branch(es) referenced by deployment_branch_policy: create a
resource "github_branch_protection" with the repository (repo) name and the
branch name(s) you want protected, configure required_status_checks (contexts
and strict), required_pull_request_reviews (dismissal_restrictions,
required_approving_review_count), enforce_admins, and restrictions as
appropriate so Terraform fully owns branch protection and satisfies
protected_branches = true. Ensure the resource name and attributes align with
the existing environment configuration and any branch names referenced by
deployment_branch_policy.

Comment on lines +24 to +26
reviewers {
users = [data.github_user.prod_environment_reviewer.id]
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Potential deadlock: self-review prevention with single reviewer.

With prevent_self_review = true and only one reviewer configured, if "tataihono" triggers a deployment (e.g., by pushing or merging to main), they cannot approve their own deployment request. Since there are no other reviewers, the deployment will be permanently blocked.

Additionally, the workflow in .github/workflows/terraform-plan.yml (line 29) grants "tataihono" approval bypass for PR checks, which seems inconsistent with making them the sole gatekeeper for prod deployments.

Ensure at least one additional reviewer is configured, or consider adding a team with multiple members.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infra/github/environments.tf` around lines 24 - 26, The reviewers block
currently assigns a single user (data.github_user.prod_environment_reviewer.id)
while prevent_self_review = true, which can deadlock deployments if that user
(tataihono) triggers the change; update the environment configuration to add at
least one additional reviewer or replace the single user with a team (e.g.,
data.github_team.prod_environment_reviewers.id) so there are multiple approvers,
and reconcile this with the approval-bypass in
.github/workflows/terraform-plan.yml to avoid conflicting permissions; locate
and update the reviewers { users = [...] } block in infra/github/environments.tf
and/or add a reviewers { teams = [...] } entry to ensure a second approver
exists.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(cms-deploy): deploy cms to aws with stage/prod

1 participant