From b26a71f302822ec9cc54c3ea46aa2644d91a64c4 Mon Sep 17 00:00:00 2001 From: Oleksandr Kuzminskyi Date: Mon, 4 May 2026 11:51:57 -0700 Subject: [PATCH 1/3] Rebuild service-repo module as pure GitHub module Replace the old service-repo module (broken local gha-admin fork) with a new implementation per the service-repo-module spec. The module now takes an arbitrary environments map, has zero AWS dependencies, and auto-generates per-environment backend config, GitHub environments with deploy gating, and function-based CODEOWNERS. Also pins CI/CD workflows to ubuntu-24.04. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/terraform-CD.yml | 2 +- .github/workflows/terraform-CI.yml | 2 +- .gitignore | 8 +- .terraform.lock.hcl | 56 +++-- .yamllint | 2 +- modules/service-repo/README.md | 11 + modules/service-repo/codeowners.tf | 35 +++ modules/service-repo/custom_properties.tf | 7 + modules/service-repo/environments.tf | 18 ++ modules/service-repo/files.tf | 49 ++++ .../service-repo/github_actions_variables.tf | 41 ++++ modules/service-repo/locals.tf | 9 + modules/service-repo/main.tf | 60 +++++ modules/service-repo/outputs.tf | 9 + modules/service-repo/rulesets.tf | 37 +++ .../templates/releases.auto.tfvars.tftpl | 3 + .../templates/secrets-scanner.yml | 24 ++ .../terraform-drift-wrapper.yml.tftpl | 33 +++ .../templates/terraform-drift.yml | 120 +++++++++ .../service-repo/templates/terraform.tf.tftpl | 26 ++ .../templates/terraform.tfvars.tftpl | 10 + .../templates/vuln-scanner-pr.yml | 53 ++++ modules/service-repo/terraform.tf | 11 + modules/service-repo/variables.tf | 228 ++++++++++++++++++ modules/service-repo/workflows.tf | 58 +++++ override.tf | 0 provider.github.tf | 8 +- terraform.tf | 2 +- test-service-repo.tf | 63 +++++ 29 files changed, 955 insertions(+), 30 deletions(-) create mode 100644 modules/service-repo/README.md create mode 100644 modules/service-repo/codeowners.tf create mode 100644 modules/service-repo/custom_properties.tf create mode 100644 modules/service-repo/environments.tf create mode 100644 modules/service-repo/files.tf create mode 100644 modules/service-repo/github_actions_variables.tf create mode 100644 modules/service-repo/locals.tf create mode 100644 modules/service-repo/main.tf create mode 100644 modules/service-repo/outputs.tf create mode 100644 modules/service-repo/rulesets.tf create mode 100644 modules/service-repo/templates/releases.auto.tfvars.tftpl create mode 100644 modules/service-repo/templates/secrets-scanner.yml create mode 100644 modules/service-repo/templates/terraform-drift-wrapper.yml.tftpl create mode 100644 modules/service-repo/templates/terraform-drift.yml create mode 100644 modules/service-repo/templates/terraform.tf.tftpl create mode 100644 modules/service-repo/templates/terraform.tfvars.tftpl create mode 100644 modules/service-repo/templates/vuln-scanner-pr.yml create mode 100644 modules/service-repo/terraform.tf create mode 100644 modules/service-repo/variables.tf create mode 100644 modules/service-repo/workflows.tf create mode 100644 override.tf create mode 100644 test-service-repo.tf diff --git a/.github/workflows/terraform-CD.yml b/.github/workflows/terraform-CD.yml index 6a1a760..1f6a995 100644 --- a/.github/workflows/terraform-CD.yml +++ b/.github/workflows/terraform-CD.yml @@ -14,7 +14,7 @@ jobs: apply: name: 'Terraform Apply' if: github.event.pull_request.merged - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 environment: production timeout-minutes: 60 env: diff --git a/.github/workflows/terraform-CI.yml b/.github/workflows/terraform-CI.yml index 0cfdcca..94a6cc4 100644 --- a/.github/workflows/terraform-CI.yml +++ b/.github/workflows/terraform-CI.yml @@ -12,7 +12,7 @@ permissions: jobs: terraform: name: 'Terraform Plan' - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 environment: production timeout-minutes: 60 env: diff --git a/.gitignore b/.gitignore index b3418ee..986856e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,11 +8,8 @@ # Crash log files crash.log -# Ignore any .tfvars files that are generated automatically for each Terraform run. Most -# .tfvars files are managed as part of configuration and so should be included in -# version control. -# -# example.tfvars +# Local environment +/.env/ # Ignore override files as they are usually used to override resources locally and so # are not checked in @@ -36,4 +33,5 @@ tf.plan # Claude Code local settings .claude/*.local.json .claude/reviews/ +.claude/plans/*.local.md nul diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl index 7393fcd..61e8476 100644 --- a/.terraform.lock.hcl +++ b/.terraform.lock.hcl @@ -28,7 +28,6 @@ provider "registry.terraform.io/hashicorp/external" { version = "2.3.5" hashes = [ "h1:FnUk98MI5nOh3VJ16cHf8mchQLewLfN1qZG/MqNgPrI=", - "h1:smKSos4zs57pJjQrNuvGBpSWth2el9SgePPbPHo0aps=", "zh:6e89509d056091266532fa64de8c06950010498adf9070bf6ff85bc485a82562", "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", "zh:86868aec05b58dc0aa1904646a2c26b9367d69b890c9ad70c33c0d3aa7b1485a", @@ -44,26 +43,45 @@ provider "registry.terraform.io/hashicorp/external" { ] } +provider "registry.terraform.io/hashicorp/random" { + version = "3.8.1" + constraints = "~> 3.5" + hashes = [ + "h1:u8AKlWVDTH5r9YLSeswoVEjiY72Rt4/ch7U+61ZDkiQ=", + "zh:08dd03b918c7b55713026037c5400c48af5b9f468f483463321bd18e17b907b4", + "zh:0eee654a5542dc1d41920bbf2419032d6f0d5625b03bd81339e5b33394a3e0ae", + "zh:229665ddf060aa0ed315597908483eee5b818a17d09b6417a0f52fd9405c4f57", + "zh:2469d2e48f28076254a2a3fc327f184914566d9e40c5780b8d96ebf7205f8bc0", + "zh:37d7eb334d9561f335e748280f5535a384a88675af9a9eac439d4cfd663bcb66", + "zh:741101426a2f2c52dee37122f0f4a2f2d6af6d852cb1db634480a86398fa3511", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:a902473f08ef8df62cfe6116bd6c157070a93f66622384300de235a533e9d4a9", + "zh:b85c511a23e57a2147355932b3b6dce2a11e856b941165793a0c3d7578d94d05", + "zh:c5172226d18eaac95b1daac80172287b69d4ce32750c82ad77fa0768be4ea4b8", + "zh:dab4434dba34aad569b0bc243c2d3f3ff86dd7740def373f2a49816bd2ff819b", + "zh:f49fd62aa8c5525a5c17abd51e27ca5e213881d58882fd42fec4a545b53c9699", + ] +} + provider "registry.terraform.io/integrations/github" { - version = "6.7.1" - constraints = "~> 6.6, 6.7.1" + version = "6.7.3" + constraints = "~> 6.6, ~> 6.7, >= 6.7.3, 6.7.3" hashes = [ - "h1:Qkf13o6QBGonJosqmTvQEkbqTvTdrVPjHP/+KCmGA1o=", - "h1:zn1fKzwYdwFi1oGgx3TzFRYI5faBU/dUGe9s9uI5dXg=", - "zh:09a7721e865ce3921cf8639cafe236f7cbe3ae697f9187b59c211b6bf9829ee5", - "zh:1a1bcae681e0d04e919ffbd3aaf48d0d635fc8022e9c1a9037eab04f2ce606cd", - "zh:1ac03fa23913a4059061d5cd1a280826389d4742f7ed0313f4ef084c92bc64d2", - "zh:1ea25064307dc9f48cb9ac7a9c087876c6fdb4e0ccc2f6dc0a4c0c4ced3a913e", - "zh:34507998d42ca57e54f8ba9f092c27902aad1a82368a4412c8455d5b71aaef32", - "zh:78bba89451d96981791ea09fbd94f1b3e222b6a7e0ca5636df1f425bd1c18fd8", - "zh:79021314ee692ce8adbc23e9264ffcc4a492ab7d3018cc1bf5df933b09c94ced", - "zh:7b46f7502f93d7aa37ed8db3e03a2e190acb29927b83842928235261386f098b", - "zh:7d85fe6dd607ec9495307b9e563ea865858b5c3098a5ee132f3d88a23f8bc451", - "zh:a161a8d58345e0dc003f4faa6c4ee71df4a65c72d4558a2a6c9860a1ce3e7438", - "zh:b9e534112d8a8cc02b186a4a0268e5aff58b751180b97667f64383994e8ce9a3", - "zh:d0e149471440698d2172d5584963cc5e1e13c747947c8c0e82c25e28a72ef13a", - "zh:f28e85660660df9bc17f827e942d78fe88c46c00686f279d056e34ee29565f92", + "h1:Jwdu/dDXKwrwLFn6RgnjItp4q3DNHKHPOvxQ6rGmPQE=", + "zh:13686ba2e4e86070a51902b07503c0b3f9d3d3a77ea9ca4487dad68bc9f51e15", + "zh:268180ca9ff7d10046d7b49b30de0e7a35074090995160d5732a9d20336d0c73", + "zh:407b43ca464b199ccd522a9948097f8d90fedc58175bd350747723545c66974b", + "zh:6296ecb946806db0a562604deff3f9fb5a0c4734d680343ed295724c83e8fa2d", + "zh:840bcd0c179c9af82e543af6792c2fd4f4c3ef508667fe8e53b1ea6a32ffa34d", + "zh:a9baa336fce1ab1531e1236178c6c1ac3d20bb9d155c5275f1db4261d2f5f7a8", + "zh:aac75eea9bd55a492ab0624675e9a1894b1b1651ab0315e72e05856fd86a0f79", + "zh:b1016867d89ca96b5815653a1b63065a06f355417c359052c1cbc7ecaff3dc53", + "zh:b186c3c1a085362fe129be696253b1285c308cccff4f9781607747c3f378ee6f", + "zh:b566693f88940fe26064e0ba714305c2d34bda2a79ca914653527ea262989870", + "zh:bcbbd2cb484e18b92e0ce0cbadac5cc3cc0dc96abdab900bf0605de478d0915d", + "zh:cafe39d22a3d78abdf6e50c296757c3ae6a39a4a59040d376e3a0797e08d4117", + "zh:de8e8386b7fa9e1eecc2b3d7fccfe93d84c015a5d4d2c1e8cc78d57b7c4cd04b", + "zh:e237e8216575713322a9cc2172535e0fcd4a91368da5ef94e923bd7f02d3594f", "zh:fbd1fee2c9df3aa19cf8851ce134dea6e45ea01cb85695c1726670c285797e25", - "zh:fccae88ec80ed894aa2d5ec10612700e713b17628947dc348d2716747564a941", ] } diff --git a/.yamllint b/.yamllint index 6b67213..3a7ea3d 100644 --- a/.yamllint +++ b/.yamllint @@ -5,4 +5,4 @@ rules: line-length: max: 120 level: warning -... \ No newline at end of file +... diff --git a/modules/service-repo/README.md b/modules/service-repo/README.md new file mode 100644 index 0000000..5c08be0 --- /dev/null +++ b/modules/service-repo/README.md @@ -0,0 +1,11 @@ +# service-repo + +Terraform module that creates and configures a GitHub repository for deploying +Terraform infrastructure to one or more AWS environments. + +The module is a pure GitHub module — it has no AWS provider dependency. IAM roles +and the state bucket are created by the caller and passed in via `var.environments` +and `var.state_bucket`. + + + diff --git a/modules/service-repo/codeowners.tf b/modules/service-repo/codeowners.tf new file mode 100644 index 0000000..a8acd77 --- /dev/null +++ b/modules/service-repo/codeowners.tf @@ -0,0 +1,35 @@ +locals { + # Order matters: last match wins in CODEOWNERS. + # Catch-all first, then increasingly specific patterns. + standard_codeowners_lines = [ + "* @${var.gh_org_name}/${var.infrastructure_approvers}", + "environments/*/releases.auto.tfvars @${var.gh_org_name}/${var.release_managers}", + ".github/workflows/** @${var.gh_org_name}/${var.pipeline_guardians}", + ] + + extra_codeowners_lines = [ + for path, slug in var.extra_codeowners : + "${path} @${var.gh_org_name}/${slug}" + ] + + codeowners_content = join("\n", concat( + local.standard_codeowners_lines, + local.extra_codeowners_lines, + )) +} + +resource "github_repository_file" "codeowners" { + count = var.archived ? 0 : 1 + repository = github_repository.this.name + file = ".github/CODEOWNERS" + overwrite_on_create = true + content = join("\n", [ + "# This file is managed by Terraform in github-control repository", + "# Do not edit this file, all changes will be overwritten", + "", + local.codeowners_content, + "", + ]) + commit_message = "Update CODEOWNERS" + branch = local.default_branch +} diff --git a/modules/service-repo/custom_properties.tf b/modules/service-repo/custom_properties.tf new file mode 100644 index 0000000..0e6f454 --- /dev/null +++ b/modules/service-repo/custom_properties.tf @@ -0,0 +1,7 @@ +resource "github_repository_custom_property" "this" { + for_each = var.custom_properties + repository = github_repository.this.name + property_name = each.key + property_type = each.value.type + property_value = each.value.value +} diff --git a/modules/service-repo/environments.tf b/modules/service-repo/environments.tf new file mode 100644 index 0000000..f7b3c61 --- /dev/null +++ b/modules/service-repo/environments.tf @@ -0,0 +1,18 @@ +resource "github_repository_environment" "ci" { + for_each = var.environments + environment = "continuous-integration-${each.key}" + repository = github_repository.this.name +} + +resource "github_repository_environment" "cd" { + for_each = var.environments + environment = "live-${each.key}" + repository = github_repository.this.name + + dynamic "reviewers" { + for_each = each.value.deploy_order > 0 ? [1] : [] + content { + teams = [data.github_team.release_managers.id] + } + } +} diff --git a/modules/service-repo/files.tf b/modules/service-repo/files.tf new file mode 100644 index 0000000..1cf25dc --- /dev/null +++ b/modules/service-repo/files.tf @@ -0,0 +1,49 @@ +resource "github_repository_file" "terraform_tf" { + for_each = var.environments + repository = github_repository.this.name + file = "environments/${each.key}/terraform.tf" + overwrite_on_create = true + content = templatefile("${path.module}/templates/terraform.tf.tftpl", { + state_bucket = var.state_bucket + environment = each.key + region = each.value.region + state_manager_role_arn = each.value.state_manager_role_arn + aws_provider_constraint = var.aws_provider_constraint + extra_required_providers = var.extra_required_providers + }) + commit_message = "Add terraform.tf for ${each.key} environment" + branch = local.default_branch +} + +resource "github_repository_file" "terraform_tfvars" { + for_each = var.environments + repository = github_repository.this.name + file = "environments/${each.key}/terraform.tfvars" + overwrite_on_create = true + content = templatefile("${path.module}/templates/terraform.tfvars.tftpl", { + state_manager_role_arn = each.value.state_manager_role_arn + admin_role_arn = each.value.admin_role_arn + github_role_arn = each.value.github_role_arn + gh_org_name = var.gh_org_name + repo_name = var.repo_name + region = each.value.region + }) + commit_message = "Add terraform.tfvars for ${each.key} environment" + branch = local.default_branch + lifecycle { + ignore_changes = [content] + } +} + +resource "github_repository_file" "releases_auto_tfvars" { + for_each = var.environments + repository = github_repository.this.name + file = "environments/${each.key}/releases.auto.tfvars" + overwrite_on_create = true + content = file("${path.module}/templates/releases.auto.tfvars.tftpl") + commit_message = "Add releases.auto.tfvars for ${each.key} environment" + branch = local.default_branch + lifecycle { + ignore_changes = [content] + } +} diff --git a/modules/service-repo/github_actions_variables.tf b/modules/service-repo/github_actions_variables.tf new file mode 100644 index 0000000..6cbe4cd --- /dev/null +++ b/modules/service-repo/github_actions_variables.tf @@ -0,0 +1,41 @@ +resource "github_actions_variable" "role_github" { + repository = github_repository.this.name + variable_name = "ROLE_GITHUB" + value = jsonencode({ + for env, config in var.environments : + env => config.github_role_arn + }) +} + +resource "github_actions_variable" "role_admin" { + repository = github_repository.this.name + variable_name = "ROLE_ADMIN" + value = jsonencode({ + for env, config in var.environments : + env => config.admin_role_arn + }) +} + +resource "github_actions_variable" "role_state_manager" { + repository = github_repository.this.name + variable_name = "ROLE_STATE_MANAGER" + value = jsonencode({ + for env, config in var.environments : + env => config.state_manager_role_arn + }) +} + +resource "github_actions_variable" "state_bucket" { + repository = github_repository.this.name + variable_name = "STATE_BUCKET" + value = var.state_bucket +} + +resource "github_actions_variable" "aws_default_region" { + repository = github_repository.this.name + variable_name = "AWS_DEFAULT_REGION" + value = jsonencode({ + for env, config in var.environments : + env => config.region + }) +} diff --git a/modules/service-repo/locals.tf b/modules/service-repo/locals.tf new file mode 100644 index 0000000..c352fc3 --- /dev/null +++ b/modules/service-repo/locals.tf @@ -0,0 +1,9 @@ +locals { + default_branch = "main" + + required_checks = concat( + [for env in keys(var.environments) : "Terraform Plan ${env}"], + ["TruffleHog", "vulnerability-check"], + var.checks, + ) +} diff --git a/modules/service-repo/main.tf b/modules/service-repo/main.tf new file mode 100644 index 0000000..6812204 --- /dev/null +++ b/modules/service-repo/main.tf @@ -0,0 +1,60 @@ +data "github_app" "terraform" { + slug = var.github_app_slug +} + +data "github_team" "release_managers" { + slug = var.release_managers +} + +resource "github_repository" "this" { + name = var.repo_name + description = var.repo_description + has_downloads = true + has_issues = true + has_projects = true + has_wiki = true + vulnerability_alerts = var.archived ? false : true + allow_update_branch = true + allow_merge_commit = true + allow_rebase_merge = true + allow_squash_merge = true + delete_branch_on_merge = true + squash_merge_commit_title = "COMMIT_OR_PR_TITLE" + squash_merge_commit_message = "COMMIT_MESSAGES" + merge_commit_title = "MERGE_MESSAGE" + merge_commit_message = "PR_TITLE" + archived = var.archived + visibility = var.repo_private ? "private" : "public" + web_commit_signoff_required = false + + template { + owner = var.gh_org_name + repository = var.template_repo + } +} + +resource "github_branch_default" "main" { + branch = local.default_branch + repository = github_repository.this.name +} + +resource "github_team_repository" "committers" { + for_each = var.committers + repository = github_repository.this.name + team_id = each.value + permission = "push" +} + +resource "github_team_repository" "admins" { + for_each = var.admins + repository = github_repository.this.name + team_id = each.value + permission = "admin" +} + +resource "github_actions_secret" "secret" { + for_each = var.secrets + repository = github_repository.this.name + secret_name = each.key + plaintext_value = each.value +} diff --git a/modules/service-repo/outputs.tf b/modules/service-repo/outputs.tf new file mode 100644 index 0000000..83bb5bc --- /dev/null +++ b/modules/service-repo/outputs.tf @@ -0,0 +1,9 @@ +output "repository_name" { + description = "The created repository name." + value = github_repository.this.name +} + +output "repository_node_id" { + description = "The GraphQL node ID of the repository." + value = github_repository.this.node_id +} diff --git a/modules/service-repo/rulesets.tf b/modules/service-repo/rulesets.tf new file mode 100644 index 0000000..2db6186 --- /dev/null +++ b/modules/service-repo/rulesets.tf @@ -0,0 +1,37 @@ +resource "github_repository_ruleset" "main" { + name = "Main Branch Protection" + repository = github_repository.this.name + target = "branch" + enforcement = "active" + + conditions { + ref_name { + include = ["~DEFAULT_BRANCH"] + exclude = [] + } + } + + bypass_actors { + actor_id = data.github_app.terraform.id + actor_type = "Integration" + bypass_mode = "always" + } + + rules { + required_status_checks { + strict_required_status_checks_policy = true + dynamic "required_check" { + for_each = toset(local.required_checks) + content { + context = required_check.key + } + } + } + + pull_request { + dismiss_stale_reviews_on_push = true + require_code_owner_review = true + required_approving_review_count = var.approvals_count + } + } +} diff --git a/modules/service-repo/templates/releases.auto.tfvars.tftpl b/modules/service-repo/templates/releases.auto.tfvars.tftpl new file mode 100644 index 0000000..b93e5df --- /dev/null +++ b/modules/service-repo/templates/releases.auto.tfvars.tftpl @@ -0,0 +1,3 @@ +# Release-managed values — bumped by CI pipelines or release managers. +# This file is owned by the service repo, not github-control. +docker_image_tag = "latest" diff --git a/modules/service-repo/templates/secrets-scanner.yml b/modules/service-repo/templates/secrets-scanner.yml new file mode 100644 index 0000000..0d6cb75 --- /dev/null +++ b/modules/service-repo/templates/secrets-scanner.yml @@ -0,0 +1,24 @@ +# This file is managed by Terraform in github-control repository +# Do not edit this file, all changes will be overwritten +--- +name: Leaked Secrets Scan +on: # yamllint disable-line rule:truthy + pull_request: + merge_group: + branches: [main] + +jobs: + TruffleHog: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: TruffleHog OSS + uses: trufflesecurity/trufflehog@main + with: + path: ./ + base: ${{ github.event.repository.default_branch }} + head: HEAD + extra_args: --only-verified diff --git a/modules/service-repo/templates/terraform-drift-wrapper.yml.tftpl b/modules/service-repo/templates/terraform-drift-wrapper.yml.tftpl new file mode 100644 index 0000000..8206f48 --- /dev/null +++ b/modules/service-repo/templates/terraform-drift-wrapper.yml.tftpl @@ -0,0 +1,33 @@ +--- +name: "[Meta] Terraform Drift Detection" + +on: + workflow_dispatch: + schedule: + # Runs daily at 10:XX AM UTC + - cron: "${random_minute} 10 * * *" + +jobs: + config-drift-check: + strategy: + matrix: + env: ${jsonencode(environments)} + uses: "./.github/workflows/terraform-drift.yml" + with: + env: "$${{ matrix.env }}" + secrets: "inherit" + + notify_on_failure: + name: "Notify #infra slack on failure" + needs: ["config-drift-check"] + if: $${{ failure() }} + runs-on: ubuntu-latest + steps: + - name: Post to Slack + uses: "slackapi/slack-github-action@v2.1.1" + with: + method: "chat.postMessage" + token: $${{ secrets.SLACK_BOT_TOKEN }} + payload: | + channel: "$${{ vars.INFRA_NOTIFICATION_CHANNEL }}" + text: "❌ Terraform Drift Detection failed for $${{ github.repository }}. Check the workflow run for more details: $${{ github.server_url }}/$${{ github.repository }}/actions/runs/$${{ github.run_id }}." diff --git a/modules/service-repo/templates/terraform-drift.yml b/modules/service-repo/templates/terraform-drift.yml new file mode 100644 index 0000000..11252fc --- /dev/null +++ b/modules/service-repo/templates/terraform-drift.yml @@ -0,0 +1,120 @@ +--- +name: "Terraform Drift Detection" + +on: # yamllint disable-line rule:truthy + workflow_call: + inputs: + env: + type: "string" + required: true + +permissions: + id-token: "write" # This is required for requesting the JWT + contents: "write" + pull-requests: "write" + issues: "write" # Required to create and apply labels + +concurrency: + group: "terraform-drift-${{ inputs.env }}" + cancel-in-progress: false + +jobs: + terraform-drift: + name: "Terraform Drift Detection" + runs-on: ubuntu-24.04 + environment: "continuous-integration-${{ inputs.env }}" + timeout-minutes: 15 + env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + REGION_JSON: "${{ vars.AWS_DEFAULT_REGION }}" + + defaults: + run: + shell: "bash" + working-directory: "environments/${{ inputs.env }}" + + steps: + - name: "Checkout" + uses: "actions/checkout@v6" + + - name: "Extract Variables" + id: "extract_vars" + env: + REGION_JSON_CONTENT: ${{ env.REGION_JSON }} + TARGET_ENV: ${{ inputs.env }} + run: | + REGION=$(echo "$REGION_JSON_CONTENT" | jq -r ".${TARGET_ENV}") + echo "REGION=$REGION" >> "$GITHUB_OUTPUT" + + - name: "Configure AWS Credentials" + uses: "aws-actions/configure-aws-credentials@v6" + with: + role-to-assume: "${{ vars.ROLE_GITHUB }}" + role-session-name: "github-actions-${{ inputs.env }}" + aws-region: "${{ steps.extract_vars.outputs.REGION }}" + + - name: "Set Terraform version" + id: "terraform_version" + run: | + echo "IH_TF_VERSION=$(cat .terraform-version)" >> "$GITHUB_OUTPUT" + + - name: "Setup Terraform" + uses: "hashicorp/setup-terraform@v4" + with: + terraform_version: "${{ steps.terraform_version.outputs.IH_TF_VERSION }}" + + - name: "Set up Python" + uses: "actions/setup-python@v6" + with: + python-version: "3.14" + + - name: "Setup Python Environment" + run: | + make bootstrap-ci + + - name: "Terraform Init" + run: | + terraform init -input=false + + - name: "Check if any changes are planned" + id: "check_drift" + run: | + terraform plan -no-color -input=false -detailed-exitcode + + - name: Cleanup working dir + run: git clean -df + + - name: "Record Config Drift Log Entry" + if: "steps.check_drift.outputs.exitcode == 2" + run: | + git \ + -c user.name="${{ github.actor }}" \ + -c user.email="${{ github.actor_id }}+${{ github.actor }}@users.noreply.github.com" \ + commit -m "Reconcile configuration drift" --allow-empty + + - name: "Check for open PR with label" + id: "check_label" + if: "steps.check_drift.outputs.exitcode == 2" + run: | + LABEL="config-drift" + REPO="${{ github.repository }}" + COUNT=$(gh pr list --repo "$REPO" --label "$LABEL" --state open --json number --jq 'length') + echo "Found $COUNT open PR(s) with label '$LABEL'" + if [[ $COUNT -gt 0 ]]; then + echo "label_exists=true" >> $GITHUB_OUTPUT + else + echo "label_exists=false" >> $GITHUB_OUTPUT + fi + + - name: "Create Pull Request" + if: "steps.check_label.outputs.label_exists == 'false'" + uses: "peter-evans/create-pull-request@v8" + with: + token: "${{ steps.app-token.outputs.token }}" + base: "main" + branch: "create-pull-request/config-drift" + title: "[config-drift] Reconcile Terraform configuration drift in ${{ inputs.env }}" + commit-message: "[config-drift] Configuration drift record for ${{ inputs.env }}" + team-reviewers: "devops-members" + labels: | + config-drift diff --git a/modules/service-repo/templates/terraform.tf.tftpl b/modules/service-repo/templates/terraform.tf.tftpl new file mode 100644 index 0000000..9f88e3d --- /dev/null +++ b/modules/service-repo/templates/terraform.tf.tftpl @@ -0,0 +1,26 @@ +terraform { + required_version = ">= 1.10" + backend "s3" { + bucket = "${state_bucket}" + key = "${environment}/terraform.tfstate" + region = "${region}" + encrypt = true + use_lockfile = true + assume_role = { + role_arn = "${state_manager_role_arn}" + } + } + + required_providers { + aws = { + source = "hashicorp/aws" + version = "${aws_provider_constraint}" + } +%{ for name, provider in extra_required_providers ~} + ${name} = { + source = "${provider.source}" + version = "${provider.version}" + } +%{ endfor ~} + } +} diff --git a/modules/service-repo/templates/terraform.tfvars.tftpl b/modules/service-repo/templates/terraform.tfvars.tftpl new file mode 100644 index 0000000..40cf1c0 --- /dev/null +++ b/modules/service-repo/templates/terraform.tfvars.tftpl @@ -0,0 +1,10 @@ +# This file was initially generated by Terraform in github-control repository +# Local changes are preserved after initial creation +aws_iam_role_arn_state_manager = "${state_manager_role_arn}" +aws_iam_role_arn_admin = "${admin_role_arn}" +aws_iam_role_arn_github = "${github_role_arn}" +default_tags = { + "created_by" : "${gh_org_name}/${repo_name}" +} + +aws_default_region = "${region}" diff --git a/modules/service-repo/templates/vuln-scanner-pr.yml b/modules/service-repo/templates/vuln-scanner-pr.yml new file mode 100644 index 0000000..f3cad70 --- /dev/null +++ b/modules/service-repo/templates/vuln-scanner-pr.yml @@ -0,0 +1,53 @@ +# This file is managed by Terraform in github-control repository +# Do not edit this file, all changes will be overwritten +--- +name: OSV-Scanner PR Scan + +on: # yamllint disable-line rule:truthy + pull_request: + branches: [main] + merge_group: + branches: [main] + +permissions: + contents: read + pull-requests: write + +env: + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + +jobs: + vulnerability-check: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + - name: Detect vulnerabilities + run: | + if [ -n "${{ github.event.pull_request.number }}" ]; then + ih-github scan \ + --repo ${{ github.repository }} \ + --pull-request ${{ github.event.pull_request.number }} + else + ih-github scan + fi + + sast-check: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v6 + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: "3.14" + + - name: SAST + run: | + pip install --upgrade semgrep + if [ -n "${{ github.event.pull_request.number }}" ]; then + ih-github run \ + ${{ github.repository }} \ + ${{ github.event.pull_request.number }} \ + semgrep scan --error + else + semgrep scan --error + fi diff --git a/modules/service-repo/terraform.tf b/modules/service-repo/terraform.tf new file mode 100644 index 0000000..3499a8b --- /dev/null +++ b/modules/service-repo/terraform.tf @@ -0,0 +1,11 @@ +terraform { + required_providers { + github = { + source = "integrations/github" + version = "~> 6.7, >= 6.7.3" + } + random = { + source = "hashicorp/random" + } + } +} diff --git a/modules/service-repo/variables.tf b/modules/service-repo/variables.tf new file mode 100644 index 0000000..561d0c5 --- /dev/null +++ b/modules/service-repo/variables.tf @@ -0,0 +1,228 @@ +variable "repo_name" { + description = "Repository name (without the org prefix)." + type = string + + validation { + condition = can(regex("^[a-z0-9][a-z0-9-]*[a-z0-9]$", var.repo_name)) + error_message = <<-EOT + repo_name must be lowercase alphanumeric with hyphens, cannot start + or end with a hyphen. Got: ${var.repo_name} + EOT + } +} + +variable "repo_description" { + description = "One-line repository description." + type = string +} + +variable "gh_org_name" { + description = "GitHub organization name." + type = string +} + +variable "github_app_slug" { + description = "Slug of the GitHub App used for branch protection bypass." + type = string +} + +variable "environments" { + description = <<-EOT + Map of environment name to configuration. Each environment gets its own + state key in the shared state bucket, injected backend configuration, + and CI/CD workflow matrix entry. + + The map key is the environment name (e.g., "sandbox", "production", + "staging"). It must be lowercase alphanumeric with underscores only + (no hyphens) to comply with Puppet environment naming and Terraform + workspace conventions. + + IAM role ARNs are created by the caller (typically via the published + terraform-aws-gha-admin module) and passed in here. The module does + not create IAM roles or state buckets. + + deploy_order controls the CD workflow sequence. Environments with the + same order deploy in parallel; higher numbers wait for lower ones to + finish (chained via needs:). Environments with deploy_order > 0 are + gated by a GitHub environment protection rule — var.release_managers + must approve before the deployment proceeds. Order 0 (default) deploys + automatically after PR merge. + EOT + type = map(object({ + region = string + admin_role_arn = string + github_role_arn = string + state_manager_role_arn = string + deploy_order = optional(number, 0) + })) + + validation { + condition = alltrue([ + for name, _ in var.environments : + can(regex("^[a-z0-9_]+$", name)) + ]) + error_message = <<-EOT + Environment names must contain only lowercase letters, numbers, + and underscores (no hyphens). + EOT + } + + validation { + condition = alltrue([ + for _, env in var.environments : + can(regex("^arn:aws:iam::[0-9]{12}:role/", env.admin_role_arn)) + ]) + error_message = "admin_role_arn must be a valid IAM role ARN." + } + + validation { + condition = alltrue([ + for _, env in var.environments : + can(regex("^arn:aws:iam::[0-9]{12}:role/", env.github_role_arn)) + ]) + error_message = "github_role_arn must be a valid IAM role ARN." + } + + validation { + condition = alltrue([ + for _, env in var.environments : + can(regex("^arn:aws:iam::[0-9]{12}:role/", env.state_manager_role_arn)) + ]) + error_message = "state_manager_role_arn must be a valid IAM role ARN." + } +} + +variable "state_bucket" { + description = <<-EOT + Name of the S3 bucket used for Terraform state. A single bucket is shared + across all environments; each environment uses a different key + ({environment}/terraform.tfstate). Created by the caller. + EOT + type = string +} + +variable "template_repo" { + description = <<-EOT + Name of the template repository to use when creating the service repo. + The template provides CI/CD workflows, Makefile, .gitignore, and other + boilerplate. Files are copied once at creation time — the service repo + owns them after that. + EOT + type = string +} + +variable "repo_private" { + description = "If true, the repository is private." + type = bool + default = true +} + +variable "committers" { + description = "Map of {team-name: team_id} with push permission." + type = map(string) + default = {} +} + +variable "admins" { + description = "Map of {team-name: team_id} with admin permission." + type = map(string) + default = {} +} + +variable "secrets" { + description = "Map of GitHub Actions secret name to value." + type = map(string) + default = {} +} + +variable "checks" { + description = <<-EOT + Additional required status checks beyond the auto-generated + per-environment "Terraform Plan {env}" checks. + EOT + type = list(string) + default = [] +} + +variable "approvals_count" { + description = "Number of PR approvals required." + type = number + default = 1 +} + +variable "aws_provider_constraint" { + description = "Version constraint for the AWS provider in generated terraform.tf files." + type = string + default = "~> 6.0" +} + +variable "extra_required_providers" { + description = "Additional provider blocks to include in generated terraform.tf files." + type = map(object({ + source = string + version = string + })) + default = {} +} + +variable "pipeline_guardians" { + description = <<-EOT + GitHub team slug that must approve changes to .github/workflows/**. + PCI-DSS separation of duties: pipeline changes require independent + review from a security/platform function. + EOT + type = string +} + +variable "infrastructure_approvers" { + description = <<-EOT + GitHub team slug that must approve infrastructure changes (catch-all + CODEOWNERS * entry, excluding release files). + EOT + type = string +} + +variable "release_managers" { + description = <<-EOT + GitHub team slug that must approve release changes — Docker image + labels, feature flags, and application configuration in + environments/{env}/releases.auto.tfvars. + EOT + type = string +} + +variable "extra_codeowners" { + description = <<-EOT + Additional CODEOWNERS entries beyond the three standard functions + (pipeline_guardians, infrastructure_approvers, release_managers). + Map of file path patterns to GitHub team slugs. + EOT + type = map(string) + default = {} +} + +variable "custom_properties" { + description = <<-EOT + Map of GitHub custom property name to configuration. Property definitions + must already exist at the org level. + EOT + type = map(object({ + type = string + value = list(string) + })) + default = {} + + validation { + condition = alltrue([ + for _, prop in var.custom_properties : + contains(["single_select", "multi_select", "string", "true_false"], prop.type) + ]) + error_message = "property type must be one of: single_select, multi_select, string, true_false." + } +} + +variable "archived" { + description = "Set to true to archive the repository." + type = bool + default = false +} diff --git a/modules/service-repo/workflows.tf b/modules/service-repo/workflows.tf new file mode 100644 index 0000000..b9113fe --- /dev/null +++ b/modules/service-repo/workflows.tf @@ -0,0 +1,58 @@ +resource "github_repository_file" "terraform_drift" { + count = var.archived ? 0 : 1 + depends_on = [ + github_repository_ruleset.main + ] + repository = github_repository.this.name + file = ".github/workflows/terraform-drift.yml" + content = file("${path.module}/templates/terraform-drift.yml") + commit_message = "Add terraform-drift.yml workflow" + overwrite_on_create = true +} + +resource "random_integer" "minute" { + min = 0 + max = 59 +} + +resource "github_repository_file" "terraform_drift_wrapper" { + count = var.archived ? 0 : 1 + depends_on = [ + github_repository_ruleset.main + ] + repository = github_repository.this.name + file = ".github/workflows/terraform-drift-wrapper.yml" + content = templatefile( + "${path.module}/templates/terraform-drift-wrapper.yml.tftpl", + { + random_minute = random_integer.minute.result + environments = keys(var.environments) + } + ) + commit_message = "Add terraform-drift-wrapper.yml workflow" + overwrite_on_create = true +} + +resource "github_repository_file" "secrets_scanner" { + count = var.archived ? 0 : 1 + depends_on = [ + github_repository_ruleset.main + ] + repository = github_repository.this.name + file = ".github/workflows/secrets-scanner.yml" + content = file("${path.module}/templates/secrets-scanner.yml") + commit_message = "Add secrets-scanner.yml workflow" + overwrite_on_create = true +} + +resource "github_repository_file" "vuln_scanner" { + count = var.archived ? 0 : 1 + depends_on = [ + github_repository_ruleset.main + ] + repository = github_repository.this.name + file = ".github/workflows/vuln-scanner-pr.yml" + content = file("${path.module}/templates/vuln-scanner-pr.yml") + commit_message = "Add vuln-scanner-pr.yml workflow" + overwrite_on_create = true +} diff --git a/override.tf b/override.tf new file mode 100644 index 0000000..e69de29 diff --git a/provider.github.tf b/provider.github.tf index 2f7179e..c1da3af 100644 --- a/provider.github.tf +++ b/provider.github.tf @@ -3,16 +3,20 @@ provider "github" { app_auth { id = "1016363" installation_id = "55607614" - pem_file = module.infrahouse-github-terraform-pem.secret_value + pem_file = local.infrahouse-github-terraform-pem } } +locals { + infrahouse-github-terraform-pem = file("${path.module}/.env/infrahouse-github-terraform.pem") +} + provider "github" { owner = "infrahouse8" alias = "infrahouse8" app_auth { id = "1016363" installation_id = "55799033" - pem_file = module.infrahouse-github-terraform-pem.secret_value + pem_file = local.infrahouse-github-terraform-pem } } diff --git a/terraform.tf b/terraform.tf index 31825c4..5689be1 100644 --- a/terraform.tf +++ b/terraform.tf @@ -13,7 +13,7 @@ terraform { required_providers { github = { source = "integrations/github" - version = "6.7.1" + version = "6.7.3" } aws = { source = "hashicorp/aws" diff --git a/test-service-repo.tf b/test-service-repo.tf new file mode 100644 index 0000000..5745a8c --- /dev/null +++ b/test-service-repo.tf @@ -0,0 +1,63 @@ +# ============================================================ +# Test service repo — safe to delete after validation +# ============================================================ + +# --- State bucket (one per repo, in the tfstates account) --- +module "test_service_state" { + source = "registry.infrahouse.com/infrahouse/state-bucket/aws" + version = "2.2.0" + providers = { + aws = aws.aws-289256138624-uw1 + } + bucket = "infrahouse-github-control-aws-service-test-delete-me" +} + +# --- IAM roles (one gha-admin call per environment) --- +# Using 303467602807 as the sandbox workload account. +# aws.cicd points to the same account — github role lands there. +module "test_service_gha_sandbox" { + source = "registry.infrahouse.com/infrahouse/gha-admin/aws" + version = "3.6.1" + providers = { + aws = aws.aws-303467602807-uw1 + aws.cicd = aws.aws-303467602807-uw1 + aws.tfstates = aws.aws-289256138624-uw1 + } + gh_org_name = "infrahouse" + repo_name = "aws-service-test-delete-me" + state_bucket = module.test_service_state.bucket_name + terraform_locks_table_arn = module.test_service_state.lock_table_arn +} + +# --- The service repo (pure GitHub, no AWS) --- +module "test_service" { + source = "./modules/service-repo" + + repo_name = "aws-service-test-delete-me" + repo_description = "Test service repo — safe to delete" + gh_org_name = "infrahouse" + github_app_slug = "infrahouse-github-terraform" + template_repo = "terraform-root-template" + state_bucket = module.test_service_state.bucket_name + + environments = { + sandbox = { + region = "us-west-1" + admin_role_arn = module.test_service_gha_sandbox.admin_role_arn + github_role_arn = module.test_service_gha_sandbox.github_role_arn + state_manager_role_arn = module.test_service_gha_sandbox.state_manager_role_arn + deploy_order = 0 + } + } + + pipeline_guardians = "admins" + infrastructure_approvers = "developers" + release_managers = "admins" + + committers = { + developers = github_team.dev.id + } + admins = { + admins = github_team.admins.id + } +} From 74b12b730182e7349f3db320ab9c0c0d5743c91e Mon Sep 17 00:00:00 2001 From: Oleksandr Kuzminskyi Date: Tue, 5 May 2026 08:43:08 -0700 Subject: [PATCH 2/3] Rename test_service modules and clean up hardcoded values Rename module identifiers from test_service to aws_service_infrahouse_app to match the repo being managed. Replace hardcoded strings with locals and team references. Remove unused gh_secrets data source. Co-Authored-By: Claude Opus 4.6 --- data_sources.tf | 10 ----- locals.tf | 6 +-- provider.github.tf | 8 +--- repo-aws-service-infrahouse-app.tf | 63 ++++++++++++++++++++++++++++++ test-service-repo.tf | 63 ------------------------------ 5 files changed, 68 insertions(+), 82 deletions(-) create mode 100644 repo-aws-service-infrahouse-app.tf delete mode 100644 test-service-repo.tf diff --git a/data_sources.tf b/data_sources.tf index 14f6ab5..bb53e07 100644 --- a/data_sources.tf +++ b/data_sources.tf @@ -28,16 +28,6 @@ data "aws_ssm_parameter" "github_control_lock_table" { name = "/terraform/github-control/backend/lock_table" } - -data "aws_secretsmanager_secrets" "gh_secrets" { - provider = aws.aws-303467602807-uw1 - filter { - name = "name" - values = [] - } -} - - data "aws_secretsmanager_secret_version" "pypi_api_token" { provider = aws.aws-303467602807-uw1 secret_id = aws_secretsmanager_secret.pypi_api_token.id diff --git a/locals.tf b/locals.tf index fcfdbdb..f98290a 100644 --- a/locals.tf +++ b/locals.tf @@ -1,9 +1,9 @@ locals { - aws_account_id = "990466748045" aws_default_region = "us-west-1" - - s_prefix = "${data.aws_ssm_parameter.gh_secrets_namespace.value}tf_admin" environment = "production" + + gh_org_name = "infrahouse" + team_members = { "akuzminsky" : [ github_team.dev.name, diff --git a/provider.github.tf b/provider.github.tf index c1da3af..2f7179e 100644 --- a/provider.github.tf +++ b/provider.github.tf @@ -3,20 +3,16 @@ provider "github" { app_auth { id = "1016363" installation_id = "55607614" - pem_file = local.infrahouse-github-terraform-pem + pem_file = module.infrahouse-github-terraform-pem.secret_value } } -locals { - infrahouse-github-terraform-pem = file("${path.module}/.env/infrahouse-github-terraform.pem") -} - provider "github" { owner = "infrahouse8" alias = "infrahouse8" app_auth { id = "1016363" installation_id = "55799033" - pem_file = local.infrahouse-github-terraform-pem + pem_file = module.infrahouse-github-terraform-pem.secret_value } } diff --git a/repo-aws-service-infrahouse-app.tf b/repo-aws-service-infrahouse-app.tf new file mode 100644 index 0000000..153e6cd --- /dev/null +++ b/repo-aws-service-infrahouse-app.tf @@ -0,0 +1,63 @@ +# ============================================================ +# InfraHouse product app — service repo +# ============================================================ + +# --- State bucket (one per repo, in the tfstates account) --- +module "aws_service_infrahouse_app_state" { + source = "registry.infrahouse.com/infrahouse/state-bucket/aws" + version = "2.2.0" + providers = { + aws = aws.aws-289256138624-uw1 + } + bucket = "infrahouse-github-control-aws-service-infrahouse-app" +} + +# --- IAM roles (one gha-admin call per environment) --- +# Using 303467602807 as the sandbox workload account. +# aws.cicd points to the same account — github role lands there. +module "aws_service_infrahouse_app_gha_sandbox" { + source = "registry.infrahouse.com/infrahouse/gha-admin/aws" + version = "3.6.1" + providers = { + aws = aws.aws-303467602807-uw1 + aws.cicd = aws.aws-303467602807-uw1 + aws.tfstates = aws.aws-289256138624-uw1 + } + gh_org_name = local.gh_org_name + repo_name = "aws-service-infrahouse-app" + state_bucket = module.aws_service_infrahouse_app_state.bucket_name + terraform_locks_table_arn = module.aws_service_infrahouse_app_state.lock_table_arn +} + +# --- The service repo (pure GitHub, no AWS) --- +module "aws_service_infrahouse_app" { + source = "./modules/service-repo" + + repo_name = "aws-service-infrahouse-app" + repo_description = "AWS infrastructure for the InfraHouse product app" + gh_org_name = local.gh_org_name + github_app_slug = "infrahouse-github-terraform" + template_repo = "terraform-root-template" + state_bucket = module.aws_service_infrahouse_app_state.bucket_name + + environments = { + sandbox = { + region = local.aws_default_region + admin_role_arn = module.aws_service_infrahouse_app_gha_sandbox.admin_role_arn + github_role_arn = module.aws_service_infrahouse_app_gha_sandbox.github_role_arn + state_manager_role_arn = module.aws_service_infrahouse_app_gha_sandbox.state_manager_role_arn + deploy_order = 0 + } + } + + pipeline_guardians = github_team.admins.slug + infrastructure_approvers = github_team.dev.slug + release_managers = github_team.admins.slug + + committers = { + developers = github_team.dev.id + } + admins = { + admins = github_team.admins.id + } +} diff --git a/test-service-repo.tf b/test-service-repo.tf deleted file mode 100644 index 5745a8c..0000000 --- a/test-service-repo.tf +++ /dev/null @@ -1,63 +0,0 @@ -# ============================================================ -# Test service repo — safe to delete after validation -# ============================================================ - -# --- State bucket (one per repo, in the tfstates account) --- -module "test_service_state" { - source = "registry.infrahouse.com/infrahouse/state-bucket/aws" - version = "2.2.0" - providers = { - aws = aws.aws-289256138624-uw1 - } - bucket = "infrahouse-github-control-aws-service-test-delete-me" -} - -# --- IAM roles (one gha-admin call per environment) --- -# Using 303467602807 as the sandbox workload account. -# aws.cicd points to the same account — github role lands there. -module "test_service_gha_sandbox" { - source = "registry.infrahouse.com/infrahouse/gha-admin/aws" - version = "3.6.1" - providers = { - aws = aws.aws-303467602807-uw1 - aws.cicd = aws.aws-303467602807-uw1 - aws.tfstates = aws.aws-289256138624-uw1 - } - gh_org_name = "infrahouse" - repo_name = "aws-service-test-delete-me" - state_bucket = module.test_service_state.bucket_name - terraform_locks_table_arn = module.test_service_state.lock_table_arn -} - -# --- The service repo (pure GitHub, no AWS) --- -module "test_service" { - source = "./modules/service-repo" - - repo_name = "aws-service-test-delete-me" - repo_description = "Test service repo — safe to delete" - gh_org_name = "infrahouse" - github_app_slug = "infrahouse-github-terraform" - template_repo = "terraform-root-template" - state_bucket = module.test_service_state.bucket_name - - environments = { - sandbox = { - region = "us-west-1" - admin_role_arn = module.test_service_gha_sandbox.admin_role_arn - github_role_arn = module.test_service_gha_sandbox.github_role_arn - state_manager_role_arn = module.test_service_gha_sandbox.state_manager_role_arn - deploy_order = 0 - } - } - - pipeline_guardians = "admins" - infrastructure_approvers = "developers" - release_managers = "admins" - - committers = { - developers = github_team.dev.id - } - admins = { - admins = github_team.admins.id - } -} From d0c0ec42d6ccda70039d989d41e9a9489300d30f Mon Sep 17 00:00:00 2001 From: Oleksandr Kuzminskyi Date: Tue, 5 May 2026 08:56:22 -0700 Subject: [PATCH 3/3] Fix formatting --- locals.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locals.tf b/locals.tf index f98290a..45c5d0d 100644 --- a/locals.tf +++ b/locals.tf @@ -1,6 +1,6 @@ locals { aws_default_region = "us-west-1" - environment = "production" + environment = "production" gh_org_name = "infrahouse"