diff --git a/.github/workflows/on-pull-request.yaml b/.github/workflows/on-pull-request.yaml index b33b6ed..1856954 100644 --- a/.github/workflows/on-pull-request.yaml +++ b/.github/workflows/on-pull-request.yaml @@ -2,20 +2,20 @@ name: "Pull Request Jobs" on: pull_request: - + workflow_dispatch: env: - tf_version: 1.7.1 + tf_version: 1.9.8 working_dir: . jobs: terraform-doc-generation: permissions: - contents: 'write' - id-token: 'write' - pull-requests: 'write' - issues: 'write' - name: "Terraform Documentation Generation" + contents: "write" + id-token: "write" + pull-requests: "write" + issues: "write" + name: "Terraform doc generation and tests" runs-on: ubuntu-latest defaults: run: @@ -37,7 +37,7 @@ jobs: run: terraform fmt -check - name: Generate TF docs - uses: terraform-docs/gh-actions@v1.0.0 + uses: terraform-docs/gh-actions@v1.3.0 with: find-dir: modules/ recursive: true @@ -45,3 +45,15 @@ jobs: git-push: true output-method: replace template: "{{ .Content }}" + + # recurse every directory in the ./modules directory + # and run the terraform test command + - name: Run Terraform Unit Tests + run: | + for dir in $(find ${{env.working_dir}}/modules -type d -not -path '*/\.terraform/*'); do + echo "Running tests in $dir" + cd $dir + terraform init + terraform test + cd - + done diff --git a/modules/enterprise-organization/organization.tftest.hcl b/modules/enterprise-organization/organization.tftest.hcl new file mode 100644 index 0000000..d6897ab --- /dev/null +++ b/modules/enterprise-organization/organization.tftest.hcl @@ -0,0 +1,44 @@ +mock_provider "github" {} + +variables { + # required variables + enterprise_id = "1234567890" + name = "github-foundations" + display_name = "GitHub Foundations" + description = "GitHub Foundations Organization" + billing_email = "billingemail@focisolutions.com" + admin_logins = ["admin1", "admin2"] +} + +run "organization_test" { + command = apply + + assert { + condition = github_enterprise_organization.organization.id != null + error_message = "The organization was not created." + } + assert { + condition = github_enterprise_organization.organization.enterprise_id == var.enterprise_id + error_message = "The organization id is incorrect. Expected ${var.enterprise_id} but got ${github_enterprise_organization.organization.enterprise_id}." + } + assert { + condition = github_enterprise_organization.organization.name == var.name + error_message = "The organization name is incorrect. Expected ${var.name} but got ${github_enterprise_organization.organization.name}." + } + assert { + condition = github_enterprise_organization.organization.display_name == var.display_name + error_message = "The organization display name is incorrect. Expected ${var.display_name} but got ${github_enterprise_organization.organization.display_name}." + } + assert { + condition = github_enterprise_organization.organization.description == var.description + error_message = "The organization description is incorrect. Expected ${var.description} but got ${github_enterprise_organization.organization.description}." + } + assert { + condition = github_enterprise_organization.organization.billing_email == var.billing_email + error_message = "The organization billing email is incorrect. Expected ${var.billing_email} but got ${github_enterprise_organization.organization.billing_email}." + } + assert { + condition = length(github_enterprise_organization.organization.admin_logins) == length(var.admin_logins) + error_message = "The organization admin logins are incorrect. Expected ${length(var.admin_logins)} but got ${length(github_enterprise_organization.organization.admin_logins)}." + } +} diff --git a/modules/github-aws-oidc/oidc.tftest.hcl b/modules/github-aws-oidc/oidc.tftest.hcl new file mode 100644 index 0000000..f920314 --- /dev/null +++ b/modules/github-aws-oidc/oidc.tftest.hcl @@ -0,0 +1,97 @@ +mock_provider "github" {} +mock_provider "aws" {} + +variables { + # required variables + github_foundations_organization_name = "github-foundations-org" + github_thumbprints = ["990F4193972F2BECF12DDEDA5237F9C952F20D9E"] +} + +run "oidc_provider_entry_test" { + command = apply + + assert { + condition = aws_iam_openid_connect_provider.oidc_provider_entry.url == "https://token.actions.githubusercontent.com" + error_message = "The url of the openid connect provider entry is incorrect. Expected 'https://token.actions.githubusercontent.com' but got '${aws_iam_openid_connect_provider.oidc_provider_entry.url}'." + } + assert { + condition = aws_iam_openid_connect_provider.oidc_provider_entry.client_id_list != null + error_message = "The client id list of the openid connect provider entry is incorrect. Expected a non-null value but got 'null'." + } + assert { + condition = aws_iam_openid_connect_provider.oidc_provider_entry.thumbprint_list[0] == "990F4193972F2BECF12DDEDA5237F9C952F20D9E" + error_message = "The thumbprint list of the openid connect provider entry is incorrect. Expected '990F4193972F2BECF12DDEDA5237F9C952F20D9E' but got '${aws_iam_openid_connect_provider.oidc_provider_entry.thumbprint_list[0]}'." + } +} + +run "organizations_role_test" { + + assert { + condition = aws_iam_role.organizations_role.name == "GhFoundationsOrganizationsAction" + error_message = "The name of the organizations role is incorrect. Expected 'GhFoundationsOrganizationsAction' but got '${aws_iam_role.organizations_role.name}'." + } + assert { + condition = jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Effect"] == "Allow" + error_message = "The assume role policy of the organizations role is incorrect. Expected 'Allow' but got '${jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Effect"]}'." + } + assert { + condition = jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Action"] == "sts:AssumeRoleWithWebIdentity" + error_message = "The assume role policy action of the organizations role is incorrect. Expected 'sts:AssumeRoleWithWebIdentity' but got '${jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Action"]}'." + } + assert { + condition = jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Principal"]["Federated"] == aws_iam_openid_connect_provider.oidc_provider_entry.arn + error_message = "The assume role policy principal federated of the organizations role is incorrect. Expected '${aws_iam_openid_connect_provider.oidc_provider_entry.arn}' but got '${jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Principal"]["Federated"]}'." + } + assert { + condition = jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Condition"]["StringEquals"]["token.actions.githubusercontent.com:aud"][0] == "sts.amazonaws.com" + error_message = "The assume role policy condition string equals token actions githubusercontent com aud of the organizations role is incorrect. Expected 'sts.amazonaws.com' but got '${jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Condition"]["StringEquals"]["token.actions.githubusercontent.com:aud"][0]}'." + } + assert { + condition = jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Condition"]["StringLike"]["token.actions.githubusercontent.com:sub"][0] == "repo:github-foundations-org/organizations:*" + error_message = "The assume role policy condition string like token actions githubusercontent com sub of the organizations role is incorrect. Expected 'repo:github-foundations-org/organizations:*' but got '${jsondecode(aws_iam_role.organizations_role.assume_role_policy)["Statement"][0]["Condition"]["StringLike"]["token.actions.githubusercontent.com:sub"][0]}'." + } + assert { + condition = aws_iam_role.organizations_role.tags.Purpose == "Github Foundations" + error_message = "The tags of the organizations role are incorrect. Expected 'Github Foundations' but got '${aws_iam_role.organizations_role.tags.Purpose}'." + } +} + +run "organizations_role_policy_test" { + + assert { + condition = aws_iam_role_policy.organizations_role_policy.name == "organizations-tf-state-management-policy" + error_message = "The name of the organizations role policy is incorrect. Expected 'organizations-tf-state-management-policy' but got '${aws_iam_role_policy.organizations_role_policy.name}'." + } + assert { + condition = aws_iam_role_policy.organizations_role_policy.role == aws_iam_role.organizations_role.id + error_message = "The role of the organizations role policy is incorrect. Expected '${aws_iam_role.organizations_role.id}' but got '${aws_iam_role_policy.organizations_role_policy.role}'." + } + assert { + condition = jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Sid"] == "StateBucketFullAccess" + error_message = "The organizations role policy statement sid is incorrect. Expected 'StateBucketFullAccess' but got '${jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Sid"]}'." + } + assert { + condition = jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Action"][0] == "s3:PutObject" + error_message = "The organizations role policy statement action is incorrect. Expected 's3:PutObject' but got '${jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Action"][0]}'." + } + assert { + condition = jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Action"][1] == "s3:GetObject" + error_message = "The organizations role policy statement action is incorrect. Expected 's3:GetObject' but got '${jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Action"][1]}'." + } + assert { + condition = jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Action"][2] == "s3:ListBucket" + error_message = "The organizations role policy statement action is incorrect. Expected 's3:ListBucket' but got '${jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Action"][2]}'." + } + assert { + condition = jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Effect"] == "Allow" + error_message = "The organizations role policy statement effect is incorrect. Expected 'Allow' but got '${jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Effect"]}'." + } + assert { + condition = jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Resource"][0] == aws_s3_bucket.state_bucket.arn + error_message = "The organizations role policy statement resource is incorrect. Expected '${aws_s3_bucket.state_bucket.arn}' but got '${jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Resource"][0]}'." + } + assert { + condition = jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Resource"][1] == "${aws_s3_bucket.state_bucket.arn}/*" + error_message = "The organizations role policy statement resource is incorrect. Expected '${aws_s3_bucket.state_bucket.arn}/*' but got '${jsondecode(aws_iam_role_policy.organizations_role_policy.policy)["Statement"][0]["Resource"][1]}'." + } +} diff --git a/modules/github-aws-oidc/outputs.tftest.hcl b/modules/github-aws-oidc/outputs.tftest.hcl new file mode 100644 index 0000000..4e92813 --- /dev/null +++ b/modules/github-aws-oidc/outputs.tftest.hcl @@ -0,0 +1,45 @@ +mock_provider "github" {} +mock_provider "aws" {} + +override_resource { + target = aws_s3_bucket.state_bucket + values = { + region = "us-west-2" + } +} +override_resource { + target = aws_iam_role.organizations_role + values = { + arn = "arn:aws:iam::123456789012:role/GhFoundationsOrganizationsAction" + } +} + +variables { + # required variables + github_foundations_organization_name = "github-foundations-org" + github_thumbprints = ["990F4193972F2BECF12DDEDA5237F9C952F20D9E"] + + # Variables for this test + bucket_name = "GithubFoundationStateBuckettyBucket" +} + +run "create_test" { + command = apply + + assert { + condition = output.s3_bucket_name == var.bucket_name + error_message = "The name of the state bucket is incorrect. Expected '${var.bucket_name}' but got '${output.s3_bucket_name}'." + } + assert { + condition = output.s3_bucket_region == "us-west-2" + error_message = "The region of the state bucket is incorrect. Expected 'us-west-2' but got '${output.s3_bucket_region}'." + } + assert { + condition = output.dynamodb_table_name == "TFLockIds" + error_message = "The name of the dynamodb table is incorrect. Expected 'TFLockIds' but got '${output.dynamodb_table_name}'." + } + assert { + condition = output.organizations_runner_role == "arn:aws:iam::123456789012:role/GhFoundationsOrganizationsAction" + error_message = "The ARN of the role is incorrect. Expected 'arn:aws:iam::123456789012:role/GhFoundationsOrganizationsAction' but got '${output.organizations_runner_role}'." + } +} diff --git a/modules/github-aws-oidc/resource_group.tftest.hcl b/modules/github-aws-oidc/resource_group.tftest.hcl new file mode 100644 index 0000000..e113aab --- /dev/null +++ b/modules/github-aws-oidc/resource_group.tftest.hcl @@ -0,0 +1,34 @@ +mock_provider "github" {} +mock_provider "aws" {} + +variables { + # required variables + github_foundations_organization_name = "github-foundations-org" + github_thumbprints = ["990F4193972F2BECF12DDEDA5237F9C952F20D9E"] +} + +run "github_foundations_rg_test" { + command = apply + + assert { + condition = aws_resourcegroups_group.github_foundations_rg.name == "GithubFoundationResources" + error_message = "The name of the resource group is incorrect. Expected 'GithubFoundationResources' but got '${aws_resourcegroups_group.github_foundations_rg.name}'." + } + assert { + condition = aws_resourcegroups_group.github_foundations_rg.resource_query[0].query == "{\"ResourceTypeFilters\":[\"AWS::AllSupported\"],\"TagFilters\":[{\"Key\":\"Purpose\",\"Values\":[\"Github Foundations\"]}]}" + error_message = "The resource query of the resource group is incorrect. Expected '{\"ResourceTypeFilters\":[\"AWS::AllSupported\"],\"TagFilters\":[{\"Key\":\"Purpose\",\"Values\":[\"Github Foundations\"]}]}' but got '${aws_resourcegroups_group.github_foundations_rg.resource_query[0].query}'." + } +} + +run "github_foundations_rg_test_rg_name" { + variables { + rg_name = "ghf-set-by-test-rg" + } + + command = apply + + assert { + condition = aws_resourcegroups_group.github_foundations_rg.name == "ghf-set-by-test-rg" + error_message = "The name of the resource group is incorrect. Expected 'ghf-set-by-test-rg' but got '${aws_resourcegroups_group.github_foundations_rg.name}'." + } +} diff --git a/modules/github-aws-oidc/storage.tftest.hcl b/modules/github-aws-oidc/storage.tftest.hcl new file mode 100644 index 0000000..24ae94b --- /dev/null +++ b/modules/github-aws-oidc/storage.tftest.hcl @@ -0,0 +1,122 @@ +mock_provider "github" {} +mock_provider "aws" {} + +variables { + # required variables + github_foundations_organization_name = "github-foundations-org" + + # Variables for this test + github_thumbprints = ["990F4193972F2BECF12DDEDA5237F9C952F20D9E", "990F4193972F2BECF12DDEDA5237F9C952F20D9F"] +} + +run "encryption_key_test" { + command = apply + + assert { + condition = aws_kms_key.encryption_key.description == "This key is used to encrypt state bucket objects" + error_message = "The description of the encryption key is incorrect. Expected 'This key is used to encrypt state bucket objects' but got '${aws_kms_key.encryption_key.description}'." + } + assert { + condition = aws_kms_key.encryption_key.deletion_window_in_days == 10 + error_message = "The deletion window in days of the encryption key is incorrect. Expected 10 but got ${aws_kms_key.encryption_key.deletion_window_in_days}." + } + assert { + condition = aws_kms_key.encryption_key.enable_key_rotation == true + error_message = "The enable key rotation of the encryption key is incorrect. Expected true but got ${aws_kms_key.encryption_key.enable_key_rotation}." + } + assert { + condition = aws_kms_key.encryption_key.tags.Purpose == "Github Foundations" + error_message = "The tags of the encryption key are incorrect. Expected 'Github Foundations' but got '${aws_kms_key.encryption_key.tags.Purpose}'." + } +} + +run "state_bucket_test" { + assert { + condition = aws_s3_bucket.state_bucket.bucket == "GithubFoundationState" + error_message = "The name of the state bucket is incorrect. Expected 'GithubFoundationState' but got '${aws_s3_bucket.state_bucket.bucket}'." + } + assert { + condition = aws_s3_bucket.state_bucket.tags.Purpose == "Github Foundations" + error_message = "The tags of the state bucket are incorrect. Expected 'Github Foundations' but got '${aws_s3_bucket.state_bucket.tags.Purpose}'." + } +} + +run "state_bucket_versioning_test" { + assert { + condition = aws_s3_bucket_versioning.state_bucket_versioning.bucket == aws_s3_bucket.state_bucket.id + error_message = "The state bucket versioning configuration is incorrect. Expected '${aws_s3_bucket.state_bucket.id}' but got '${aws_s3_bucket_versioning.state_bucket_versioning.bucket}'." + } + assert { + condition = aws_s3_bucket_versioning.state_bucket_versioning.versioning_configuration[0].status == "Enabled" + error_message = "The state bucket versioning status is incorrect. Expected 'Enabled' but got '${aws_s3_bucket_versioning.state_bucket_versioning.versioning_configuration[0].status}'." + } +} + +run "state_bucket_encryption_test" { + assert { + condition = aws_s3_bucket_server_side_encryption_configuration.state_bucket_encryption.bucket == aws_s3_bucket.state_bucket.id + error_message = "The state bucket encryption configuration is incorrect. Expected '${aws_s3_bucket.state_bucket.id}' but got '${aws_s3_bucket_server_side_encryption_configuration.state_bucket_encryption.bucket}'." + } + assert { + condition = aws_s3_bucket_server_side_encryption_configuration.state_bucket_encryption.rule != null + error_message = "The state bucket encryption rule is incorrect. Expected 1 but got null." + } +} + +run "state_bucket_access_test" { + assert { + condition = aws_s3_bucket_public_access_block.state_bucket_access.bucket == aws_s3_bucket.state_bucket.id + error_message = "The state bucket access configuration is incorrect. Expected '${aws_s3_bucket.state_bucket.id}' but got '${aws_s3_bucket_public_access_block.state_bucket_access.bucket}'." + } + assert { + condition = aws_s3_bucket_public_access_block.state_bucket_access.block_public_acls == true + error_message = "The state bucket access block public acls is incorrect. Expected true but got '${aws_s3_bucket_public_access_block.state_bucket_access.block_public_acls}'." + } + assert { + condition = aws_s3_bucket_public_access_block.state_bucket_access.block_public_policy == true + error_message = "The state bucket access block public policy is incorrect. Expected true but got '${aws_s3_bucket_public_access_block.state_bucket_access.block_public_policy}'." + } + assert { + condition = aws_s3_bucket_public_access_block.state_bucket_access.ignore_public_acls == true + error_message = "The state bucket access ignore public acls is incorrect. Expected true but got '${aws_s3_bucket_public_access_block.state_bucket_access.ignore_public_acls}'." + } + assert { + condition = aws_s3_bucket_public_access_block.state_bucket_access.restrict_public_buckets == true + error_message = "The state bucket access restrict public buckets is incorrect. Expected true but got '${aws_s3_bucket_public_access_block.state_bucket_access.restrict_public_buckets}'." + } +} + +run "state_lock_table_test" { + assert { + condition = aws_dynamodb_table.state_lock_table.name == "TFLockIds" + error_message = "The name of the state lock table is incorrect. Expected 'TFLockIds' but got '${aws_dynamodb_table.state_lock_table.name}'." + } + assert { + condition = aws_dynamodb_table.state_lock_table.read_capacity == 20 + error_message = "The read capacity of the state lock table is incorrect. Expected 20 but got '${aws_dynamodb_table.state_lock_table.read_capacity}'." + } + assert { + condition = aws_dynamodb_table.state_lock_table.write_capacity == 20 + error_message = "The write capacity of the state lock table is incorrect. Expected 20 but got '${aws_dynamodb_table.state_lock_table.write_capacity}'." + } + assert { + condition = aws_dynamodb_table.state_lock_table.billing_mode == "PROVISIONED" + error_message = "The billing mode of the state lock table is incorrect. Expected 'PROVISIONED' but got '${aws_dynamodb_table.state_lock_table.billing_mode}'." + } + assert { + condition = aws_dynamodb_table.state_lock_table.hash_key == "LockID" + error_message = "The hash key of the state lock table is incorrect. Expected 'LockID' but got '${aws_dynamodb_table.state_lock_table.hash_key}'." + } + assert { + condition = aws_dynamodb_table.state_lock_table.point_in_time_recovery[0].enabled == true + error_message = "The point in time recovery of the state lock table is incorrect. Expected true but got '${aws_dynamodb_table.state_lock_table.point_in_time_recovery[0].enabled}'." + } + assert { + condition = aws_dynamodb_table.state_lock_table.attribute != null + error_message = "The number of attributes of the state lock table is incorrect. Expected 1 but got null." + } + assert { + condition = aws_dynamodb_table.state_lock_table.tags.Purpose == "Github Foundations" + error_message = "The tags of the state lock table are incorrect. Expected 'Github Foundations' but got '${aws_dynamodb_table.state_lock_table.tags.Purpose}'." + } +} diff --git a/modules/github-azure-oidc/README.md b/modules/github-azure-oidc/README.md index 12a3503..16ac08e 100644 --- a/modules/github-azure-oidc/README.md +++ b/modules/github-azure-oidc/README.md @@ -3,14 +3,14 @@ | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.6 | -| [azurerm](#requirement\_azurerm) | >=3.0.0 | +| [azurerm](#requirement\_azurerm) | >=4.7.0 | | [random](#requirement\_random) | >= 3.6 | ## Providers | Name | Version | |------|---------| -| [azurerm](#provider\_azurerm) | >=3.0.0 | +| [azurerm](#provider\_azurerm) | >=4.7.0 | ## Modules @@ -55,7 +55,7 @@ No modules. | [sa\_tier](#input\_sa\_tier) | The tier of the storage account for github foundations. Valid options are Standard and Premium. Defaults to Standard. | `string` | `"Standard"` | no | | [tf\_state\_container](#input\_tf\_state\_container) | The name of the container to store the terraform state file(s) in. | `string` | `"tfstate"` | no | | [tf\_state\_container\_anonymous\_access\_level](#input\_tf\_state\_container\_anonymous\_access\_level) | The anonymous access level of the container to store the terraform state file(s) in. | `string` | `"private"` | no | -| [tf\_state\_container\_default\_encryption\_scope](#input\_tf\_state\_container\_default\_encryption\_scope) | The default encryption scope of the container to store the terraform state file(s) in. |
object({
name = string
source = string
key_vault_key_id = optional(string)
}) | {
"name": "",
"source": "",
"storage_account_id": ""
} | no |
+| [tf\_state\_container\_default\_encryption\_scope](#input\_tf\_state\_container\_default\_encryption\_scope) | The default encryption scope of the container to store the terraform state file(s) in. | object({
name = string
source = string
key_vault_key_id = optional(string)
}) | {
"name": "",
"source": "",
"storage_account_id": ""
} | no |
| [tf\_state\_container\_encryption\_scope\_override\_enabled](#input\_tf\_state\_container\_encryption\_scope\_override\_enabled) | Whether or not the encryption scope override is enabled for the container to store the terraform state file(s) in. Defaults to false | `bool` | `false` | no |
## Outputs
diff --git a/modules/github-azure-oidc/oidc.tftest.hcl b/modules/github-azure-oidc/oidc.tftest.hcl
new file mode 100644
index 0000000..aec7552
--- /dev/null
+++ b/modules/github-azure-oidc/oidc.tftest.hcl
@@ -0,0 +1,223 @@
+mock_provider "github" {}
+mock_provider "azurerm" {
+ # We need to mock the azurerm provider to avoid creating real resources
+ override_resource {
+ target = azurerm_user_assigned_identity.organization_identity
+ values = {
+ id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ghf-organizations-identity"
+ }
+ }
+ override_resource {
+ target = azurerm_user_assigned_identity.bootstrap_identity
+ values = {
+ id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ghf-bootstrap-identity"
+ }
+ }
+ override_resource {
+ target = azurerm_storage_account.github_foundations_sa
+ values = {
+ name = "ghfoundations"
+ id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.Storage/storageAccounts/ghfoundations"
+ }
+ }
+}
+
+override_data {
+ target = data.azurerm_key_vault.key_vault[0]
+ values = {
+ id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/kvrg/providers/Microsoft.KeyVault/vaults/awesome-possum"
+ }
+}
+
+# local overrides
+override_resource {
+ target = azurerm_storage_container.github_foundations_tf_state_container[0]
+ values = {
+ resource_manager_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.Storage/storageAccounts/ghfoundations/blobServices/default/containers/ghf-state"
+ }
+}
+
+variables {
+ # required variables
+ github_foundations_organization_name = "github-foundations"
+ rg_name = "github-foundations"
+ rg_location = "eastus"
+ sa_name = "ghfoundations"
+ drift_detection_branch_name = "drift-test-branch"
+ kv_resource_group = "kvrg"
+
+ # variables for this test
+ tf_state_container = "ghf-state"
+ kv_name = "awesome-possum"
+
+}
+
+run "bootstrap_identity_test" {
+ command = apply
+
+ assert {
+ condition = azurerm_user_assigned_identity.bootstrap_identity.location == var.rg_location
+ error_message = "The location of the bootstrap identity is incorrect. Expected: ${var.rg_location}, Got: ${azurerm_user_assigned_identity.bootstrap_identity.location}"
+ }
+ assert {
+ condition = azurerm_user_assigned_identity.bootstrap_identity.resource_group_name == var.rg_name
+ error_message = "The resource group name of the bootstrap identity is incorrect. Expected: ${var.rg_name}, Got: ${azurerm_user_assigned_identity.bootstrap_identity.resource_group_name}"
+ }
+ assert {
+ condition = azurerm_user_assigned_identity.bootstrap_identity.name == "${var.bootstrap_repo_name}-identity"
+ error_message = "The name of the bootstrap identity is incorrect. Expected: ${var.bootstrap_repo_name}-identity, Got: ${azurerm_user_assigned_identity.bootstrap_identity.name}"
+ }
+}
+
+run "bootstrap_role_assignment" {
+ assert {
+ condition = azurerm_role_assignment.bootstrap_role_assignment["storage-account-ghfoundations-contributor"].scope == azurerm_storage_account.github_foundations_sa.id
+ error_message = "The scope of the bootstrap role assignment is incorrect. Expected: ${azurerm_storage_account.github_foundations_sa.id}, Got: ${azurerm_role_assignment.bootstrap_role_assignment["storage-account-ghfoundations-contributor"].scope}"
+ }
+ assert {
+ condition = azurerm_role_assignment.bootstrap_role_assignment["storage-account-ghfoundations-contributor"].role_definition_name == "Storage Account Contributor"
+ error_message = "The role definition name of the bootstrap role assignment is incorrect. Expected: Storage Account Contributor, Got: ${azurerm_role_assignment.bootstrap_role_assignment["storage-account-ghfoundations-contributor"].role_definition_name}"
+ }
+}
+
+run "organization_identity_test" {
+ assert {
+ condition = azurerm_user_assigned_identity.organization_identity.location == var.rg_location
+ error_message = "The location of the organization identity is incorrect. Expected: ${var.rg_location}, Got: ${azurerm_user_assigned_identity.organization_identity.location}"
+ }
+ assert {
+ condition = azurerm_user_assigned_identity.organization_identity.resource_group_name == var.rg_name
+ error_message = "The resource group name of the organization identity is incorrect. Expected: ${var.rg_name}, Got: ${azurerm_user_assigned_identity.organization_identity.resource_group_name}"
+ }
+ assert {
+ condition = azurerm_user_assigned_identity.organization_identity.name == "${var.organizations_repo_name}-identity"
+ error_message = "The name of the organization identity is incorrect. Expected: ${var.organizations_repo_name}-identity, Got: ${azurerm_user_assigned_identity.organization_identity.name}"
+ }
+}
+
+run "organization_role_assignment" {
+ assert {
+ condition = azurerm_role_assignment.organization_role_assignment["keyvault-${data.azurerm_key_vault.key_vault[0].name}-secret-read"].scope == data.azurerm_key_vault.key_vault[0].id
+ error_message = "The scope of the organization role assignment is incorrect. Expected: ${data.azurerm_key_vault.key_vault[0].id}, Got: ${azurerm_role_assignment.organization_role_assignment["keyvault-${data.azurerm_key_vault.key_vault[0].name}-secret-read"].scope}"
+ }
+ assert {
+ condition = azurerm_role_assignment.organization_role_assignment["keyvault-${data.azurerm_key_vault.key_vault[0].name}-secret-read"].role_definition_name == "Key Vault Secrets User"
+ error_message = "The role definition name of the organization role assignment is incorrect. Expected: Key Vault Secrets User, Got: ${azurerm_role_assignment.organization_role_assignment["keyvault-${data.azurerm_key_vault.key_vault[0].name}-secret-read"].role_definition_name}"
+ }
+ assert {
+ condition = azurerm_role_assignment.organization_role_assignment["keyvault-${data.azurerm_key_vault.key_vault[0].name}-vault-read"].role_definition_name == "Key Vault Reader"
+ error_message = "The role definition name of the organization role assignment is incorrect. Expected: Key Vault Reader, Got: ${azurerm_role_assignment.organization_role_assignment["keyvault-${data.azurerm_key_vault.key_vault[0].name}-vault-read"].role_definition_name}"
+ }
+ assert {
+ condition = azurerm_role_assignment.organization_role_assignment["keyvault-${data.azurerm_key_vault.key_vault[0].name}-vault-read"].scope == data.azurerm_key_vault.key_vault[0].id
+ error_message = "The scope of the organization role assignment is incorrect. Expected: ${data.azurerm_key_vault.key_vault[0].id}, Got: ${azurerm_role_assignment.organization_role_assignment["keyvault-${data.azurerm_key_vault.key_vault[0].name}-vault-read"].scope}"
+ }
+}
+
+run "bootstrap_pull_request_credentials_test" {
+ assert {
+ condition = azurerm_federated_identity_credential.bootstrap_pull_request_credentials.name == "${var.github_foundations_organization_name}-${var.bootstrap_repo_name}-pr-credentials"
+ error_message = "The name of the bootstrap pull request credentials is incorrect. Expected: ${var.github_foundations_organization_name}-${var.bootstrap_repo_name}-pr-credentials, Got: ${azurerm_federated_identity_credential.bootstrap_pull_request_credentials.name}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.bootstrap_pull_request_credentials.resource_group_name == var.rg_name
+ error_message = "The resource group name of the bootstrap pull request credentials is incorrect. Expected: ${var.rg_name}, Got: ${azurerm_federated_identity_credential.bootstrap_pull_request_credentials.resource_group_name}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.bootstrap_pull_request_credentials.audience[0] == local.default_audience_name
+ error_message = "The audience of the bootstrap pull request credentials is incorrect. Expected: ${local.default_audience_name}, Got: ${azurerm_federated_identity_credential.bootstrap_pull_request_credentials.audience[0]}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.bootstrap_pull_request_credentials.issuer == local.github_issuer_url
+ error_message = "The issuer of the bootstrap pull request credentials is incorrect. Expected: ${local.github_issuer_url}, Got: ${azurerm_federated_identity_credential.bootstrap_pull_request_credentials.issuer}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.bootstrap_pull_request_credentials.parent_id == azurerm_user_assigned_identity.bootstrap_identity.id
+ error_message = "The parent id of the bootstrap pull request credentials is incorrect. Expected: ${azurerm_user_assigned_identity.bootstrap_identity.id}, Got: ${azurerm_federated_identity_credential.bootstrap_pull_request_credentials.parent_id}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.bootstrap_pull_request_credentials.subject == "repo:${var.github_foundations_organization_name}/${var.bootstrap_repo_name}:pull_request"
+ error_message = "The subject of the bootstrap pull request credentials is incorrect. Expected: repo:${var.github_foundations_organization_name}/${var.bootstrap_repo_name}:pull_request, Got: ${azurerm_federated_identity_credential.bootstrap_pull_request_credentials.subject}"
+ }
+}
+
+run "bootstrap_drift_credentials_test" {
+ assert {
+ condition = azurerm_federated_identity_credential.bootstrap_drift_credentials.name == "${var.github_foundations_organization_name}-${var.bootstrap_repo_name}-drift-credentials"
+ error_message = "The name of the bootstrap drift credentials is incorrect. Expected: ${var.github_foundations_organization_name}-${var.bootstrap_repo_name}-drift-credentials, Got: ${azurerm_federated_identity_credential.bootstrap_drift_credentials.name}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.bootstrap_drift_credentials.resource_group_name == var.rg_name
+ error_message = "The resource group name of the bootstrap drift credentials is incorrect. Expected: ${var.rg_name}, Got: ${azurerm_federated_identity_credential.bootstrap_drift_credentials.resource_group_name}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.bootstrap_drift_credentials.audience[0] == local.default_audience_name
+ error_message = "The audience of the bootstrap drift credentials is incorrect. Expected: ${local.default_audience_name}, Got: ${azurerm_federated_identity_credential.bootstrap_drift_credentials.audience[0]}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.bootstrap_drift_credentials.issuer == local.github_issuer_url
+ error_message = "The issuer of the bootstrap drift credentials is incorrect. Expected: ${local.github_issuer_url}, Got: ${azurerm_federated_identity_credential.bootstrap_drift_credentials.issuer}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.bootstrap_drift_credentials.parent_id == azurerm_user_assigned_identity.bootstrap_identity.id
+ error_message = "The parent id of the bootstrap drift credentials is incorrect. Expected: ${azurerm_user_assigned_identity.bootstrap_identity.id}, Got: ${azurerm_federated_identity_credential.bootstrap_drift_credentials.parent_id}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.bootstrap_drift_credentials.subject == "repo:${var.github_foundations_organization_name}/${var.bootstrap_repo_name}:ref:refs/heads/${var.drift_detection_branch_name}"
+ error_message = "The subject of the bootstrap drift credentials is incorrect. Expected: repo:${var.github_foundations_organization_name}/${var.bootstrap_repo_name}:ref:refs/heads/${var.drift_detection_branch_name}, Got: ${azurerm_federated_identity_credential.bootstrap_drift_credentials.subject}"
+ }
+}
+
+run "organization_pull_request_credentials_test" {
+ assert {
+ condition = azurerm_federated_identity_credential.organization_pull_request_credentials.name == "${var.github_foundations_organization_name}-${var.organizations_repo_name}-pr-credentials"
+ error_message = "The name of the organization pull request credentials is incorrect. Expected: ${var.github_foundations_organization_name}-${var.organizations_repo_name}-pr-credentials, Got: ${azurerm_federated_identity_credential.organization_pull_request_credentials.name}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.organization_pull_request_credentials.resource_group_name == var.rg_name
+ error_message = "The resource group name of the organization pull request credentials is incorrect. Expected: ${var.rg_name}, Got: ${azurerm_federated_identity_credential.organization_pull_request_credentials.resource_group_name}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.organization_pull_request_credentials.audience[0] == local.default_audience_name
+ error_message = "The audience of the organization pull request credentials is incorrect. Expected: ${local.default_audience_name}, Got: ${azurerm_federated_identity_credential.organization_pull_request_credentials.audience[0]}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.organization_pull_request_credentials.issuer == local.github_issuer_url
+ error_message = "The issuer of the organization pull request credentials is incorrect. Expected: ${local.github_issuer_url}, Got: ${azurerm_federated_identity_credential.organization_pull_request_credentials.issuer}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.organization_pull_request_credentials.parent_id == azurerm_user_assigned_identity.organization_identity.id
+ error_message = "The parent id of the organization pull request credentials is incorrect. Expected: ${azurerm_user_assigned_identity.organization_identity.id}, Got: ${azurerm_federated_identity_credential.organization_pull_request_credentials.parent_id}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.organization_pull_request_credentials.subject == "repo:${var.github_foundations_organization_name}/${var.organizations_repo_name}:pull_request"
+ error_message = "The subject of the organization pull request credentials is incorrect. Expected: repo:${var.github_foundations_organization_name}/${var.organizations_repo_name}:pull_request, Got: ${azurerm_federated_identity_credential.organization_pull_request_credentials.subject}"
+ }
+}
+
+run "organization_drift_credentials_test" {
+ assert {
+ condition = azurerm_federated_identity_credential.organization_drift_credentials.name == "${var.github_foundations_organization_name}-${var.organizations_repo_name}-drift-credentials"
+ error_message = "The name of the organization drift credentials is incorrect. Expected: ${var.github_foundations_organization_name}-${var.organizations_repo_name}-drift-credentials, Got: ${azurerm_federated_identity_credential.organization_drift_credentials.name}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.organization_drift_credentials.resource_group_name == var.rg_name
+ error_message = "The resource group name of the organization drift credentials is incorrect. Expected: ${var.rg_name}, Got: ${azurerm_federated_identity_credential.organization_drift_credentials.resource_group_name}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.organization_drift_credentials.audience[0] == local.default_audience_name
+ error_message = "The audience of the organization drift credentials is incorrect. Expected: ${local.default_audience_name}, Got: ${azurerm_federated_identity_credential.organization_drift_credentials.audience[0]}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.organization_drift_credentials.issuer == local.github_issuer_url
+ error_message = "The issuer of the organization drift credentials is incorrect. Expected: ${local.github_issuer_url}, Got: ${azurerm_federated_identity_credential.organization_drift_credentials.issuer}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.organization_drift_credentials.parent_id == azurerm_user_assigned_identity.organization_identity.id
+ error_message = "The parent id of the organization drift credentials is incorrect. Expected: ${azurerm_user_assigned_identity.organization_identity.id}, Got: ${azurerm_federated_identity_credential.organization_drift_credentials.parent_id}"
+ }
+ assert {
+ condition = azurerm_federated_identity_credential.organization_drift_credentials.subject == "repo:${var.github_foundations_organization_name}/${var.organizations_repo_name}:ref:refs/heads/${var.drift_detection_branch_name}"
+ error_message = "The subject of the organization drift credentials is incorrect. Expected: repo:${var.github_foundations_organization_name}/${var.organizations_repo_name}:ref:refs/heads/${var.drift_detection_branch_name}, Got: ${azurerm_federated_identity_credential.organization_drift_credentials.subject}"
+ }
+}
diff --git a/modules/github-azure-oidc/outputs.tftest.hcl b/modules/github-azure-oidc/outputs.tftest.hcl
new file mode 100644
index 0000000..244668b
--- /dev/null
+++ b/modules/github-azure-oidc/outputs.tftest.hcl
@@ -0,0 +1,102 @@
+mock_provider "github" {}
+mock_provider "azurerm" {
+ # We need to mock the azurerm provider to avoid creating real resources
+ override_resource {
+ target = azurerm_user_assigned_identity.organization_identity
+ values = {
+ id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ghf-organizations-identity"
+ client_id = "00000000-0000-0000-0000-000000000000"
+ }
+ }
+ override_resource {
+ target = azurerm_user_assigned_identity.bootstrap_identity
+ values = {
+ id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ghf-bootstrap-identity"
+ client_id = "11111111-1111-1111-1111-111111111111"
+ }
+ }
+ override_resource {
+ target = azurerm_storage_account.github_foundations_sa
+ values = {
+ name = "ghfoundations"
+ id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.Storage/storageAccounts/ghfoundations"
+ }
+ }
+}
+
+override_data {
+ target = data.azurerm_key_vault.key_vault[0]
+ values = {
+ id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/kvrg/providers/Microsoft.KeyVault/vaults/awesome-possum"
+ }
+}
+override_data {
+ target = data.azurerm_client_config.current
+ values = {
+ tenant_id = "33333333-3333-3333-3333-333333333333"
+ subscription_id = "44444444-4444-4444-4444-444444444444"
+ }
+}
+override_resource {
+ target = azurerm_storage_container.github_foundations_tf_state_container[0]
+ values = {
+ resource_manager_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.Storage/storageAccounts/ghfoundations/blobServices/default/containers/ghf-state"
+ }
+}
+override_resource {
+ target = azurerm_storage_container.github_foundations_tf_state_encrypted_container[0]
+ values = {
+ resource_manager_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.Storage/storageAccounts/ghfoundations/blobServices/default/containers/ghf-state"
+ }
+}
+
+variables {
+ # required variables
+ github_foundations_organization_name = "github-foundations"
+ rg_name = "github-foundations"
+ rg_location = "eastus"
+ sa_name = "ghfoundationssa"
+ drift_detection_branch_name = "drift-test-branch"
+ kv_resource_group = "kvrg"
+
+ # variables for this test
+ tf_state_container = "ghf-state-container"
+ kv_name = "awesome-possum"
+}
+
+run "create_test" {
+ command = apply
+
+ assert {
+ condition = output.resource_group == var.rg_name
+ error_message = "Resource group name does not match. Expected: ${var.rg_name}, got: ${output.resource_group}"
+ }
+ assert {
+ condition = output.bootstrap_client_id == "11111111-1111-1111-1111-111111111111"
+ error_message = "Bootstrap repository client id does not match. Expected: 11111111-1111-1111-1111-111111111111, got: ${output.bootstrap_client_id}"
+ }
+ assert {
+ condition = output.organization_client_id == "00000000-0000-0000-0000-000000000000"
+ error_message = "Organizations repository client id does not match. Expected: 00000000-0000-0000-0000-000000000000, got: ${output.organization_client_id}"
+ }
+ assert {
+ condition = output.tenant_id == "33333333-3333-3333-3333-333333333333"
+ error_message = "Tenant id does not match. Expected: 33333333-3333-3333-3333-333333333333, got: ${output.tenant_id}"
+ }
+ assert {
+ condition = output.subscription_id == "44444444-4444-4444-4444-444444444444"
+ error_message = "Subscription id does not match. Expected: 44444444-4444-4444-4444-444444444444, got: ${output.subscription_id}"
+ }
+ assert {
+ condition = output.sa_name == var.sa_name
+ error_message = "Storage account name does not match. Expected: ${var.sa_name}, got: ${output.sa_name}"
+ }
+ assert {
+ condition = output.container_name == var.tf_state_container
+ error_message = "Container name does not match. Expected: ${var.tf_state_container}, got: ${output.container_name}"
+ }
+ assert {
+ condition = output.key_vault_id == "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/kvrg/providers/Microsoft.KeyVault/vaults/awesome-possum"
+ error_message = "Key vault id does not match. Expected: /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/kvrg/providers/Microsoft.KeyVault/vaults/awesome-possum, got: ${output.key_vault_id}"
+ }
+}
diff --git a/modules/github-azure-oidc/storage.tf b/modules/github-azure-oidc/storage.tf
index 109394d..dd9c3fb 100644
--- a/modules/github-azure-oidc/storage.tf
+++ b/modules/github-azure-oidc/storage.tf
@@ -23,14 +23,14 @@ resource "azurerm_storage_encryption_scope" "encryption_scope" {
resource "azurerm_storage_container" "github_foundations_tf_state_container" {
count = local.default_encryption_scope == null ? 1 : 0
name = var.tf_state_container
- storage_account_name = azurerm_storage_account.github_foundations_sa.name
+ storage_account_id = azurerm_storage_account.github_foundations_sa.id
container_access_type = var.tf_state_container_anonymous_access_level
}
resource "azurerm_storage_container" "github_foundations_tf_state_encrypted_container" {
count = local.default_encryption_scope != null ? 1 : 0
name = var.tf_state_container
- storage_account_name = azurerm_storage_account.github_foundations_sa.name
+ storage_account_id = azurerm_storage_account.github_foundations_sa.id
container_access_type = var.tf_state_container_anonymous_access_level
default_encryption_scope = local.default_encryption_scope
encryption_scope_override_enabled = var.tf_state_container_encryption_scope_override_enabled
diff --git a/modules/github-azure-oidc/storage.tftest.hcl b/modules/github-azure-oidc/storage.tftest.hcl
new file mode 100644
index 0000000..10ec23a
--- /dev/null
+++ b/modules/github-azure-oidc/storage.tftest.hcl
@@ -0,0 +1,174 @@
+mock_provider "github" {}
+mock_provider "azurerm" {
+ # We need to mock the azurerm provider to avoid creating real resources
+ override_resource {
+ target = azurerm_user_assigned_identity.organization_identity
+ values = {
+ id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ghf-organizations-identity"
+ }
+ }
+ override_resource {
+ target = azurerm_user_assigned_identity.bootstrap_identity
+ values = {
+ id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.ManagedIdentity/userAssignedIdentities/ghf-bootstrap-identity"
+ }
+ }
+ override_resource {
+ target = azurerm_storage_account.github_foundations_sa
+ values = {
+ name = "ghfoundations"
+ id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.Storage/storageAccounts/ghfoundations"
+ }
+ }
+}
+
+override_data {
+ target = data.azurerm_key_vault.key_vault[0]
+ values = {
+ id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/kvrg/providers/Microsoft.KeyVault/vaults/awesome-possum"
+ }
+}
+override_resource {
+ target = azurerm_storage_container.github_foundations_tf_state_container[0]
+ values = {
+ resource_manager_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.Storage/storageAccounts/ghfoundations/blobServices/default/containers/ghf-state"
+ }
+}
+override_resource {
+ target = azurerm_storage_container.github_foundations_tf_state_encrypted_container[0]
+ values = {
+ resource_manager_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/github-foundations/providers/Microsoft.Storage/storageAccounts/ghfoundations/blobServices/default/containers/ghf-state"
+ }
+}
+
+variables {
+ # required variables
+ github_foundations_organization_name = "github-foundations"
+ rg_name = "github-foundations"
+ rg_location = "eastus"
+ sa_name = "ghfoundations"
+ drift_detection_branch_name = "drift-test-branch"
+ kv_resource_group = "kvrg"
+
+ # variables for this test
+ sa_tier = "Premium"
+ sa_replication_type = "LRS"
+ tf_state_container = "ghf-state"
+ tf_state_container_anonymous_access_level = "container"
+ tf_state_container_encryption_scope_override_enabled = true
+ tf_state_container_default_encryption_scope = {
+ name = "defaultEncryptionScope"
+ source = "Microsoft.Storage"
+ key_vault_key_id = "https://myvault.vault.azure.net/secrets/mysecret"
+ }
+ kv_name = "awesome-possum"
+ organizations_repo_name = "ghf-organizations"
+ bootstrap_repo_name = "ghf-bootstrap"
+}
+
+run "github_foundations_sa_test" {
+ command = apply
+
+ assert {
+ condition = azurerm_storage_account.github_foundations_sa.resource_group_name == var.rg_name
+ error_message = "The storage account resource group name is incorrect. Expected ${var.rg_name} but got ${azurerm_storage_account.github_foundations_sa.resource_group_name}."
+ }
+ assert {
+ condition = azurerm_storage_account.github_foundations_sa.location == var.rg_location
+ error_message = "The storage account location is incorrect. Expected ${var.rg_location} but got ${azurerm_storage_account.github_foundations_sa.location}."
+ }
+ assert {
+ condition = azurerm_storage_account.github_foundations_sa.account_tier == var.sa_tier
+ error_message = "The storage account tier is incorrect. Expected ${var.sa_tier} but got ${azurerm_storage_account.github_foundations_sa.account_tier}."
+ }
+ assert {
+ condition = azurerm_storage_account.github_foundations_sa.account_replication_type == var.sa_replication_type
+ error_message = "The storage account replication type is incorrect. Expected ${var.sa_replication_type} but got ${azurerm_storage_account.github_foundations_sa.account_replication_type}."
+ }
+ assert {
+ condition = azurerm_storage_account.github_foundations_sa.min_tls_version == "TLS1_2"
+ error_message = "The storage account min tls version is incorrect. Expected TLS1_2 but got ${azurerm_storage_account.github_foundations_sa.min_tls_version}."
+ }
+}
+
+run "encryption_scope_test" {
+
+ assert {
+ condition = azurerm_storage_container.github_foundations_tf_state_encrypted_container[0] != null
+ error_message = "The custom encryption scope is not set."
+ }
+ assert {
+ condition = length(azurerm_storage_container.github_foundations_tf_state_container) == 0
+ error_message = "The default encryption scope should not be set."
+ }
+ assert {
+ condition = azurerm_storage_encryption_scope.encryption_scope[0].name == var.tf_state_container_default_encryption_scope.name
+ error_message = "The encryption scope name is incorrect. Expected ${var.tf_state_container_default_encryption_scope.name} but got ${azurerm_storage_encryption_scope.encryption_scope[0].name}."
+ }
+ assert {
+ condition = azurerm_storage_encryption_scope.encryption_scope[0].storage_account_id == azurerm_storage_account.github_foundations_sa.id
+ error_message = "The encryption scope storage account id is incorrect. Expected ${azurerm_storage_account.github_foundations_sa.id} but got ${azurerm_storage_encryption_scope.encryption_scope[0].storage_account_id}."
+ }
+ assert {
+ condition = azurerm_storage_encryption_scope.encryption_scope[0].source == var.tf_state_container_default_encryption_scope.source
+ error_message = "The encryption scope source is incorrect. Expected ${var.tf_state_container_default_encryption_scope.source} but got ${azurerm_storage_encryption_scope.encryption_scope[0].source}."
+ }
+ assert {
+ condition = azurerm_storage_encryption_scope.encryption_scope[0].key_vault_key_id == var.tf_state_container_default_encryption_scope.key_vault_key_id
+ error_message = "The encryption scope key vault key id is incorrect. Expected ${var.tf_state_container_default_encryption_scope.key_vault_key_id} but got ${azurerm_storage_encryption_scope.encryption_scope[0].key_vault_key_id}."
+ }
+ # Check the state container configuration
+ assert {
+ condition = azurerm_storage_container.github_foundations_tf_state_encrypted_container[0].name == var.tf_state_container
+ error_message = "The custom encryption scope container name is incorrect. Expected ${var.tf_state_container} but got ${azurerm_storage_container.github_foundations_tf_state_encrypted_container[0].name}."
+ }
+ assert {
+ condition = azurerm_storage_container.github_foundations_tf_state_encrypted_container[0].container_access_type == var.tf_state_container_anonymous_access_level
+ error_message = "The custom encryption scope container access type is incorrect. Expected ${var.tf_state_container_anonymous_access_level} but got ${azurerm_storage_container.github_foundations_tf_state_encrypted_container[0].container_access_type}."
+ }
+}
+
+run "default_encryption_scope_test" {
+ variables {
+ tf_state_container_default_encryption_scope = {
+ name = ""
+ source = ""
+ key_vault_key_id = ""
+ }
+ }
+
+ command = apply
+ plan_options {
+ refresh = true
+ }
+ assert {
+ condition = azurerm_storage_container.github_foundations_tf_state_container[0] != null
+ error_message = "The default encryption scope is not set."
+ }
+ assert {
+ condition = length(azurerm_storage_container.github_foundations_tf_state_encrypted_container) == 0
+ error_message = "The custom encryption scope should not be set."
+ }
+ assert {
+ condition = length(azurerm_storage_encryption_scope.encryption_scope) == 0
+ error_message = "The encryption scope should not be set."
+ }
+ assert {
+ condition = azurerm_storage_container.github_foundations_tf_state_container[0].name == var.tf_state_container
+ error_message = "The default encryption scope container name is incorrect. Expected ${var.tf_state_container} but got ${azurerm_storage_container.github_foundations_tf_state_container[0].name}."
+ }
+ assert {
+ condition = azurerm_storage_container.github_foundations_tf_state_container[0].container_access_type == var.tf_state_container_anonymous_access_level
+ error_message = "The default encryption scope container access type is incorrect. Expected ${var.tf_state_container_anonymous_access_level} but got ${azurerm_storage_container.github_foundations_tf_state_container[0].container_access_type}."
+ }
+}
+
+
+# run "default_encryption_scope_test" {
+# override_data {
+# target = data.azurerm_key_vault.key_vault[0]
+# values = {
+# id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/kvrg/providers/Microsoft.KeyVault/vaults/awesome-possum"
+# }
+# }
+# }
diff --git a/modules/github-azure-oidc/versions.tf b/modules/github-azure-oidc/versions.tf
index 9a66b59..3add54a 100644
--- a/modules/github-azure-oidc/versions.tf
+++ b/modules/github-azure-oidc/versions.tf
@@ -3,7 +3,7 @@ terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
- version = ">=3.0.0" #tftest
+ version = ">=4.7.0" #tftest
}
random = {
source = "hashicorp/random"
diff --git a/modules/github-foundations/README.md b/modules/github-foundations/README.md
index 65c6099..d484143 100644
--- a/modules/github-foundations/README.md
+++ b/modules/github-foundations/README.md
@@ -61,7 +61,7 @@
| [account\_type](#input\_account\_type) | The type of GitHub account being used. Should be one of either `Personal`, `Organization`, or `Enterprise`. | `string` | n/a | yes |
| [bootstrap\_repository\_name](#input\_bootstrap\_repository\_name) | The name of the bootstrap repository. | `string` | `"bootstrap"` | no |
| [foundation\_devs\_team\_name](#input\_foundation\_devs\_team\_name) | The name of the foundation developers team. | `string` | `"foundation-devs"` | no |
-| [oidc\_configuration](#input\_oidc\_configuration) | n/a | object({
gcp = optional(object({
workload_identity_provider_name_secret_name = optional(string)
workload_identity_provider_name = string
organization_workload_identity_sa_secret_name = optional(string)
organization_workload_identity_sa = string
gcp_secret_manager_project_id_variable_name = optional(string)
gcp_secret_manager_project_id = string
gcp_tf_state_bucket_project_id_variable_name = optional(string)
gcp_tf_state_bucket_project_id = string
bucket_name_variable_name = optional(string)
bucket_name = string
bucket_location_variable_name = optional(string)
bucket_location = string
}))
azure = optional(object({
bootstrap_client_id_variable_name = optional(string)
bootstrap_client_id = string
organization_client_id_variable_name = optional(string)
organization_client_id = string
tenant_id_variable_name = optional(string)
tenant_id = string
subscription_id_variable_name = optional(string)
subscription_id = string
resource_group_name_variable_name = optional(string)
resource_group_name = string
storage_account_name_variable_name = optional(string)
storage_account_name = string
container_name_variable_name = optional(string)
container_name = string
key_vault_id_variable_name = optional(string)
key_vault_id = string
}))
aws = optional(object({
s3_bucket_variable_name = optional(string)
s3_bucket = string
region_variable_name = optional(string)
region = string
organizations_role_variable_name = optional(string)
organizations_role = string
dynamodb_table_variable_name = optional(string)
dynamodb_table = string
}))
custom = optional(object({
organization_secrets = map(string)
organization_variables = map(string)
repository_secrets = map(map(string))
repository_variables = map(map(string))
}))
}) | n/a | yes |
+| [oidc\_configuration](#input\_oidc\_configuration) | n/a | object({
gcp = optional(object({
workload_identity_provider_name_secret_name = optional(string)
workload_identity_provider_name = string
organization_workload_identity_sa_secret_name = optional(string)
organization_workload_identity_sa = string
gcp_secret_manager_project_id_variable_name = optional(string)
gcp_secret_manager_project_id = string
gcp_tf_state_bucket_project_id_variable_name = optional(string)
gcp_tf_state_bucket_project_id = string
bucket_name_variable_name = optional(string)
bucket_name = string
bucket_location_variable_name = optional(string)
bucket_location = string
}))
azure = optional(object({
bootstrap_client_id_variable_name = optional(string)
bootstrap_client_id = string
organization_client_id_variable_name = optional(string)
organization_client_id = string
tenant_id_variable_name = optional(string)
tenant_id = string
subscription_id_variable_name = optional(string)
subscription_id = string
resource_group_name_variable_name = optional(string)
resource_group_name = string
storage_account_name_variable_name = optional(string)
storage_account_name = string
container_name_variable_name = optional(string)
container_name = string
key_vault_id_variable_name = optional(string)
key_vault_id = string
}))
aws = optional(object({
s3_bucket_variable_name = optional(string)
s3_bucket = string
region_variable_name = optional(string)
region = string
organizations_role_variable_name = optional(string)
organizations_role = string
dynamodb_table_variable_name = optional(string)
dynamodb_table = string
}))
custom = optional(object({
organization_secrets = map(string)
organization_variables = map(string)
repository_secrets = map(map(string))
repository_variables = map(map(string))
}))
}) | n/a | yes |
| [organizations\_repository\_name](#input\_organizations\_repository\_name) | The name of the organizations repository. | `string` | `"organizations"` | no |
| [readme\_path](#input\_readme\_path) | Local Path to the README file in your current codebase. Pushed to the github foundation repository. | `string` | `""` | no |
diff --git a/modules/github-foundations/aws-oidc-variables.tftest.hcl b/modules/github-foundations/aws-oidc-variables.tftest.hcl
new file mode 100644
index 0000000..eaf910d
--- /dev/null
+++ b/modules/github-foundations/aws-oidc-variables.tftest.hcl
@@ -0,0 +1,107 @@
+mock_provider "github" {}
+
+variables {
+ # required variables
+ account_type = "Organization"
+ oidc_configuration = {
+ aws = {
+ s3_bucket_variable_name = "aws_s3_bucket_variable_name"
+ s3_bucket = "my-s3-bucket"
+
+ region_variable_name = "aws_region_variable_name"
+ region = "us-west-2"
+
+ organizations_role_variable_name = "aws_organizations_role_variable_name"
+ organizations_role = "my-organizations-role"
+
+ dynamodb_table_variable_name = "aws_dynamodb_table_variable_name"
+ dynamodb_table = "my-dynamodb-table"
+
+
+ custom = {
+ organization_secrets = {
+ secret1 = "secret1",
+ secret2 = "secret2"
+ }
+ organization_variables = {
+ variable1 = "variable1",
+ variable2 = "variable2"
+ }
+ repository_secrets = {
+ repo1 = {
+ secret1 = "secret1",
+ secret2 = "secret2"
+ },
+ repo2 = {
+ secret1 = "secret1",
+ secret2 = "secret2"
+ }
+ }
+ repository_variables = {
+ repo1 = {
+ variable1 = "variable1",
+ variable2 = "variable2"
+ },
+ repo2 = {
+ variable1 = "variable1",
+ variable2 = "variable2"
+ }
+ }
+ }
+ }
+ }
+}
+
+run "s3_bucket_test" {
+ command = apply
+
+ assert {
+ condition = github_actions_organization_variable.s3_bucket[0].variable_name == var.oidc_configuration.aws.s3_bucket_variable_name
+ error_message = "s3_bucket variable name does not match. Expected: ${var.oidc_configuration.aws.s3_bucket_variable_name}, got: ${github_actions_organization_variable.s3_bucket[0].variable_name}"
+ }
+ assert {
+ condition = github_actions_organization_variable.s3_bucket[0].value == var.oidc_configuration.aws.s3_bucket
+ error_message = "s3_bucket value does not match. Expected: ${var.oidc_configuration.aws.s3_bucket}, got: ${github_actions_organization_variable.s3_bucket[0].value}"
+ }
+ assert {
+ condition = github_actions_organization_variable.s3_bucket[0].visibility == "selected"
+ error_message = "s3_bucket visibility does not match. Expected: selected, got: ${github_actions_organization_variable.s3_bucket[0].visibility}"
+ }
+}
+
+run "region_test" {
+ assert {
+ condition = github_actions_organization_variable.region[0].variable_name == var.oidc_configuration.aws.region_variable_name
+ error_message = "region variable name does not match. Expected: ${var.oidc_configuration.aws.region_variable_name}, got: ${github_actions_organization_variable.region[0].variable_name}"
+ }
+ assert {
+ condition = github_actions_organization_variable.region[0].value == var.oidc_configuration.aws.region
+ error_message = "region value does not match. Expected: ${var.oidc_configuration.aws.region}, got: ${github_actions_organization_variable.region[0].value}"
+ }
+ assert {
+ condition = github_actions_organization_variable.region[0].visibility == "selected"
+ error_message = "region visibility does not match. Expected: selected, got: ${github_actions_organization_variable.region[0].visibility}"
+ }
+}
+
+run "organizations_iam_role_test" {
+ assert {
+ condition = github_actions_secret.organizations_iam_role[0].secret_name == var.oidc_configuration.aws.organizations_role_variable_name
+ error_message = "organizations_iam_role secret name does not match. Expected: ${var.oidc_configuration.aws.organizations_role_variable_name}, got: ${github_actions_secret.organizations_iam_role[0].secret_name}"
+ }
+ assert {
+ condition = github_actions_secret.organizations_iam_role[0].plaintext_value == var.oidc_configuration.aws.organizations_role
+ error_message = "organizations_iam_role plaintext value does not match. Expected: ${var.oidc_configuration.aws.organizations_role}, got: ${nonsensitive(github_actions_secret.organizations_iam_role[0].plaintext_value)}"
+ }
+}
+
+run "dynamodb_table_name_test" {
+ assert {
+ condition = github_actions_variable.dynamodb_table_name[0].variable_name == var.oidc_configuration.aws.dynamodb_table_variable_name
+ error_message = "dynamodb_table_name variable name does not match. Expected: ${var.oidc_configuration.aws.dynamodb_table_variable_name}, got: ${github_actions_variable.dynamodb_table_name[0].variable_name}"
+ }
+ assert {
+ condition = github_actions_variable.dynamodb_table_name[0].value == var.oidc_configuration.aws.dynamodb_table
+ error_message = "dynamodb_table_name value does not match. Expected: ${var.oidc_configuration.aws.dynamodb_table}, got: ${github_actions_variable.dynamodb_table_name[0].value}"
+ }
+}
diff --git a/modules/github-foundations/azure-oidc-variables.tftest.hcl b/modules/github-foundations/azure-oidc-variables.tftest.hcl
new file mode 100644
index 0000000..f458aeb
--- /dev/null
+++ b/modules/github-foundations/azure-oidc-variables.tftest.hcl
@@ -0,0 +1,114 @@
+mock_provider "github" {}
+
+variables {
+ # required variables
+ account_type = "Organization"
+ oidc_configuration = {
+ azure = {
+ bootstrap_client_id_variable_name = "bootstrap_client_id_variable_name"
+ bootstrap_client_id = "bootstrap_client_id"
+
+ organization_client_id_variable_name = "organization_client_id_variable_name"
+ organization_client_id = "organization_client_id"
+
+ tenant_id_variable_name = "tenant_id_variable_name"
+ tenant_id = "tenant_id"
+
+ subscription_id_variable_name = "subscription_id_variable_name"
+ subscription_id = "subscription_id"
+
+ resource_group_name_variable_name = "resource_group_name_variable_name"
+ resource_group_name = "resource_group_name"
+
+ storage_account_name_variable_name = "storage_account_name_variable_name"
+ storage_account_name = "storage_account_name"
+
+ container_name_variable_name = "container_name_variable_name"
+ container_name = "container_name"
+
+ key_vault_id_variable_name = "key_vault_id_variable_name"
+ key_vault_id = "key_vault_id"
+ }
+ }
+}
+
+run "organization_managed_identity_client_id_test" {
+ command = apply
+
+ assert {
+ condition = github_actions_secret.organization_managed_identity_client_id[0].repository == "organizations"
+ error_message = "The org-managed client ID secret repository is incorrect. Expected: 'organizations', got: ${github_actions_secret.organization_managed_identity_client_id[0].repository}"
+ }
+ assert {
+ condition = github_actions_secret.organization_managed_identity_client_id[0].secret_name == var.oidc_configuration.azure.organization_client_id_variable_name
+ error_message = "The org-managed client ID secret name is incorrect. Expected: '${var.oidc_configuration.azure.organization_client_id_variable_name}', got: ${github_actions_secret.organization_managed_identity_client_id[0].secret_name}"
+ }
+ assert {
+ condition = github_actions_secret.organization_managed_identity_client_id[0].plaintext_value == var.oidc_configuration.azure.organization_client_id
+ error_message = "The org-managed client ID secret value is incorrect. Expected: '${var.oidc_configuration.azure.organization_client_id}', got: ${nonsensitive(github_actions_secret.organization_managed_identity_client_id[0].plaintext_value)}"
+ }
+}
+
+run "bootstrap_managed_identity_client_id_test" {
+
+ assert {
+ condition = github_actions_secret.bootstrap_managed_identity_client_id[0].repository == "bootstrap"
+ error_message = "The bootstrap-managed client ID secret repository is incorrect. Expected: 'bootstrap' got: ${github_actions_secret.bootstrap_managed_identity_client_id[0].repository}"
+ }
+ assert {
+ condition = github_actions_secret.bootstrap_managed_identity_client_id[0].secret_name == var.oidc_configuration.azure.bootstrap_client_id_variable_name
+ error_message = "The bootstrap-managed client ID secret name is incorrect. Expected: '${var.oidc_configuration.azure.bootstrap_client_id_variable_name}', got: ${github_actions_secret.bootstrap_managed_identity_client_id[0].secret_name}"
+ }
+ assert {
+ condition = github_actions_secret.bootstrap_managed_identity_client_id[0].plaintext_value == var.oidc_configuration.azure.bootstrap_client_id
+ error_message = "The bootstrap-managed client ID secret value is incorrect. Expected: '${var.oidc_configuration.azure.bootstrap_client_id}', got: ${nonsensitive(github_actions_secret.bootstrap_managed_identity_client_id[0].plaintext_value)}"
+ }
+}
+
+run "tenant_id_test" {
+
+ assert {
+ condition = github_actions_organization_secret.tenant_id[0].secret_name == var.oidc_configuration.azure.tenant_id_variable_name
+ error_message = "The tenant ID secret name is incorrect. Expected: '${var.oidc_configuration.azure.tenant_id_variable_name}', got: ${github_actions_organization_secret.tenant_id[0].secret_name}"
+ }
+ assert {
+ condition = github_actions_organization_secret.tenant_id[0].plaintext_value == var.oidc_configuration.azure.tenant_id
+ error_message = "The tenant ID secret value is incorrect. Expected: '${var.oidc_configuration.azure.tenant_id}', got: ${nonsensitive(github_actions_organization_secret.tenant_id[0].plaintext_value)}"
+ }
+}
+
+run "subscription_id_test" {
+
+ assert {
+ condition = github_actions_organization_variable.subscription_id[0].variable_name == var.oidc_configuration.azure.subscription_id_variable_name
+ error_message = "The subscription ID variable name is incorrect. Expected: '${var.oidc_configuration.azure.subscription_id_variable_name}', got: ${github_actions_organization_variable.subscription_id[0].variable_name}"
+ }
+ assert {
+ condition = github_actions_organization_variable.subscription_id[0].value == var.oidc_configuration.azure.subscription_id
+ error_message = "The subscription ID variable value is incorrect. Expected: '${var.oidc_configuration.azure.subscription_id}', got: ${nonsensitive(github_actions_organization_variable.subscription_id[0].value)}"
+ }
+}
+
+run "resource_group_name_test" {
+
+ assert {
+ condition = github_actions_organization_variable.resource_group_name[0].variable_name == var.oidc_configuration.azure.resource_group_name_variable_name
+ error_message = "The resource group name variable name is incorrect. Expected: '${var.oidc_configuration.azure.resource_group_name_variable_name}', got: ${github_actions_organization_variable.resource_group_name[0].variable_name}"
+ }
+ assert {
+ condition = github_actions_organization_variable.resource_group_name[0].value == var.oidc_configuration.azure.resource_group_name
+ error_message = "The resource group name variable value is incorrect. Expected: '${var.oidc_configuration.azure.resource_group_name}', got: ${nonsensitive(github_actions_organization_variable.resource_group_name[0].value)}"
+ }
+}
+
+run "storage_account_name_test" {
+
+ assert {
+ condition = github_actions_organization_variable.storage_account_name[0].variable_name == var.oidc_configuration.azure.storage_account_name_variable_name
+ error_message = "The storage account name variable name is incorrect. Expected: '${var.oidc_configuration.azure.storage_account_name_variable_name}', got: ${github_actions_organization_variable.storage_account_name[0].variable_name}"
+ }
+ assert {
+ condition = github_actions_organization_variable.storage_account_name[0].value == var.oidc_configuration.azure.storage_account_name
+ error_message = "The storage account name variable value is incorrect. Expected: '${var.oidc_configuration.azure.storage_account_name}', got: ${nonsensitive(github_actions_organization_variable.storage_account_name[0].value)}"
+ }
+}
diff --git a/modules/github-foundations/custom-oidc-variables.tf b/modules/github-foundations/custom-oidc-variables.tf
index 26f219d..0e05e28 100644
--- a/modules/github-foundations/custom-oidc-variables.tf
+++ b/modules/github-foundations/custom-oidc-variables.tf
@@ -8,8 +8,8 @@ locals {
repository = repo
}
}
- ]
- ), [])
+ ]...
+ ), {})
expanded_list_of_repo_variables = try(merge(
[
@@ -20,8 +20,8 @@ locals {
repository = repo
}
}
- ]
- ), [])
+ ]...
+ ), {})
}
resource "github_actions_organization_secret" "custom_oidc_organization_secret" {
@@ -49,7 +49,7 @@ resource "github_actions_organization_variable" "custom_oidc_organization_variab
}
resource "github_actions_secret" "repository_secret" {
- for_each = toset(local.expanded_list_of_repo_secrets)
+ for_each = local.expanded_list_of_repo_secrets
repository = each.value.repository
secret_name = each.value.name
@@ -57,7 +57,7 @@ resource "github_actions_secret" "repository_secret" {
}
resource "github_actions_variable" "repository_variable" {
- for_each = toset(local.expanded_list_of_repo_variables)
+ for_each = local.expanded_list_of_repo_variables
repository = each.value.repository
variable_name = each.value.name
diff --git a/modules/github-foundations/custom-oidc-variables.tftest.hcl b/modules/github-foundations/custom-oidc-variables.tftest.hcl
new file mode 100644
index 0000000..bb7952c
--- /dev/null
+++ b/modules/github-foundations/custom-oidc-variables.tftest.hcl
@@ -0,0 +1,120 @@
+mock_provider "github" {}
+
+variables {
+ # required variables
+ account_type = "Organization"
+ oidc_configuration = {
+ gcp = {
+ workload_identity_provider_name_secret_name = "workload_identity_provider_name_secret_name"
+ workload_identity_provider_name = "workload_identity_provider_name"
+
+ organization_workload_identity_sa_secret_name = "organization_workload_identity_sa_secret_name"
+ organization_workload_identity_sa = "organization_workload_identity_sa"
+
+ gcp_secret_manager_project_id_variable_name = "gcp_secret_manager_project_id_variable_name"
+ gcp_secret_manager_project_id = "gcp_secret_manager_project_id"
+
+ gcp_tf_state_bucket_project_id_variable_name = "gcp_tf_state_bucket_project_id_variable_name"
+ gcp_tf_state_bucket_project_id = "gcp_tf_state_bucket_project_id"
+
+ bucket_name_variable_name = "bucket_name_variable_name"
+ bucket_name = "bucket_name"
+
+ bucket_location_variable_name = "bucket_location_variable_name"
+ bucket_location = "bucket_location"
+ }
+
+ custom = {
+ organization_secrets = {
+ secret1 = "c2VjcmV0MQ==", # "secret1" base64 encoded
+ secret2 = "c2VjcmV0Mg==" # "secret2" base64
+ }
+ organization_variables = {
+ variable1 = "variable1"
+ variable2 = "variable2"
+ }
+ repository_secrets = {
+ repo1 = {
+ secret1 = "c2VjcmV0MQ==", # "secret1" base64 encoded
+ secret2 = "c2VjcmV0Mg==" # "secret2" base64
+ },
+ repo2 = {
+ secret1 = "c2VjcmV0MQ==", # "secret1" base64 encoded
+ secret2 = "c2VjcmV0Mg==" # "secret2" base64
+ }
+ }
+ repository_variables = {
+ repo1 = {
+ variable1 = "variable1",
+ variable2 = "variable2"
+ },
+ repo2 = {
+ variable1 = "variable1",
+ variable2 = "variable2"
+ }
+ }
+ }
+ }
+}
+
+run "custom_oidc_organization_secret_test" {
+ command = apply
+
+ assert {
+ condition = length(github_actions_organization_secret.custom_oidc_organization_secret) == 2
+ error_message = "The number of organization secrets is incorrect. Expected 2 but got ${length(github_actions_organization_secret.custom_oidc_organization_secret)}."
+ }
+}
+
+run "custom_oidc_organization_variable_test" {
+ assert {
+ condition = length(github_actions_organization_variable.custom_oidc_organization_variable) == 2
+ error_message = "The number of organization variables is incorrect. Expected 2 but got ${length(github_actions_organization_variable.custom_oidc_organization_variable)}."
+ }
+}
+
+run "repository_secret_test" {
+ assert {
+ condition = length(github_actions_secret.repository_secret) == 4
+ error_message = "The number of repository secrets is incorrect. Expected 4 but got ${length(github_actions_secret.repository_secret)}."
+ }
+ assert {
+ condition = github_actions_secret.repository_secret["repo1_secret1"].encrypted_value == var.oidc_configuration.custom.repository_secrets.repo1.secret1
+ error_message = "Repository secret repo1_secret1 is incorrect. Expected '${var.oidc_configuration.custom.repository_secrets.repo1.secret1}' but got ${nonsensitive(github_actions_secret.repository_secret["repo1_secret1"].encrypted_value)}."
+ }
+ assert {
+ condition = github_actions_secret.repository_secret["repo1_secret2"].encrypted_value == var.oidc_configuration.custom.repository_secrets.repo1.secret2
+ error_message = "Repository secret repo1_secret2 is incorrect. Expected '${var.oidc_configuration.custom.repository_secrets.repo1.secret2}' but got ${nonsensitive(github_actions_secret.repository_secret["repo1_secret2"].encrypted_value)}."
+ }
+ assert {
+ condition = github_actions_secret.repository_secret["repo2_secret1"].encrypted_value == var.oidc_configuration.custom.repository_secrets.repo2.secret1
+ error_message = "Repository secret repo2_secret1 is incorrect. Expected '${var.oidc_configuration.custom.repository_secrets.repo2.secret1}' but got ${nonsensitive(github_actions_secret.repository_secret["repo2_secret1"].encrypted_value)}."
+ }
+ assert {
+ condition = github_actions_secret.repository_secret["repo2_secret2"].encrypted_value == var.oidc_configuration.custom.repository_secrets.repo2.secret2
+ error_message = "Repository secret repo2_secret2 is incorrect. Expected '${var.oidc_configuration.custom.repository_secrets.repo2.secret2}' but got ${nonsensitive(github_actions_secret.repository_secret["repo2_secret2"].encrypted_value)}."
+ }
+}
+
+run "repository_variable_test" {
+ assert {
+ condition = length(github_actions_variable.repository_variable) == 4
+ error_message = "The number of repository variables is incorrect. Expected 4 but got ${length(github_actions_variable.repository_variable)}."
+ }
+ assert {
+ condition = github_actions_variable.repository_variable["repo1_variable1"].value == var.oidc_configuration.custom.repository_variables.repo1.variable1
+ error_message = "Repository variable repo1_variable1 is incorrect. Expected '${var.oidc_configuration.custom.repository_variables.repo1.variable1}' but got ${github_actions_variable.repository_variable["repo1_variable1"].value}."
+ }
+ assert {
+ condition = github_actions_variable.repository_variable["repo1_variable2"].value == var.oidc_configuration.custom.repository_variables.repo1.variable2
+ error_message = "Repository variable repo1_variable2 is incorrect. Expected '${var.oidc_configuration.custom.repository_variables.repo1.variable2}' but got ${github_actions_variable.repository_variable["repo1_variable2"].value}."
+ }
+ assert {
+ condition = github_actions_variable.repository_variable["repo2_variable1"].value == var.oidc_configuration.custom.repository_variables.repo2.variable1
+ error_message = "Repository variable repo2_variable1 is incorrect. Expected '${var.oidc_configuration.custom.repository_variables.repo2.variable1}' but got ${github_actions_variable.repository_variable["repo2_variable1"].value}."
+ }
+ assert {
+ condition = github_actions_variable.repository_variable["repo2_variable2"].value == var.oidc_configuration.custom.repository_variables.repo2.variable2
+ error_message = "Repository variable repo2_variable2 is incorrect. Expected '${var.oidc_configuration.custom.repository_variables.repo2.variable2}' but got ${github_actions_variable.repository_variable["repo2_variable2"].value}."
+ }
+}
diff --git a/modules/github-foundations/gcp-oidc-variables.tftest.hcl b/modules/github-foundations/gcp-oidc-variables.tftest.hcl
new file mode 100644
index 0000000..d566c6f
--- /dev/null
+++ b/modules/github-foundations/gcp-oidc-variables.tftest.hcl
@@ -0,0 +1,108 @@
+mock_provider "github" {}
+
+variables {
+ # required variables
+ account_type = "Organization"
+ oidc_configuration = {
+ gcp = {
+ workload_identity_provider_name_secret_name = "workload_identity_provider_name_secret_name"
+ workload_identity_provider_name = "workload_identity_provider_name"
+
+ organization_workload_identity_sa_secret_name = "organization_workload_identity_sa_secret_name"
+ organization_workload_identity_sa = "organization_workload_identity_sa"
+
+ gcp_secret_manager_project_id_variable_name = "gcp_secret_manager_project_id_variable_name"
+ gcp_secret_manager_project_id = "gcp_secret_manager_project_id"
+
+ gcp_tf_state_bucket_project_id_variable_name = "gcp_tf_state_bucket_project_id_variable_name"
+ gcp_tf_state_bucket_project_id = "gcp_tf_state_bucket_project_id"
+
+ bucket_name_variable_name = "bucket_name_variable_name"
+ bucket_name = "bucket_name"
+
+ bucket_location_variable_name = "bucket_location_variable_name"
+ bucket_location = "bucket_location"
+ }
+ }
+}
+
+run "organization_workload_identity_sa_test" {
+
+ command = apply
+
+ assert {
+ condition = github_actions_secret.organization_workload_identity_sa[0].repository == "organizations"
+ error_message = "The repository of the organization workload identity service account is incorrect. Expected 'organizations' but got '${github_actions_secret.organization_workload_identity_sa[0].repository}'."
+ }
+ assert {
+ condition = github_actions_secret.organization_workload_identity_sa[0].secret_name == var.oidc_configuration.gcp.organization_workload_identity_sa_secret_name
+ error_message = "The secret_name of the organization workload identity service account is incorrect. Expected '${var.oidc_configuration.gcp.organization_workload_identity_sa_secret_name}' but got '${github_actions_secret.organization_workload_identity_sa[0].secret_name}'."
+ }
+ assert {
+ condition = github_actions_secret.organization_workload_identity_sa[0].plaintext_value == var.oidc_configuration.gcp.organization_workload_identity_sa
+ error_message = "The plaintext_value of the organization workload identity service account is incorrect. Expected '${var.oidc_configuration.gcp.organization_workload_identity_sa}' but got '${nonsensitive(github_actions_secret.organization_workload_identity_sa[0].plaintext_value)}'."
+ }
+}
+
+run "gcp_secret_manager_project_id_test" {
+ assert {
+ condition = github_actions_variable.gcp_secret_manager_project_id[0].repository == "organizations"
+ error_message = "The repository of the GCP Secret Manager project ID is incorrect. Expected 'organizations' but got '${github_actions_variable.gcp_secret_manager_project_id[0].repository}'."
+ }
+ assert {
+ condition = github_actions_variable.gcp_secret_manager_project_id[0].variable_name == var.oidc_configuration.gcp.gcp_secret_manager_project_id_variable_name
+ error_message = "The variable_name of the GCP Secret Manager project ID is incorrect. Expected '${var.oidc_configuration.gcp.gcp_secret_manager_project_id_variable_name}' but got '${github_actions_variable.gcp_secret_manager_project_id[0].variable_name}'."
+ }
+ assert {
+ condition = github_actions_variable.gcp_secret_manager_project_id[0].value == var.oidc_configuration.gcp.gcp_secret_manager_project_id
+ error_message = "The value of the GCP Secret Manager project ID is incorrect. Expected '${var.oidc_configuration.gcp.gcp_secret_manager_project_id}' but got '${github_actions_variable.gcp_secret_manager_project_id[0].value}'."
+ }
+}
+
+run "workload_identity_provider_test" {
+ assert {
+ condition = github_actions_organization_secret.workload_identity_provider[0].secret_name == var.oidc_configuration.gcp.workload_identity_provider_name_secret_name
+ error_message = "The secret_name of the workload identity provider is incorrect. Expected '${var.oidc_configuration.gcp.workload_identity_provider_name_secret_name}' but got '${github_actions_organization_secret.workload_identity_provider[0].secret_name}'."
+ }
+ assert {
+ condition = github_actions_organization_secret.workload_identity_provider[0].plaintext_value == var.oidc_configuration.gcp.workload_identity_provider_name
+ error_message = "The plaintext_value of the workload identity provider is incorrect. Expected '${var.oidc_configuration.gcp.workload_identity_provider_name}' but got '${nonsensitive(github_actions_organization_secret.workload_identity_provider[0].plaintext_value)}'."
+ }
+}
+
+run "tf_state_bucket_project_id_test" {
+ assert {
+ condition = github_actions_organization_variable.tf_state_bucket_project_id[0].variable_name == var.oidc_configuration.gcp.gcp_tf_state_bucket_project_id_variable_name
+ error_message = "The variable_name of the TF state bucket project ID is incorrect. Expected '${var.oidc_configuration.gcp.gcp_tf_state_bucket_project_id_variable_name}' but got '${github_actions_organization_variable.tf_state_bucket_project_id[0].variable_name}'."
+ }
+ assert {
+ condition = github_actions_organization_variable.tf_state_bucket_project_id[0].value == var.oidc_configuration.gcp.gcp_tf_state_bucket_project_id
+ error_message = "The value of the TF state bucket project ID is incorrect. Expected '${var.oidc_configuration.gcp.gcp_tf_state_bucket_project_id}' but got '${github_actions_organization_variable.tf_state_bucket_project_id[0].value}'."
+ }
+}
+
+run "tf_state_bucket_name_test" {
+ assert {
+ condition = github_actions_organization_variable.tf_state_bucket_name[0].variable_name == var.oidc_configuration.gcp.bucket_name_variable_name
+ error_message = "The variable_name of the TF state bucket name is incorrect. Expected '${var.oidc_configuration.gcp.bucket_name_variable_name}' but got '${github_actions_organization_variable.tf_state_bucket_name[0].variable_name}'."
+ }
+ assert {
+ condition = github_actions_organization_variable.tf_state_bucket_name[0].value == var.oidc_configuration.gcp.bucket_name
+ error_message = "The value of the TF state bucket name is incorrect. Expected '${var.oidc_configuration.gcp.bucket_name}' but got '${github_actions_organization_variable.tf_state_bucket_name[0].value}'."
+ }
+}
+
+run "tf_state_bucket_location_test" {
+ assert {
+ condition = github_actions_organization_variable.tf_state_bucket_location[0].variable_name == var.oidc_configuration.gcp.bucket_location_variable_name
+ error_message = "The variable_name of the TF state bucket location is incorrect. Expected '${var.oidc_configuration.gcp.bucket_location_variable_name}' but got '${github_actions_organization_variable.tf_state_bucket_location[0].variable_name}'."
+ }
+ assert {
+ condition = github_actions_organization_variable.tf_state_bucket_location[0].value == var.oidc_configuration.gcp.bucket_location
+ error_message = "The value of the TF state bucket location is incorrect. Expected '${var.oidc_configuration.gcp.bucket_location}' but got '${github_actions_organization_variable.tf_state_bucket_location[0].value}'."
+ }
+ assert {
+ condition = github_actions_organization_variable.tf_state_bucket_location[0].visibility == "selected"
+ error_message = "The visibility of the TF state bucket location is incorrect. Expected 'selected' but got '${github_actions_organization_variable.tf_state_bucket_location[0].visibility}'."
+ }
+}
diff --git a/modules/github-foundations/repo_readme.tftest.hcl b/modules/github-foundations/repo_readme.tftest.hcl
new file mode 100644
index 0000000..f5911e0
--- /dev/null
+++ b/modules/github-foundations/repo_readme.tftest.hcl
@@ -0,0 +1,48 @@
+mock_provider "github" {}
+
+variables {
+ # required variables
+ account_type = "Organization"
+ oidc_configuration = {
+ gcp = {
+ workload_identity_provider_name_secret_name = "workload_identity_provider_name_secret_name"
+ workload_identity_provider_name = "workload_identity_provider_name"
+
+ organization_workload_identity_sa_secret_name = "organization_workload_identity_sa_secret_name"
+ organization_workload_identity_sa = "organization_workload_identity_sa"
+
+ gcp_secret_manager_project_id_variable_name = "gcp_secret_manager_project_id_variable_name"
+ gcp_secret_manager_project_id = "gcp_secret_manager_project_id"
+
+ gcp_tf_state_bucket_project_id_variable_name = "gcp_tf_state_bucket_project_id_variable_name"
+ gcp_tf_state_bucket_project_id = "gcp_tf_state_bucket_project_id"
+
+ bucket_name_variable_name = "bucket_name_variable_name"
+ bucket_name = "bucket_name"
+
+ bucket_location_variable_name = "bucket_location_variable_name"
+ bucket_location = "bucket_location"
+ }
+ }
+
+ # Variables for this test
+ readme_path = "../../README.md"
+}
+
+run "repo_readme_test" {
+
+ command = apply
+
+ assert {
+ condition = github_repository_file.main_readme[0].repository == "organizations"
+ error_message = "The repository of the main readme file is incorrect. Expected 'organizations' but got '${github_repository_file.main_readme[0].repository}'."
+ }
+ assert {
+ condition = github_repository_file.main_readme[0].file == "README.md"
+ error_message = "The file of the main readme file is incorrect. Expected 'README.md' but got '${github_repository_file.main_readme[0].file}'."
+ }
+ assert {
+ condition = startswith(github_repository_file.main_readme[0].content, "# github-foundations-modules\n")
+ error_message = "The content of the main readme file is incorrect."
+ }
+}
diff --git a/modules/github-foundations/repositories.tf b/modules/github-foundations/repositories.tf
index 7a3e95e..1c4bafc 100644
--- a/modules/github-foundations/repositories.tf
+++ b/modules/github-foundations/repositories.tf
@@ -73,7 +73,7 @@ resource "github_issue_labels" "drift_labels" {
}
label {
- color = "ededed"
name = "Drift"
+ color = "ededed"
}
}
diff --git a/modules/github-foundations/repositories.tftest.hcl b/modules/github-foundations/repositories.tftest.hcl
new file mode 100644
index 0000000..b0de1b9
--- /dev/null
+++ b/modules/github-foundations/repositories.tftest.hcl
@@ -0,0 +1,112 @@
+mock_provider "github" {}
+
+variables {
+ # required variables
+ account_type = "Organization"
+ oidc_configuration = {
+ gcp = {
+ workload_identity_provider_name_secret_name = "workload_identity_provider_name_secret_name"
+ workload_identity_provider_name = "workload_identity_provider_name"
+
+ organization_workload_identity_sa_secret_name = "organization_workload_identity_sa_secret_name"
+ organization_workload_identity_sa = "organization_workload_identity_sa"
+
+ gcp_secret_manager_project_id_variable_name = "gcp_secret_manager_project_id_variable_name"
+ gcp_secret_manager_project_id = "gcp_secret_manager_project_id"
+
+ gcp_tf_state_bucket_project_id_variable_name = "gcp_tf_state_bucket_project_id_variable_name"
+ gcp_tf_state_bucket_project_id = "gcp_tf_state_bucket_project_id"
+
+ bucket_name_variable_name = "bucket_name_variable_name"
+ bucket_name = "bucket_name"
+
+ bucket_location_variable_name = "bucket_location_variable_name"
+ bucket_location = "bucket_location"
+ }
+ }
+
+ # Variables for this test
+ bootstrap_repository_name = "bootstrap-repo"
+}
+
+run "bootstrap_repo_test" {
+
+ command = apply
+
+ assert {
+ condition = github_repository.bootstrap_repo.name == var.bootstrap_repository_name
+ error_message = "The name of the bootstrap repository is incorrect. Expected '${var.bootstrap_repository_name}' but got '${github_repository.bootstrap_repo.name}'."
+ }
+ assert {
+ condition = github_repository.bootstrap_repo.visibility == "private"
+ error_message = "The visibility of the bootstrap repository is incorrect. Expected 'private' but got '${github_repository.bootstrap_repo.visibility}'."
+ }
+ assert {
+ condition = github_repository.bootstrap_repo.auto_init == true
+ error_message = "The auto_init of the bootstrap repository is incorrect. Expected 'true' but got '${github_repository.bootstrap_repo.auto_init}'."
+ }
+ assert {
+ condition = github_repository.bootstrap_repo.delete_branch_on_merge == true
+ error_message = "The delete_branch_on_merge of the bootstrap repository is incorrect. Expected 'true' but got '${github_repository.bootstrap_repo.delete_branch_on_merge}'."
+ }
+ assert {
+ condition = github_repository.bootstrap_repo.vulnerability_alerts == true
+ error_message = "The vulnerability_alerts of the bootstrap repository is incorrect. Expected 'true' but got '${github_repository.bootstrap_repo.vulnerability_alerts}'."
+ }
+}
+
+run "bootstrap_repo_collaborators_test" {
+ assert {
+ condition = github_repository_collaborators.bootstrap_repo_collaborators.repository == github_repository.bootstrap_repo.name
+ error_message = "The repository of the bootstrap repository collaborators is incorrect. Expected '${github_repository.bootstrap_repo.name}' but got '${github_repository_collaborators.bootstrap_repo_collaborators.repository}'."
+ }
+ assert {
+ condition = github_repository_collaborators.bootstrap_repo_collaborators.team != null
+ error_message = "The permission of the bootstrap repository collaborators is incorrect. Got null."
+ }
+}
+
+run "organizations_repo_test" {
+ assert {
+ condition = github_repository.organizations_repo.name == var.organizations_repository_name
+ error_message = "The name of the organizations repository is incorrect. Expected '${var.organizations_repository_name}' but got '${github_repository.organizations_repo.name}'."
+ }
+ assert {
+ condition = github_repository.organizations_repo.visibility == "private"
+ error_message = "The visibility of the organizations repository is incorrect. Expected 'private' but got '${github_repository.organizations_repo.visibility}'."
+ }
+ assert {
+ condition = github_repository.organizations_repo.auto_init == true
+ error_message = "The auto_init of the organizations repository is incorrect. Expected 'true' but got '${github_repository.organizations_repo.auto_init}'."
+ }
+ assert {
+ condition = github_repository.organizations_repo.delete_branch_on_merge == true
+ error_message = "The delete_branch_on_merge of the organizations repository is incorrect. Expected 'true' but got '${github_repository.organizations_repo.delete_branch_on_merge}'."
+ }
+ assert {
+ condition = github_repository.organizations_repo.vulnerability_alerts == true
+ error_message = "The vulnerability_alerts of the organizations repository is incorrect. Expected 'true' but got '${github_repository.organizations_repo.vulnerability_alerts}'."
+ }
+ assert {
+ condition = github_repository.organizations_repo.has_issues == true
+ error_message = "The has_issues of the organizations repository is incorrect. Expected 'true' but got '${github_repository.organizations_repo.has_issues}'."
+ }
+}
+
+run "organization_repo_collaborators_test" {
+ assert {
+ condition = github_repository_collaborators.organization_repo_collaborators.repository == github_repository.organizations_repo.name
+ error_message = "The repository of the organizations repository collaborators is incorrect. Expected '${github_repository.organizations_repo.name}' but got '${github_repository_collaborators.organization_repo_collaborators.repository}'."
+ }
+ assert {
+ condition = github_repository_collaborators.organization_repo_collaborators.team != null
+ error_message = "The permission of the organizations repository collaborators is incorrect. Got null."
+ }
+}
+
+run "drift_labels_test" {
+ assert {
+ condition = length(github_issue_labels.drift_labels[0]) == 3
+ error_message = "The number of drift labels is incorrect. Expected '3' but got '${length(github_issue_labels.drift_labels[0])}'."
+ }
+}
diff --git a/modules/github-foundations/rulesets.tftest.hcl b/modules/github-foundations/rulesets.tftest.hcl
new file mode 100644
index 0000000..431f04f
--- /dev/null
+++ b/modules/github-foundations/rulesets.tftest.hcl
@@ -0,0 +1,36 @@
+mock_provider "github" {}
+
+variables {
+ # required variables
+ account_type = "Enterprise"
+ oidc_configuration = {
+ gcp = {
+ workload_identity_provider_name_secret_name = "workload_identity_provider_name_secret_name"
+ workload_identity_provider_name = "workload_identity_provider_name"
+
+ organization_workload_identity_sa_secret_name = "organization_workload_identity_sa_secret_name"
+ organization_workload_identity_sa = "organization_workload_identity_sa"
+
+ gcp_secret_manager_project_id_variable_name = "gcp_secret_manager_project_id_variable_name"
+ gcp_secret_manager_project_id = "gcp_secret_manager_project_id"
+
+ gcp_tf_state_bucket_project_id_variable_name = "gcp_tf_state_bucket_project_id_variable_name"
+ gcp_tf_state_bucket_project_id = "gcp_tf_state_bucket_project_id"
+
+ bucket_name_variable_name = "bucket_name_variable_name"
+ bucket_name = "bucket_name"
+
+ bucket_location_variable_name = "bucket_location_variable_name"
+ bucket_location = "bucket_location"
+ }
+ }
+}
+
+run "base_ruleset_module_test" {
+ command = apply
+
+ assert {
+ condition = length(module.base_ruleset) == 1
+ error_message = "The base ruleset module was not created."
+ }
+}
diff --git a/modules/github-foundations/teams.tftest.hcl b/modules/github-foundations/teams.tftest.hcl
new file mode 100644
index 0000000..e0841c0
--- /dev/null
+++ b/modules/github-foundations/teams.tftest.hcl
@@ -0,0 +1,40 @@
+mock_provider "github" {}
+
+variables {
+ # required variables
+ account_type = "Organization"
+ oidc_configuration = {
+ gcp = {
+ workload_identity_provider_name_secret_name = "workload_identity_provider_name_secret_name"
+ workload_identity_provider_name = "workload_identity_provider_name"
+
+ organization_workload_identity_sa_secret_name = "organization_workload_identity_sa_secret_name"
+ organization_workload_identity_sa = "organization_workload_identity_sa"
+
+ gcp_secret_manager_project_id_variable_name = "gcp_secret_manager_project_id_variable_name"
+ gcp_secret_manager_project_id = "gcp_secret_manager_project_id"
+
+ gcp_tf_state_bucket_project_id_variable_name = "gcp_tf_state_bucket_project_id_variable_name"
+ gcp_tf_state_bucket_project_id = "gcp_tf_state_bucket_project_id"
+
+ bucket_name_variable_name = "bucket_name_variable_name"
+ bucket_name = "bucket_name"
+
+ bucket_location_variable_name = "bucket_location_variable_name"
+ bucket_location = "bucket_location"
+ }
+ }
+
+ # Variables for this test
+ foundation_devs_team_name = "foundation-devs"
+}
+
+run "create_gcp_teams_test" {
+
+ command = apply
+
+ assert {
+ condition = github_team.foundation_devs.name == var.foundation_devs_team_name
+ error_message = "The name of the foundation developers team is incorrect. Expected '${var.foundation_devs_team_name}' but got '${github_team.foundation_devs.name}'."
+ }
+}
diff --git a/modules/github-gcloud-oidc/README.md b/modules/github-gcloud-oidc/README.md
index 94ec762..0fe4c83 100644
--- a/modules/github-gcloud-oidc/README.md
+++ b/modules/github-gcloud-oidc/README.md
@@ -3,22 +3,22 @@
| Name | Version |
|------|---------|
| [terraform](#requirement\_terraform) | >= 1.6 |
-| [google](#requirement\_google) | >= 3.77 |
-| [google-beta](#requirement\_google-beta) | >= 3.77 |
+| [google](#requirement\_google) | >= 6.12.0 |
+| [google-beta](#requirement\_google-beta) | >= 6.12.0 |
| [random](#requirement\_random) | >= 3.6 |
## Providers
| Name | Version |
|------|---------|
-| [google](#provider\_google) | >= 3.77 |
+| [google](#provider\_google) | >= 6.12.0 |
| [random](#provider\_random) | >= 3.6 |
## Modules
| Name | Source | Version |
|------|--------|---------|
-| [oidc](#module\_oidc) | terraform-google-modules/github-actions-runners/google//modules/gh-oidc | 3.1.2 |
+| [oidc](#module\_oidc) | terraform-google-modules/github-actions-runners/google//modules/gh-oidc | 4.0.0 |
## Resources
@@ -45,7 +45,7 @@
| [billing\_account](#input\_billing\_account) | Billing account id. | `string` | `null` | no |
| [bootstrap\_repo\_name](#input\_bootstrap\_repo\_name) | The name of the github foundations bootstrap repository. Defaults to `bootstrap` | `string` | `"bootstrap"` | no |
| [bucket\_name](#input\_bucket\_name) | Bucket name | `string` | n/a | yes |
-| [cors](#input\_cors) | CORS configuration for the bucket. Defaults to null. | object({
origin = optional(list(string))
method = optional(list(string))
response_header = optional(list(string))
max_age_seconds = optional(number)
}) | `null` | no |
+| [cors](#input\_cors) | CORS configuration for the bucket. Defaults to null. | object({
origin = optional(list(string))
method = optional(list(string))
response_header = optional(list(string))
max_age_seconds = optional(number)
}) | `null` | no |
| [custom\_placement\_config](#input\_custom\_placement\_config) | The bucket's custom location configuration, which specifies the individual regions that comprise a dual-region bucket. If the bucket is designated as REGIONAL or MULTI\_REGIONAL, the parameters are empty. | `list(string)` | `null` | no |
| [default\_event\_based\_hold](#input\_default\_event\_based\_hold) | Enable event based hold to new objects added to specific bucket, defaults to false. | `bool` | `null` | no |
| [descriptive\_name](#input\_descriptive\_name) | Name of the project name. Used for project name instead of `project_name` variable. | `string` | `null` | no |
@@ -56,9 +56,9 @@
| [github\_foundations\_organization\_name](#input\_github\_foundations\_organization\_name) | The name of the organization that the github foundation repos will be under. | `string` | n/a | yes |
| [id](#input\_id) | Folder ID in case you use folder\_create=false. | `string` | `null` | no |
| [labels](#input\_labels) | Resource labels. | `map(string)` | `{}` | no |
-| [lifecycle\_rules](#input\_lifecycle\_rules) | Bucket lifecycle rule. | map(object({
action = object({
type = string
storage_class = optional(string)
})
condition = object({
age = optional(number)
created_before = optional(string)
custom_time_before = optional(string)
days_since_custom_time = optional(number)
days_since_noncurrent_time = optional(number)
matches_prefix = optional(list(string))
matches_storage_class = optional(list(string)) # STANDARD, MULTI_REGIONAL, REGIONAL, NEARLINE, COLDLINE, ARCHIVE, DURABLE_REDUCED_AVAILABILITY
matches_suffix = optional(list(string))
noncurrent_time_before = optional(string)
num_newer_versions = optional(number)
with_state = optional(string) # "LIVE", "ARCHIVED", "ANY"
})
})) | `{}` | no |
+| [lifecycle\_rules](#input\_lifecycle\_rules) | Bucket lifecycle rule. | map(object({
action = object({
type = string
storage_class = optional(string)
})
condition = object({
age = optional(number)
created_before = optional(string)
custom_time_before = optional(string)
days_since_custom_time = optional(number)
days_since_noncurrent_time = optional(number)
matches_prefix = optional(list(string))
matches_storage_class = optional(list(string)) # STANDARD, MULTI_REGIONAL, REGIONAL, NEARLINE, COLDLINE, ARCHIVE, DURABLE_REDUCED_AVAILABILITY
matches_suffix = optional(list(string))
noncurrent_time_before = optional(string)
num_newer_versions = optional(number)
with_state = optional(string) # "LIVE", "ARCHIVED", "ANY"
})
})) | `{}` | no |
| [location](#input\_location) | Bucket location. | `string` | n/a | yes |
-| [logging\_config](#input\_logging\_config) | Bucket logging configuration. | object({
log_bucket = string
log_object_prefix = optional(string)
}) | `null` | no |
+| [logging\_config](#input\_logging\_config) | Bucket logging configuration. | object({
log_bucket = string
log_object_prefix = optional(string)
}) | `null` | no |
| [organizations\_repo\_name](#input\_organizations\_repo\_name) | The name of the github foundations organizations repository. Defaults to `organizations` | `string` | `"organizations"` | no |
| [parent](#input\_parent) | Parent in folders/folder\_id or organizations/org\_id format. | `string` | `null` | no |
| [prefix](#input\_prefix) | Optional prefix used to generate project id and name. | `string` | `null` | no |
@@ -66,14 +66,13 @@
| [project\_name](#input\_project\_name) | Project name and id suffix. | `string` | n/a | yes |
| [project\_parent](#input\_project\_parent) | Parent folder or organization in 'folders/folder\_id' or 'organizations/org\_id' format. | `string` | `null` | no |
| [requester\_pays](#input\_requester\_pays) | Enables Requester Pays on a storage bucket. | `bool` | `null` | no |
-| [retention\_policy](#input\_retention\_policy) | Bucket retention policy. | object({
retention_period = number
is_locked = optional(bool)
}) | `null` | no |
-| [service\_config](#input\_service\_config) | Configure service API activation. | object({
disable_on_destroy = bool
disable_dependent_services = bool
}) | {
"disable_dependent_services": false,
"disable_on_destroy": false
} | no |
+| [retention\_policy](#input\_retention\_policy) | Bucket retention policy. | object({
retention_period = number
is_locked = optional(bool)
}) | `null` | no |
+| [service\_config](#input\_service\_config) | Configure service API activation. | object({
disable_on_destroy = bool
disable_dependent_services = bool
}) | {
"disable_dependent_services": false,
"disable_on_destroy": false
} | no |
| [services](#input\_services) | Service APIs to enable. | `list(string)` | `[]` | no |
-| [skip\_delete](#input\_skip\_delete) | Allows the underlying resources to be destroyed without destroying the project itself. | `bool` | `false` | no |
| [storage\_class](#input\_storage\_class) | Bucket storage class. | `string` | `"STANDARD"` | no |
| [uniform\_bucket\_level\_access](#input\_uniform\_bucket\_level\_access) | Allow using object ACLs (false) or not (true, this is the recommended behavior) , defaults to true (which is the recommended practice, but not the behavior of storage API). | `bool` | `true` | no |
| [versioning](#input\_versioning) | Enable versioning, defaults to false. | `bool` | `false` | no |
-| [website](#input\_website) | Bucket website. | object({
main_page_suffix = optional(string)
not_found_page = optional(string)
}) | `null` | no |
+| [website](#input\_website) | Bucket website. | object({
main_page_suffix = optional(string)
not_found_page = optional(string)
}) | `null` | no |
## Outputs
diff --git a/modules/github-gcloud-oidc/folder.tftest.hcl b/modules/github-gcloud-oidc/folder.tftest.hcl
new file mode 100644
index 0000000..e473320
--- /dev/null
+++ b/modules/github-gcloud-oidc/folder.tftest.hcl
@@ -0,0 +1,41 @@
+mock_provider "github" {}
+mock_provider "google" {}
+mock_provider "google-beta" {}
+mock_provider "github-actions-runners" {}
+override_module {
+ target = module.oidc
+ outputs = {
+ gh-oidc = {
+ sa_name = "test-sa"
+ attribute = "attribute.repository/github-foundations/bootstrap"
+ }
+ }
+}
+
+variables {
+ # required variables
+ github_foundations_organization_name = "github-foundations"
+ parent = "organizations/1234567890"
+ project_parent = "organizations/1234567890"
+ project_name = "test-project"
+ folder_name = "test-folder"
+ bucket_name = "test-bucket"
+ location = "US"
+
+ # variables for this test
+ folder_create = true
+}
+
+run "folder_test" {
+ command = apply
+
+ assert {
+ condition = google_folder.folder[0].display_name == var.folder_name
+ error_message = "The folder name is incorrect. Expected ${var.folder_name} but got ${google_folder.folder[0].display_name}."
+ }
+ assert {
+ condition = google_folder.folder[0].parent == var.parent
+ error_message = "The folder parent is incorrect. Expected ${var.parent} but got ${google_folder.folder[0].parent}."
+ }
+
+}
diff --git a/modules/github-gcloud-oidc/oidc.tf b/modules/github-gcloud-oidc/oidc.tf
index e0462ab..16edca6 100644
--- a/modules/github-gcloud-oidc/oidc.tf
+++ b/modules/github-gcloud-oidc/oidc.tf
@@ -47,13 +47,15 @@ resource "google_project_iam_member" "organizations_member" {
/*
* oidc setup
*/
+#trivy:ignore:avd-gcp-0068
module "oidc" {
source = "terraform-google-modules/github-actions-runners/google//modules/gh-oidc"
- version = "3.1.2"
+ version = "4.0.0"
depends_on = [google_project_service.project_services, google_service_account.bootstrap_sa, google_service_account.organizations_sa]
project_id = google_project.project[0].project_id
pool_id = local.pool_id
provider_id = local.provider_id
+
sa_mapping = {
(google_service_account.bootstrap_sa.account_id) = {
sa_name = google_service_account.bootstrap_sa.name
diff --git a/modules/github-gcloud-oidc/oidc.tftest.hcl b/modules/github-gcloud-oidc/oidc.tftest.hcl
new file mode 100644
index 0000000..c2df5dd
--- /dev/null
+++ b/modules/github-gcloud-oidc/oidc.tftest.hcl
@@ -0,0 +1,67 @@
+mock_provider "github" {}
+mock_provider "google-beta" {}
+mock_provider "github-actions-runners" {}
+override_module {
+ target = module.oidc
+ outputs = {
+ gh-oidc = {
+ sa_name = "test-sa"
+ attribute = "attribute.repository/github-foundations/bootstrap"
+ }
+ }
+}
+
+variables {
+ # required variables
+ github_foundations_organization_name = "github-foundations"
+ parent = "organizations/1234567890"
+ project_parent = "organizations/1234567890"
+ project_name = "test-project"
+ folder_name = "test-folder"
+ bucket_name = "test-bucket"
+ location = "US"
+
+ # variables for this test
+ bootstrap_repo_name = "bootstrap-repo"
+ organizations_repo_name = "organizations-repo"
+}
+
+
+mock_provider "google" {
+ override_resource {
+ target = google_service_account.bootstrap_sa
+ values = {
+ name = "bootstrap-repo-sa"
+ }
+ }
+ override_resource {
+ target = google_service_account.organizations_sa
+ values = {
+ name = "organizations-repo-sa"
+ }
+ }
+}
+
+run "bootstrap_sa_test" {
+ command = apply
+
+ assert {
+ condition = google_service_account.bootstrap_sa.name == "${var.bootstrap_repo_name}-sa"
+ error_message = "The bootstrap service account name is incorrect. Expected ${var.bootstrap_repo_name}-sa but got ${google_service_account.bootstrap_sa.name}."
+ }
+ assert {
+ condition = google_service_account.bootstrap_sa.account_id == "${var.bootstrap_repo_name}-sa"
+ error_message = "The bootstrap service account account_id is incorrect. Expected ${var.bootstrap_repo_name} but got ${google_service_account.bootstrap_sa.account_id}."
+ }
+}
+
+run "organizations_sa_test" {
+ assert {
+ condition = google_service_account.organizations_sa.name == "${var.organizations_repo_name}-sa"
+ error_message = "The organizations service account name is incorrect. Expected ${var.organizations_repo_name}-sa but got ${google_service_account.organizations_sa.name}."
+ }
+ assert {
+ condition = google_service_account.organizations_sa.account_id == "${var.organizations_repo_name}-sa"
+ error_message = "The organizations service account account_id is incorrect. Expected ${var.organizations_repo_name} but got ${google_service_account.organizations_sa.account_id}."
+ }
+}
diff --git a/modules/github-gcloud-oidc/outputs.tftest.hcl b/modules/github-gcloud-oidc/outputs.tftest.hcl
new file mode 100644
index 0000000..5c85ae6
--- /dev/null
+++ b/modules/github-gcloud-oidc/outputs.tftest.hcl
@@ -0,0 +1,75 @@
+mock_provider "github" {}
+mock_provider "google" {}
+mock_provider "google-beta" {}
+mock_provider "github-actions-runners" {}
+override_module {
+ target = module.oidc
+ outputs = {
+ gh-oidc = {
+ sa_name = "test-sa"
+ attribute = "attribute.repository/github-foundations/bootstrap"
+ }
+ provider_name = "gh-oidc-provider"
+ }
+}
+
+variables {
+ # required variables
+ github_foundations_organization_name = "github-foundations"
+ parent = "organizations/1234567890"
+ project_parent = "organizations/1234567890"
+ project_name = "test-project"
+ folder_name = "test-folder"
+ bucket_name = "test-bucket"
+ location = "US"
+
+}
+
+run "outputs_test" {
+ command = apply
+
+ assert {
+ condition = output.folder != null
+ error_message = "The folder was not created."
+ }
+ assert {
+ condition = output.id == output.folder.name
+ error_message = "The folder id is incorrect. Expected ${output.folder.name} but got ${output.id}."
+ }
+ assert {
+ condition = startswith(output.project_id, var.project_name)
+ error_message = "The project id is incorrect. Expected ${var.project_name}#### but got ${output.project_id}."
+ }
+ assert {
+ condition = output.name == var.folder_name
+ error_message = "The folder name is incorrect. Expected ${var.folder_name} but got ${output.name}."
+ }
+ assert {
+ condition = output.provider_name == "gh-oidc-provider"
+ error_message = "The provider name is incorrect. Expected gh-oidc-provider but got ${output.provider_name}."
+ }
+ assert {
+ condition = output.bootstrap_sa != null
+ error_message = "The bootstrap service account was not created."
+ }
+ assert {
+ condition = output.bootstrap_sa_name != null
+ error_message = "The bootstrap service account name was not created."
+ }
+ assert {
+ condition = output.organizations_sa != null
+ error_message = "The organizations service account was not created."
+ }
+ assert {
+ condition = output.organizations_sa_name != null
+ error_message = "The organizations service account name was not created."
+ }
+ assert {
+ condition = output.bucket_name == var.bucket_name
+ error_message = "The bucket name is incorrect. Expected ${var.bucket_name} but got ${output.bucket_name}."
+ }
+ assert {
+ condition = output.bucket_location == var.location
+ error_message = "The bucket location is incorrect. Expected ${var.location} but got ${output.bucket_location}."
+ }
+}
diff --git a/modules/github-gcloud-oidc/project.tf b/modules/github-gcloud-oidc/project.tf
index a52ccc9..415f112 100644
--- a/modules/github-gcloud-oidc/project.tf
+++ b/modules/github-gcloud-oidc/project.tf
@@ -55,7 +55,6 @@ resource "google_project" "project" {
billing_account = var.billing_account
auto_create_network = var.auto_create_network
labels = var.labels
- skip_delete = var.skip_delete
depends_on = [google_folder.folder]
}
diff --git a/modules/github-gcloud-oidc/project.tftest.hcl b/modules/github-gcloud-oidc/project.tftest.hcl
new file mode 100644
index 0000000..37e521f
--- /dev/null
+++ b/modules/github-gcloud-oidc/project.tftest.hcl
@@ -0,0 +1,138 @@
+mock_provider "github" {}
+mock_provider "google" {}
+mock_provider "google-beta" {}
+mock_provider "github-actions-runners" {}
+override_module {
+ target = module.oidc
+ outputs = {
+ gh-oidc = {
+ sa_name = "test-sa"
+ attribute = "attribute.repository/github-foundations/bootstrap"
+ }
+ }
+}
+
+variables {
+ # required variables
+ github_foundations_organization_name = "github-foundations"
+ parent = "organizations/1234567890"
+ project_parent = "organizations/1234567890"
+ project_name = "test-project"
+ folder_name = "test-folder"
+ bucket_name = "test-bucket"
+ location = "US"
+
+ # variables for this test
+ project_create = true
+ billing_account = "billing-account"
+ auto_create_network = true
+ labels = { label1 = "value1", label2 = "value2" }
+ services = ["service1.googleapis.com", "service2.googleapis.com"]
+ service_config = {
+ disable_on_destroy = true
+ disable_dependent_services = true
+ }
+}
+
+
+# run "expect_project_errors_test" {
+# variables {
+# services = ["service1", "service2"]
+# }
+# command = apply
+# plan_options {
+# refresh = true
+# }
+
+# expect_failures = [ google_project.project, google_project_service.project_services ]
+# }
+
+run "google_project_test" {
+ command = apply
+
+ assert {
+ condition = length(google_project.project) == 1
+ error_message = "The project was not created."
+ }
+ assert {
+ condition = google_project.project[0].org_id == split("/", var.project_parent)[1]
+ error_message = "The org_id is incorrect. Expected ${split("/", var.project_parent)[1]} but got ${google_project.project[0].org_id}."
+ }
+ assert {
+ condition = google_project.project[0].folder_id == null
+ error_message = "The folder_id is incorrect. Expected null."
+ }
+ assert {
+ condition = startswith(google_project.project[0].project_id, var.project_name)
+ error_message = "The project name is incorrect. Expected '${var.project_name}####' but got '${google_project.project[0].project_id}'."
+ }
+ assert {
+ condition = startswith(google_project.project[0].name, var.project_name)
+ error_message = "The project name is incorrect. Expected '${var.project_name}####' but got '${google_project.project[0].name}'."
+ }
+ assert {
+ condition = google_project.project[0].billing_account == var.billing_account
+ error_message = "The billing account is incorrect. Expected '${var.billing_account}' but got '${google_project.project[0].billing_account}'."
+ }
+ assert {
+ condition = google_project.project[0].auto_create_network == var.auto_create_network
+ error_message = "The auto_create_network is incorrect. Expected '${var.auto_create_network}' but got '${google_project.project[0].auto_create_network}'."
+ }
+ assert {
+ condition = google_project.project[0].labels.label1 == var.labels.label1
+ error_message = "The label1 is incorrect. Expected '${var.labels.label1}' but got '${google_project.project[0].labels.label1}'."
+ }
+ assert {
+ condition = google_project.project[0].labels.label2 == var.labels.label2
+ error_message = "The label2 is incorrect. Expected '${var.labels.label2}' but got '${google_project.project[0].labels.label2}'."
+ }
+}
+
+run "project_services_test" {
+ assert {
+ condition = length(google_project_service.project_services) == length(var.services)
+ error_message = "The number of services is incorrect. Expected ${length(var.services)} but got ${length(google_project_service.project_services)}."
+ }
+ # test the first service
+ assert {
+ condition = google_project_service.project_services["service1.googleapis.com"].project == google_project.project[0].project_id
+ error_message = "The service1 project is incorrect. Expected '${google_project.project[0].project_id}' but got '${google_project_service.project_services["service1.googleapis.com"].project}'."
+ }
+ assert {
+ condition = google_project_service.project_services["service1.googleapis.com"] != null
+ error_message = "The service1 was not created."
+ }
+ assert {
+ condition = google_project_service.project_services["service1.googleapis.com"].service == "service1.googleapis.com"
+ error_message = "The service1 name is incorrect. Expected 'service1.googleapis.com' but got '${google_project_service.project_services["service1.googleapis.com"].service}'."
+ }
+ assert {
+ condition = google_project_service.project_services["service1.googleapis.com"].disable_on_destroy == var.service_config.disable_on_destroy
+ error_message = "The service1 disable_on_destroy is incorrect. Expected '${var.service_config.disable_on_destroy}' but got '${google_project_service.project_services["service1.googleapis.com"].disable_on_destroy}'."
+ }
+ assert {
+ condition = google_project_service.project_services["service1.googleapis.com"].disable_dependent_services == var.service_config.disable_dependent_services
+ error_message = "The service1 disable_dependent_services is incorrect. Expected '${var.service_config.disable_dependent_services}' but got '${google_project_service.project_services["service1.googleapis.com"].disable_dependent_services}'."
+ }
+ # test the second service
+ assert {
+ condition = google_project_service.project_services["service2.googleapis.com"].project == google_project.project[0].project_id
+ error_message = "The service2 project is incorrect. Expected '${google_project.project[0].project_id}' but got '${google_project_service.project_services["service2.googleapis.com"].project}'."
+ }
+ assert {
+ condition = google_project_service.project_services["service2.googleapis.com"] != null
+ error_message = "The service2 was not created."
+ }
+ assert {
+ condition = google_project_service.project_services["service2.googleapis.com"].service == "service2.googleapis.com"
+ error_message = "The service2 name is incorrect. Expected 'service2.googleapis.com' but got '${google_project_service.project_services["service2.googleapis.com"].service}'."
+ }
+ assert {
+ condition = google_project_service.project_services["service2.googleapis.com"].disable_on_destroy == var.service_config.disable_on_destroy
+ error_message = "The service2 disable_on_destroy is incorrect. Expected '${var.service_config.disable_on_destroy}' but got '${google_project_service.project_services["service2.googleapis.com"].disable_on_destroy}'."
+ }
+ assert {
+ condition = google_project_service.project_services["service2.googleapis.com"].disable_dependent_services == var.service_config.disable_dependent_services
+ error_message = "The service2 disable_dependent_services is incorrect. Expected '${var.service_config.disable_dependent_services}' but got '${google_project_service.project_services["service2.googleapis.com"].disable_dependent_services}'."
+ }
+}
diff --git a/modules/github-gcloud-oidc/storage.tftest.hcl b/modules/github-gcloud-oidc/storage.tftest.hcl
new file mode 100644
index 0000000..32477cc
--- /dev/null
+++ b/modules/github-gcloud-oidc/storage.tftest.hcl
@@ -0,0 +1,188 @@
+mock_provider "github" {}
+mock_provider "google" {}
+mock_provider "google-beta" {}
+mock_provider "github-actions-runners" {}
+override_module {
+ target = module.oidc
+ outputs = {
+ gh-oidc = {
+ sa_name = "test-sa"
+ attribute = "attribute.repository/github-foundations/bootstrap"
+ }
+ }
+}
+
+variables {
+ # required variables
+ github_foundations_organization_name = "github-foundations"
+ parent = "organizations/1234567890"
+ project_parent = "organizations/1234567890"
+ project_name = "test-project"
+ folder_name = "test-folder"
+ bucket_name = "test-bucket"
+ location = "US"
+
+ # variables for this test
+ storage_class = "REGIONAL"
+ force_destroy = true
+ uniform_bucket_level_access = false
+ labels = { label1 = "value1", label2 = "value2" }
+ default_event_based_hold = true
+ requester_pays = true
+ versioning = true
+}
+
+run "expect_parent_errors_test" {
+ variables {
+ parent = "invalid-parent"
+ }
+ command = plan
+
+ expect_failures = [var.parent]
+}
+
+run "expect_prefix_errors_test" {
+ variables {
+ prefix = ""
+ }
+
+ command = plan
+
+ expect_failures = [var.prefix]
+}
+
+run "expect_project_parent_errors_test" {
+ variables {
+ project_parent = "invalid-parent"
+ }
+ command = plan
+
+ expect_failures = [var.project_parent]
+}
+
+run "expect_storage_class_errors_test" {
+ variables {
+ storage_class = "INVALID"
+ }
+ command = plan
+
+ expect_failures = [var.storage_class]
+}
+
+run "expect_lifecycle_rules_action_type_errors_test" {
+ variables {
+ lifecycle_rules = {
+ rule1 = {
+ action = {
+ type = "INVALID"
+ }
+ condition = {
+ age = 30
+ }
+ }
+ }
+ }
+ command = plan
+
+ expect_failures = [var.lifecycle_rules]
+}
+
+run "expect_lifecycle_rules_storage_class_errors_test" {
+ variables {
+ lifecycle_rules = {
+ rule1 = {
+ action = {
+ type = "SetStorageClass"
+ }
+ condition = {
+ age = 30
+ }
+ }
+ }
+ }
+ command = plan
+
+ expect_failures = [var.lifecycle_rules]
+}
+
+run "storage_bucket_test" {
+
+ # Set some extra variables for this test
+ variables {
+ autoclass = true
+ website = {
+ main_page_suffix = "index.html"
+ not_found_page = "404.html"
+ }
+ encryption_key = "projects/${var.project_name}/locations/${var.location}/keyRings/${var.bucket_name}-keyring/cryptoKeys/${var.bucket_name}-key"
+ retention_policy = {
+ retention_period = 30
+ is_locked = true
+ }
+ logging_config = {
+ log_bucket = "log-bucket"
+ log_object_prefix = "log-object-prefix"
+ }
+ cors = {
+ origin = ["*"]
+ method = ["GET", "POST"]
+ response_header = ["Content-Type"]
+ max_age_seconds = 3600
+ }
+ lifecycle_rules = {
+ rule1 = {
+ action = {
+ type = "Delete"
+ storage_class = "NEARLINE"
+ }
+ condition = {
+ age = 30
+ }
+ }
+ }
+ custom_placement_config = ["us-central1", "us-west1"]
+ }
+
+ command = apply
+
+ assert {
+ condition = google_storage_bucket.bucket.name == var.bucket_name
+ error_message = "The bucket name is incorrect. Expected ${var.bucket_name} but got ${google_storage_bucket.bucket.name}."
+ }
+ assert {
+ condition = startswith(google_storage_bucket.bucket.project, var.project_name)
+ error_message = "The project name is incorrect. Expected ${var.project_name}#### but got ${google_storage_bucket.bucket.project}."
+ }
+ assert {
+ condition = google_storage_bucket.bucket.location == var.location
+ error_message = "The location is incorrect. Expected ${var.location} but got ${google_storage_bucket.bucket.location}."
+ }
+ assert {
+ condition = google_storage_bucket.bucket.storage_class == var.storage_class
+ error_message = "The storage class is incorrect. Expected ${var.storage_class} but got ${google_storage_bucket.bucket.storage_class}."
+ }
+ assert {
+ condition = google_storage_bucket.bucket.force_destroy == var.force_destroy
+ error_message = "The force destroy is incorrect. Expected ${var.force_destroy} but got ${google_storage_bucket.bucket.force_destroy}."
+ }
+ assert {
+ condition = google_storage_bucket.bucket.uniform_bucket_level_access == var.uniform_bucket_level_access
+ error_message = "The uniform bucket level access is incorrect. Expected ${var.uniform_bucket_level_access} but got ${google_storage_bucket.bucket.uniform_bucket_level_access}."
+ }
+ assert {
+ condition = google_storage_bucket.bucket.labels.label1 == var.labels.label1
+ error_message = "The label1 is incorrect. Expected ${var.labels.label1} but got ${google_storage_bucket.bucket.labels.label1}."
+ }
+ assert {
+ condition = google_storage_bucket.bucket.labels.label2 == var.labels.label2
+ error_message = "The label2 is incorrect. Expected ${var.labels.label2} but got ${google_storage_bucket.bucket.labels.label2}."
+ }
+ assert {
+ condition = google_storage_bucket.bucket.default_event_based_hold == var.default_event_based_hold
+ error_message = "The default event based hold is incorrect. Expected ${var.default_event_based_hold} but got ${google_storage_bucket.bucket.default_event_based_hold}."
+ }
+ assert {
+ condition = google_storage_bucket.bucket.requester_pays == var.requester_pays
+ error_message = "The requester pays is incorrect. Expected ${var.requester_pays} but got ${google_storage_bucket.bucket.requester_pays}."
+ }
+}
diff --git a/modules/github-gcloud-oidc/variables.tf b/modules/github-gcloud-oidc/variables.tf
index 64ad92a..5d7385b 100644
--- a/modules/github-gcloud-oidc/variables.tf
+++ b/modules/github-gcloud-oidc/variables.tf
@@ -76,12 +76,6 @@ variable "labels" {
default = {}
}
-variable "skip_delete" {
- description = "Allows the underlying resources to be destroyed without destroying the project itself."
- type = bool
- default = false
-}
-
variable "services" {
description = "Service APIs to enable."
type = list(string)
diff --git a/modules/github-gcloud-oidc/versions.tf b/modules/github-gcloud-oidc/versions.tf
index 4658f90..821da76 100644
--- a/modules/github-gcloud-oidc/versions.tf
+++ b/modules/github-gcloud-oidc/versions.tf
@@ -3,11 +3,11 @@ terraform {
required_providers {
google = {
source = "hashicorp/google"
- version = ">= 3.77" # tftest
+ version = ">= 6.12.0" # tftest
}
google-beta = {
source = "hashicorp/google-beta"
- version = ">= 3.77" # tftest
+ version = ">= 6.12.0" # tftest
}
random = {
source = "hashicorp/random"
diff --git a/modules/organization/README.md b/modules/organization/README.md
index 944d60f..7fa5253 100644
--- a/modules/organization/README.md
+++ b/modules/organization/README.md
@@ -43,11 +43,11 @@
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
-| [actions\_secrets](#input\_actions\_secrets) | A map of organization-level GitHub Actions secrets to create. The key is the name of the secret and the value is an object describing how to create the secret. | map(object({
encrypted_value = string
visibility = string
})) | `{}` | no |
-| [codespaces\_secrets](#input\_codespaces\_secrets) | A map of organization-level GitHub Codespaces secrets to create. The key is the name of the secret and the value is an object describing how to create the secret. | map(object({
encrypted_value = string
visibility = string
})) | `{}` | no |
-| [custom\_repository\_roles](#input\_custom\_repository\_roles) | A map of custom repository roles to create. The key is the name of the role and the value is the role configurations. | map(object({
description = string
base_role = string
permissions = list(string)
})) | n/a | yes |
-| [default\_branch\_protection\_rulesets](#input\_default\_branch\_protection\_rulesets) | n/a | object({
base_protection = optional(object({
enforcement = string
}))
minimum_approvals = optional(object({
enforcement = string
approvals_required = number
}))
dismiss_stale_reviews = optional(object({
enforcement = string
}))
require_signatures = optional(object({
enforcement = string
}))
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
}) | `{}` | no |
-| [dependabot\_secrets](#input\_dependabot\_secrets) | A map of organization-level Dependabot secrets to create. The key is the name of the secret and the value is an object describing how to create the secret. | map(object({
encrypted_value = string
visibility = string
})) | `{}` | no |
+| [actions\_secrets](#input\_actions\_secrets) | A map of organization-level GitHub Actions secrets to create. The key is the name of the secret and the value is an object describing how to create the secret. | map(object({
encrypted_value = string
visibility = string
})) | `{}` | no |
+| [codespaces\_secrets](#input\_codespaces\_secrets) | A map of organization-level GitHub Codespaces secrets to create. The key is the name of the secret and the value is an object describing how to create the secret. | map(object({
encrypted_value = string
visibility = string
})) | `{}` | no |
+| [custom\_repository\_roles](#input\_custom\_repository\_roles) | A map of custom repository roles to create. The key is the name of the role and the value is the role configurations. | map(object({
description = string
base_role = string
permissions = list(string)
})) | n/a | yes |
+| [default\_branch\_protection\_rulesets](#input\_default\_branch\_protection\_rulesets) | n/a | object({
base_protection = optional(object({
enforcement = string
}))
minimum_approvals = optional(object({
enforcement = string
approvals_required = number
}))
dismiss_stale_reviews = optional(object({
enforcement = string
}))
require_signatures = optional(object({
enforcement = string
}))
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
}) | `{}` | no |
+| [dependabot\_secrets](#input\_dependabot\_secrets) | A map of organization-level Dependabot secrets to create. The key is the name of the secret and the value is an object describing how to create the secret. | map(object({
encrypted_value = string
visibility = string
})) | `{}` | no |
| [enable\_community\_manager\_role](#input\_enable\_community\_manager\_role) | If `true` will create a custom repository role for community managers. Defaults to `false`. If `true` the maximum number of `custom_repository_roles` that can be defined will be reduced by one. | `bool` | `false` | no |
| [enable\_contractor\_role](#input\_enable\_contractor\_role) | If `true` will create a custom repository role for contractors. Defaults to `false`. If `true` the maximum number of `custom_repository_roles` that can be defined will be reduced by one. | `bool` | `false` | no |
| [enable\_security\_engineer\_role](#input\_enable\_security\_engineer\_role) | If `true` will create a custom repository role for security engineers. Defaults to `false`. If `true` the maximum number of `custom_repository_roles` that can be defined will be reduced by one. | `bool` | `false` | no |
@@ -63,10 +63,10 @@
| [github\_organization\_enable\_secret\_scanning\_push\_protection](#input\_github\_organization\_enable\_secret\_scanning\_push\_protection) | If set secret scanning push protection will be enabled for new repositories in the organization. Defaults to `true`. | `bool` | `true` | no |
| [github\_organization\_location](#input\_github\_organization\_location) | Organization location. Defaults to `''`. | `string` | `""` | no |
| [github\_organization\_members](#input\_github\_organization\_members) | A list of usernames to invite to the organization. Defaults to `[]`. | `list(string)` | `[]` | no |
-| [github\_organization\_pages\_settings](#input\_github\_organization\_pages\_settings) | Settings for organization page creation. The default setting does not allow members to create public and private pages. | object({
members_can_create_public = bool,
members_can_create_private = bool
}) | {
"members_can_create_private": false,
"members_can_create_public": false
} | no |
-| [github\_organization\_repository\_settings](#input\_github\_organization\_repository\_settings) | Settings for organization repository creation. The default setting allows members to create internal and private repositories but not public. | object({
members_can_create_public = bool,
members_can_create_internal = bool,
members_can_create_private = bool
}) | {
"members_can_create_internal": true,
"members_can_create_private": true,
"members_can_create_public": false
} | no |
+| [github\_organization\_pages\_settings](#input\_github\_organization\_pages\_settings) | Settings for organization page creation. The default setting does not allow members to create public and private pages. | object({
members_can_create_public = bool,
members_can_create_private = bool
}) | {
"members_can_create_private": false,
"members_can_create_public": false
} | no |
+| [github\_organization\_repository\_settings](#input\_github\_organization\_repository\_settings) | Settings for organization repository creation. The default setting allows members to create internal and private repositories but not public. | object({
members_can_create_public = bool,
members_can_create_internal = bool,
members_can_create_private = bool
}) | {
"members_can_create_internal": true,
"members_can_create_private": true,
"members_can_create_public": false
} | no |
| [github\_organization\_requires\_web\_commit\_signing](#input\_github\_organization\_requires\_web\_commit\_signing) | If set commit signatures are required for commits to the organization. Defaults to `false`. | `bool` | `false` | no |
-| [rulesets](#input\_rulesets) | n/a | map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
repository_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_workflows = optional(object({
required_workflows = list(object({
repository_id = number
path = string
ref = optional(string)
}))
}))
})
target = string
enforcement = string
})) | `{}` | no |
+| [rulesets](#input\_rulesets) | n/a | map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
repository_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_workflows = optional(object({
required_workflows = list(object({
repository_id = number
path = string
ref = optional(string)
}))
}))
})
target = string
enforcement = string
})) | `{}` | no |
## Outputs
diff --git a/modules/organization/block.tftest.hcl b/modules/organization/block.tftest.hcl
new file mode 100644
index 0000000..597bf0d
--- /dev/null
+++ b/modules/organization/block.tftest.hcl
@@ -0,0 +1,32 @@
+mock_provider "github" {}
+
+variables {
+ # required variables
+ github_organization_billing_email = "org_billing_email@focisolutions.com"
+
+ custom_repository_roles = {
+ custom_role1 = {
+ description = "Custom role 1"
+ base_role = "read"
+ permissions = ["pull", "push"]
+ }
+ custom_role2 = {
+ description = "Custom role 2"
+ base_role = "write"
+ permissions = ["pull", "push", "delete"]
+ }
+ }
+
+ # variables for this test
+ github_organization_blocked_users = ["user1", "user2", "user3", "user4"]
+}
+
+run "blocked_user_test" {
+
+ command = apply
+
+ assert {
+ condition = length(github_organization_block.blocked_user) == length(var.github_organization_blocked_users)
+ error_message = "The number or blocked users is incorrect. Expected ${length(var.github_organization_blocked_users)} but got ${length(github_organization_block.blocked_user)}."
+ }
+}
diff --git a/modules/organization/members.tftest.hcl b/modules/organization/members.tftest.hcl
new file mode 100644
index 0000000..0cfae05
--- /dev/null
+++ b/modules/organization/members.tftest.hcl
@@ -0,0 +1,32 @@
+mock_provider "github" {}
+
+variables {
+ # required variables
+ github_organization_billing_email = "org_billing_email@focisolutions.com"
+
+ custom_repository_roles = {
+ custom_role1 = {
+ description = "Custom role 1"
+ base_role = "read"
+ permissions = ["pull", "push"]
+ }
+ custom_role2 = {
+ description = "Custom role 2"
+ base_role = "write"
+ permissions = ["pull", "push", "delete"]
+ }
+ }
+
+ # variables for this test
+ github_organization_members = ["user1", "user2", "user3", "user4", "user5"]
+}
+
+run "members_test" {
+
+ command = apply
+
+ assert {
+ condition = length(github_membership.membership_for_user) == length(var.github_organization_members)
+ error_message = "The number or members is incorrect. Expected ${length(var.github_organization_members)} but got ${length(github_membership.membership_for_user)}."
+ }
+}
diff --git a/modules/organization/organization.tftest.hcl b/modules/organization/organization.tftest.hcl
new file mode 100644
index 0000000..d1f7483
--- /dev/null
+++ b/modules/organization/organization.tftest.hcl
@@ -0,0 +1,36 @@
+mock_provider "github" {}
+
+variables {
+ # required variables
+ github_organization_billing_email = "org_billing_email@focisolutions.com"
+
+ custom_repository_roles = {
+ custom_role1 = {
+ description = "Custom role 1"
+ base_role = "read"
+ permissions = ["pull", "push"]
+ }
+ custom_role2 = {
+ description = "Custom role 2"
+ base_role = "write"
+ permissions = ["pull", "push", "delete"]
+ }
+ }
+
+ # variables for this test
+ enable_security_engineer_role = true
+ enable_contractor_role = false
+ enable_community_manager_role = true
+
+ github_organization_enable_ghas = false
+}
+
+run "custom_repository_role_test" {
+
+ command = apply
+
+ assert {
+ condition = length(output.custom_role_ids) == length(var.custom_repository_roles)
+ error_message = "The number or roles is incorrect. Expected ${length(var.custom_repository_roles)} but got ${length(output.custom_role_ids)}."
+ }
+}
diff --git a/modules/organization/roles.tf b/modules/organization/roles.tf
index f4427ac..169b1c5 100644
--- a/modules/organization/roles.tf
+++ b/modules/organization/roles.tf
@@ -8,7 +8,7 @@ resource "github_organization_custom_role" "custom_repository_role" {
lifecycle {
precondition {
condition = length(var.custom_repository_roles) <= 5 - (var.enable_security_engineer_role ? 1 : 0) - (var.enable_contractor_role ? 1 : 0) - (var.enable_community_manager_role ? 1 : 0)
- error_message = "To many custom repository roles defined, an orrganization's maximum is 5. This limit is reduced by one for each of the following variables that are set to true: `enable_security_engineer_role`, `enable_contractor_role`, `enable_community_manager_role`."
+ error_message = "To many custom repository roles defined, an organization's maximum is 5. This limit is reduced by one for each of the following variables that are set to true: `enable_security_engineer_role`, `enable_contractor_role`, `enable_community_manager_role`."
}
}
}
diff --git a/modules/organization/roles.tftest.hcl b/modules/organization/roles.tftest.hcl
new file mode 100644
index 0000000..fb89f40
--- /dev/null
+++ b/modules/organization/roles.tftest.hcl
@@ -0,0 +1,121 @@
+mock_provider "github" {}
+
+variables {
+ # required variables
+ github_organization_billing_email = "org_billing_email@focisolutions.com"
+
+ custom_repository_roles = {
+ custom_role1 = {
+ description = "Custom role 1"
+ base_role = "read"
+ permissions = ["pull", "push"]
+ }
+ custom_role2 = {
+ description = "Custom role 2"
+ base_role = "write"
+ permissions = ["pull", "push", "delete"]
+ }
+ }
+
+ # variables for this test
+ enable_security_engineer_role = true
+ enable_contractor_role = true
+ enable_community_manager_role = true
+}
+
+run "custom_repository_role_test" {
+
+ command = apply
+
+ assert {
+ condition = length(github_organization_custom_role.custom_repository_role) == length(var.custom_repository_roles)
+ error_message = "The number or roles is incorrect. Expected ${length(var.custom_repository_roles)} but got ${length(github_organization_custom_role.custom_repository_role)}."
+ }
+}
+
+run "custom_repository_role_failure_test" {
+
+ # too many custom roles - expect failure
+ variables {
+ custom_repository_roles = {
+ custom_role1 = {
+ description = "Custom role 1"
+ base_role = "read"
+ permissions = ["pull", "push"]
+ }
+ custom_role2 = {
+ description = "Custom role 2"
+ base_role = "write"
+ permissions = ["pull", "push", "delete"]
+ }
+ custom_role3 = {
+ description = "Custom role 3"
+ base_role = "maintain"
+ permissions = ["pull", "push", "delete", "admin"]
+ }
+ }
+ }
+
+ command = plan
+ expect_failures = [github_organization_custom_role.custom_repository_role]
+}
+
+run "security_engineer_role_test" {
+
+ assert {
+ condition = github_organization_custom_role.security_engineer_role[0] != null
+ error_message = "The security engineer role was not created."
+ }
+ assert {
+ condition = github_organization_custom_role.security_engineer_role[0].name == "Security Engineer"
+ error_message = "The security engineer role name is incorrect. Expected 'Security Engineer' but got '${github_organization_custom_role.security_engineer_role[0].name}'."
+ }
+ assert {
+ condition = github_organization_custom_role.security_engineer_role[0].base_role == "maintain"
+ error_message = "The security engineer role base role is incorrect. Expected 'maintain' but got '${github_organization_custom_role.security_engineer_role[0].base_role}'."
+ }
+ assert {
+ condition = length(github_organization_custom_role.security_engineer_role[0].permissions) == 2
+ error_message = "The security engineer role permissions are incorrect. Expected 2 but got ${length(github_organization_custom_role.security_engineer_role[0].permissions)}."
+ }
+}
+
+run "contractor_role_test" {
+
+ assert {
+ condition = github_organization_custom_role.contractor_role[0] != null
+ error_message = "The contractor role was not created."
+ }
+ assert {
+ condition = github_organization_custom_role.contractor_role[0].name == "Contractor"
+ error_message = "The contractor role name is incorrect. Expected 'Contractor' but got '${github_organization_custom_role.contractor_role[0].name}'."
+ }
+ assert {
+ condition = github_organization_custom_role.contractor_role[0].base_role == "write"
+ error_message = "The contractor role base role is incorrect. Expected 'write' but got '${github_organization_custom_role.contractor_role[0].base_role}'."
+ }
+ assert {
+ condition = length(github_organization_custom_role.contractor_role[0].permissions) == 1
+ error_message = "The contractor role permissions are incorrect. Expected 1 but got ${length(github_organization_custom_role.contractor_role[0].permissions)}."
+ }
+}
+
+run "community_manager_role_test" {
+
+ assert {
+ condition = github_organization_custom_role.community_manager_role[0] != null
+ error_message = "The community manager role was not created."
+ }
+ assert {
+ condition = github_organization_custom_role.community_manager_role[0].name == "Community Manager"
+ error_message = "The community manager role name is incorrect. Expected 'Community Manager' but got '${github_organization_custom_role.community_manager_role[0].name}'."
+ }
+ assert {
+ condition = github_organization_custom_role.community_manager_role[0].base_role == "read"
+ error_message = "The community manager role base role is incorrect. Expected 'read' but got '${github_organization_custom_role.community_manager_role[0].base_role}'."
+ }
+ assert {
+ condition = length(github_organization_custom_role.community_manager_role[0].permissions) == 13
+ error_message = "The community manager role permissions are incorrect. Expected 13 but got ${length(github_organization_custom_role.community_manager_role[0].permissions)}."
+ }
+}
diff --git a/modules/organization/rulesets.tftest.hcl b/modules/organization/rulesets.tftest.hcl
new file mode 100644
index 0000000..41f02ec
--- /dev/null
+++ b/modules/organization/rulesets.tftest.hcl
@@ -0,0 +1,186 @@
+mock_provider "github" {}
+
+# We need to mock some data sources in this teset
+override_data {
+ target = data.github_team.branch_ruleset_bypasser["team1"]
+ values = {
+ id = 2
+ }
+}
+
+override_data {
+ target = data.github_user.branch_ruleset_bypasser["user5"]
+ values = {
+ id = 5
+ }
+}
+
+variables {
+ github_organization_billing_email = "org_billing_email@focisolutions.com"
+
+ custom_repository_roles = {
+ custom_role1 = {
+ description = "Custom role 1"
+ base_role = "read"
+ permissions = ["pull", "push"]
+ }
+ }
+
+ default_branch_protection_rulesets = {
+ base_protection = {
+ enforcement = "evaluate"
+ }
+ minimum_approvals = {
+ enforcement = "active"
+ approvals_required = 1
+ }
+ dismiss_stale_reviews = {
+ enforcement = "disabled"
+ }
+ require_signatures = {
+ enforcement = "evaluate"
+ }
+ bypass_actors = {
+ repository_roles = [
+ {
+ role = "maintain"
+ always_bypass = false
+ }
+ ]
+ teams = [
+ {
+ team = "team1"
+ always_bypass = true
+ }
+ ]
+ integrations = [
+ {
+ installation_id = 333333
+ always_bypass = false
+ }
+ ]
+ organization_admins = [
+ {
+ user = "user5"
+ always_bypass = true
+ }
+ ]
+ }
+ }
+
+ rulesets = {
+ ruleset1 = {
+ target = "branch"
+ enforcement = "evaluate"
+
+ bypass_actors = {
+ repository_roles = [
+ {
+ role = "maintain"
+ always_bypass = false
+ }
+ ]
+ teams = [
+ {
+ team = "team1"
+ always_bypass = true
+ }
+ ]
+ integrations = [
+ {
+ installation_id = 333333
+ always_bypass = false
+ }
+ ]
+ organization_admins = [
+ {
+ user = "user5"
+ always_bypass = true
+ }
+ ]
+ }
+ conditions = {
+ ref_name = {
+ include = ["main"]
+ exclude = ["feature/*"]
+ }
+ repository_name = {
+ include = ["repo1"]
+ exclude = ["repo2"]
+ }
+ }
+ rules = {
+ branch_name_pattern = {
+ operator = "regex"
+ pattern = "main"
+ name = "branch_name_pattern"
+ negate = false
+ }
+ commit_author_email_pattern = {
+ operator = "regex"
+ pattern = "pattern"
+ name = "commit_author_email_pattern"
+ negate = false
+ }
+ commit_message_pattern = {
+ operator = "regex"
+ pattern = "pattern"
+ name = "commit_message_pattern"
+ negate = false
+ }
+ committer_email_pattern = {
+ operator = "regex"
+ pattern = "pattern"
+ name = "committer_email_pattern"
+ negate = false
+ }
+ creation = true
+ deletion = false
+ update = true
+ non_fast_forward = false
+ required_linear_history = true
+ required_signatures = false
+ update_allows_fetch_and_merge = true
+ }
+ }
+ }
+}
+
+run "ruleset_test" {
+ command = apply
+
+ assert {
+ condition = module.ruleset != null
+ error_message = "The ruleset is null"
+ }
+}
+
+# This is crashing Terraform (v.1.9.8)
+# run "base_default_branch_protection_ruleset_test" {
+
+# assert {
+# condition = module.base_default_branch_protection != null
+# error_message = "The base_default_branch_protection is null"
+# }
+# }
+
+# run "minimum_approvals_test" {
+# assert {
+# condition = module.minimum_approvals != null
+# error_message = "The minimum_approvals is null"
+# }
+# }
+
+# run "dismiss_stale_reviews_test" {
+# assert {
+# condition = module.dismiss_stale_reviews != null
+# error_message = "The dismiss_stale_reviews is null"
+# }
+# }
+
+# run "require_signatures_test" {
+# assert {
+# condition = module.require_signatures != null
+# error_message = "The require_signatures is null"
+# }
+# }
diff --git a/modules/organization/secrets.tftest.hcl b/modules/organization/secrets.tftest.hcl
new file mode 100644
index 0000000..acfaa83
--- /dev/null
+++ b/modules/organization/secrets.tftest.hcl
@@ -0,0 +1,157 @@
+mock_provider "github" {}
+
+variables {
+ github_organization_billing_email = "org_billing_email@focisolutions.com"
+
+ custom_repository_roles = {
+ custom_role1 = {
+ description = "Custom role 1"
+ base_role = "read"
+ permissions = ["pull", "push"]
+ }
+ }
+
+ actions_secrets = {
+ action_secret1 = {
+ encrypted_value = "dmFsdWUxCg==" # base64 encoded
+ visibility = "all"
+ },
+ action_secret2 = {
+ encrypted_value = "dmFsdWUyCg==" # base64 encoded
+ visibility = "private"
+ }
+
+ }
+
+ codespaces_secrets = {
+ codespace_secret1 = {
+ encrypted_value = "dmFsdWUxCg==" # base64 encoded
+ visibility = "all"
+ }
+ }
+
+ dependabot_secrets = {
+ dependabot_secret1 = {
+ encrypted_value = "dmFsdWUxCg==" # base64 encoded
+ visibility = "all"
+ },
+ dependabot_secret2 = {
+ encrypted_value = "dmFsdWUyCg==" # base64 encoded
+ visibility = "private"
+ },
+ dependabot_secret3 = {
+ encrypted_value = "dmFsdWUzCg==" # base64 encoded
+ visibility = "private"
+ }
+ }
+}
+
+run "action_secret_test" {
+ command = apply
+
+ assert {
+ condition = github_actions_organization_secret.action_secret["action_secret1"].secret_name == "action_secret1"
+ error_message = "The action secret name is incorrect. Expected `action_secret1`, got `${github_actions_organization_secret.action_secret["action_secret1"].secret_name}`"
+ }
+ assert {
+ condition = github_actions_organization_secret.action_secret["action_secret1"].encrypted_value == "dmFsdWUxCg=="
+ error_message = "The action secret encrypted value is incorrect. Expected `dmFsdWUxCg==`, got `${nonsensitive(github_actions_organization_secret.action_secret["action_secret1"].encrypted_value)}`"
+ }
+ assert {
+ condition = github_actions_organization_secret.action_secret["action_secret1"].visibility == "all"
+ error_message = "The action secret visibility is incorrect. Expected `all`, got `${github_actions_organization_secret.action_secret["action_secret1"].visibility}`"
+ }
+ assert {
+ condition = length(github_actions_organization_secret.action_secret["action_secret1"].selected_repository_ids) == 0
+ error_message = "The action secret selected repository ids is incorrect. Expected `0`, got `${length(github_actions_organization_secret.action_secret["action_secret1"].selected_repository_ids)}`"
+ }
+ assert {
+ condition = github_actions_organization_secret.action_secret["action_secret2"].secret_name == "action_secret2"
+ error_message = "The action secret name is incorrect. Expected `action_secret2`, got `${github_actions_organization_secret.action_secret["action_secret2"].secret_name}`"
+ }
+ assert {
+ condition = github_actions_organization_secret.action_secret["action_secret2"].encrypted_value == "dmFsdWUyCg=="
+ error_message = "The action secret encrypted value is incorrect. Expected `dmFsdWUyCg==`, got `${nonsensitive(github_actions_organization_secret.action_secret["action_secret2"].encrypted_value)}`"
+ }
+ assert {
+ condition = github_actions_organization_secret.action_secret["action_secret2"].visibility == "private"
+ error_message = "The action secret visibility is incorrect. Expected `private`, got `${github_actions_organization_secret.action_secret["action_secret2"].visibility}`"
+ }
+ assert {
+ condition = length(github_actions_organization_secret.action_secret["action_secret2"].selected_repository_ids) == 0
+ error_message = "The action secret selected repository ids is incorrect. Expected `0`, got `${length(github_actions_organization_secret.action_secret["action_secret2"].selected_repository_ids)}`"
+ }
+}
+
+run "codespace_secret_test" {
+
+ assert {
+ condition = github_codespaces_organization_secret.codespace_secret["codespace_secret1"].secret_name == "codespace_secret1"
+ error_message = "The codespace secret name is incorrect. Expected `codespace_secret1`, got `${github_codespaces_organization_secret.codespace_secret["codespace_secret1"].secret_name}`"
+ }
+ assert {
+ condition = github_codespaces_organization_secret.codespace_secret["codespace_secret1"].encrypted_value == "dmFsdWUxCg=="
+ error_message = "The codespace secret encrypted value is incorrect. Expected `dmFsdWUxCg==`, got `${nonsensitive(github_codespaces_organization_secret.codespace_secret["codespace_secret1"].encrypted_value)}`"
+ }
+ assert {
+ condition = github_codespaces_organization_secret.codespace_secret["codespace_secret1"].visibility == "all"
+ error_message = "The codespace secret visibility is incorrect. Expected `all`, got `${github_codespaces_organization_secret.codespace_secret["codespace_secret1"].visibility}`"
+ }
+ assert {
+ condition = length(github_codespaces_organization_secret.codespace_secret["codespace_secret1"].selected_repository_ids) == 0
+ error_message = "The codespace secret selected repository ids is incorrect. Expected `0`, got `${length(github_codespaces_organization_secret.codespace_secret["codespace_secret1"].selected_repository_ids)}`"
+ }
+}
+
+run "dependabot_secret_test" {
+ assert {
+ condition = github_dependabot_organization_secret.dependabot_secret["dependabot_secret1"].secret_name == "dependabot_secret1"
+ error_message = "The dependabot secret name is incorrect. Expected `dependabot_secret1`, got `${github_dependabot_organization_secret.dependabot_secret["dependabot_secret1"].secret_name}`"
+ }
+ assert {
+ condition = github_dependabot_organization_secret.dependabot_secret["dependabot_secret1"].encrypted_value == "dmFsdWUxCg=="
+ error_message = "The dependabot secret encrypted value is incorrect. Expected `dmFsdWUxCg==`, got `${nonsensitive(github_dependabot_organization_secret.dependabot_secret["dependabot_secret1"].encrypted_value)}`"
+ }
+ assert {
+ condition = github_dependabot_organization_secret.dependabot_secret["dependabot_secret1"].visibility == "all"
+ error_message = "The dependabot secret visibility is incorrect. Expected `all`, got `${github_dependabot_organization_secret.dependabot_secret["dependabot_secret1"].visibility}`"
+ }
+ assert {
+ condition = length(github_dependabot_organization_secret.dependabot_secret["dependabot_secret1"].selected_repository_ids) == 0
+ error_message = "The dependabot secret selected repository ids is incorrect. Expected `0`, got `${length(github_dependabot_organization_secret.dependabot_secret["dependabot_secret1"].selected_repository_ids)}`"
+ }
+ # test the second secret
+ assert {
+ condition = github_dependabot_organization_secret.dependabot_secret["dependabot_secret2"].secret_name == "dependabot_secret2"
+ error_message = "The dependabot secret name is incorrect. Expected `dependabot_secret2`, got `${github_dependabot_organization_secret.dependabot_secret["dependabot_secret2"].secret_name}`"
+ }
+ assert {
+ condition = github_dependabot_organization_secret.dependabot_secret["dependabot_secret2"].encrypted_value == "dmFsdWUyCg=="
+ error_message = "The dependabot secret encrypted value is incorrect. Expected `dmFsdWUyCg==`, got `${nonsensitive(github_dependabot_organization_secret.dependabot_secret["dependabot_secret2"].encrypted_value)}`"
+ }
+ assert {
+ condition = github_dependabot_organization_secret.dependabot_secret["dependabot_secret2"].visibility == "private"
+ error_message = "The dependabot secret visibility is incorrect. Expected `private`, got `${github_dependabot_organization_secret.dependabot_secret["dependabot_secret2"].visibility}`"
+ }
+ assert {
+ condition = length(github_dependabot_organization_secret.dependabot_secret["dependabot_secret2"].selected_repository_ids) == 0
+ error_message = "The dependabot secret selected repository ids is incorrect. Expected `0`, got `${length(github_dependabot_organization_secret.dependabot_secret["dependabot_secret2"].selected_repository_ids)}`"
+ }
+ # test the third secret
+ assert {
+ condition = github_dependabot_organization_secret.dependabot_secret["dependabot_secret3"].secret_name == "dependabot_secret3"
+ error_message = "The dependabot secret name is incorrect. Expected `dependabot_secret3`, got `${github_dependabot_organization_secret.dependabot_secret["dependabot_secret3"].secret_name}`"
+ }
+ assert {
+ condition = github_dependabot_organization_secret.dependabot_secret["dependabot_secret3"].encrypted_value == "dmFsdWUzCg=="
+ error_message = "The dependabot secret encrypted value is incorrect. Expected `dmFsdWUzCg==`, got `${nonsensitive(github_dependabot_organization_secret.dependabot_secret["dependabot_secret3"].encrypted_value)}`"
+ }
+ assert {
+ condition = github_dependabot_organization_secret.dependabot_secret["dependabot_secret3"].visibility == "private"
+ error_message = "The dependabot secret visibility is incorrect. Expected `private`, got `${github_dependabot_organization_secret.dependabot_secret["dependabot_secret3"].visibility}`"
+ }
+ assert {
+ condition = length(github_dependabot_organization_secret.dependabot_secret["dependabot_secret3"].selected_repository_ids) == 0
+ error_message = "The dependabot secret selected repository ids is incorrect. Expected `0`, got `${length(github_dependabot_organization_secret.dependabot_secret["dependabot_secret3"].selected_repository_ids)}`"
+ }
+}
diff --git a/modules/organization/settings.tf b/modules/organization/settings.tf
index ebd9bc0..74370c0 100644
--- a/modules/organization/settings.tf
+++ b/modules/organization/settings.tf
@@ -25,7 +25,7 @@ resource "github_organization_settings" "organization_settings" {
members_can_create_public_pages = var.github_organization_pages_settings.members_can_create_public
members_can_create_private_pages = var.github_organization_pages_settings.members_can_create_private
- #Oranization Repository settings
+ #Organization Repository settings
members_can_create_repositories = local.members_can_create_repositories
members_can_create_public_repositories = var.github_organization_repository_settings.members_can_create_public
members_can_create_internal_repositories = var.github_organization_repository_settings.members_can_create_internal
diff --git a/modules/organization/settings.tftest.hcl b/modules/organization/settings.tftest.hcl
new file mode 100644
index 0000000..4c079b7
--- /dev/null
+++ b/modules/organization/settings.tftest.hcl
@@ -0,0 +1,134 @@
+mock_provider "github" {}
+
+variables {
+ github_organization_billing_email = "org_billing_email@focisolutions.com"
+ github_organization_email = "org_email@focisolutions.com"
+ github_organization_blog = "org_blog"
+ github_organization_location = "org_location"
+ github_organization_requires_web_commit_signing = false
+
+ github_organization_enable_ghas = true
+ github_organization_enable_dependabot_alerts = true
+ github_organization_enable_dependabot_updates = true
+ github_organization_enable_dependancy_graph = true
+ github_organization_enable_secret_scanning = true
+ github_organization_enable_secret_scanning_push_protection = true
+
+ github_organization_pages_settings = {
+ members_can_create_public = true
+ members_can_create_private = true
+ }
+
+ github_organization_repository_settings = {
+ members_can_create_public = true
+ members_can_create_internal = true
+ members_can_create_private = true
+ }
+
+ custom_repository_roles = {
+ custom_role1 = {
+ description = "Custom role 1"
+ base_role = "read"
+ permissions = ["pull", "push"]
+ }
+ }
+}
+
+run "organization_settings_test" {
+ command = apply
+
+ assert {
+ condition = github_organization_settings.organization_settings.billing_email == var.github_organization_billing_email
+ error_message = "The billing email is not set correctly. Expected: ${var.github_organization_billing_email}, got: ${github_organization_settings.organization_settings.billing_email}"
+ }
+ assert {
+ condition = github_organization_settings.organization_settings.email == var.github_organization_email
+ error_message = "The email is not set correctly. Expected: ${var.github_organization_email}, got: ${github_organization_settings.organization_settings.email}"
+ }
+ assert {
+ condition = github_organization_settings.organization_settings.blog == var.github_organization_blog
+ error_message = "The blog is not set correctly. Expected: ${var.github_organization_blog}, got: ${github_organization_settings.organization_settings.blog}"
+ }
+ assert {
+ condition = github_organization_settings.organization_settings.location == var.github_organization_location
+ error_message = "The location is not set correctly. Expected: ${var.github_organization_location}, got: ${github_organization_settings.organization_settings.location}"
+ }
+ assert {
+ condition = github_organization_settings.organization_settings.web_commit_signoff_required == var.github_organization_requires_web_commit_signing
+ error_message = "The web commit signoff required is not set correctly. Expected: ${var.github_organization_requires_web_commit_signing}, got: ${github_organization_settings.organization_settings.web_commit_signoff_required}"
+ }
+ assert {
+ condition = github_organization_settings.organization_settings.has_organization_projects == true
+ error_message = "The organization projects are not enabled. Expected: true, got: ${github_organization_settings.organization_settings.has_organization_projects}"
+ }
+ assert {
+ condition = github_organization_settings.organization_settings.has_repository_projects == true
+ error_message = "The repository projects are not enabled. Expected: true, got: ${github_organization_settings.organization_settings.has_repository_projects}"
+ }
+
+ # Github advance security, dependabot, and secret scanning
+ assert {
+ condition = github_organization_settings.organization_settings.advanced_security_enabled_for_new_repositories == var.github_organization_enable_ghas
+ error_message = "The advance security is not set correctly. Expected: ${var.github_organization_enable_ghas}, got: ${github_organization_settings.organization_settings.advanced_security_enabled_for_new_repositories}"
+ }
+ assert {
+ condition = github_organization_settings.organization_settings.dependabot_alerts_enabled_for_new_repositories == var.github_organization_enable_dependabot_alerts
+ error_message = "The dependabot alerts are not enabled. Expected: ${var.github_organization_enable_dependabot_alerts}, got: ${github_organization_settings.organization_settings.dependabot_alerts_enabled_for_new_repositories}"
+ }
+ assert {
+ condition = github_organization_settings.organization_settings.dependabot_security_updates_enabled_for_new_repositories == var.github_organization_enable_dependabot_updates
+ error_message = "The dependabot security updates are not enabled. Expected: ${var.github_organization_enable_dependabot_updates}, got: ${github_organization_settings.organization_settings.dependabot_security_updates_enabled_for_new_repositories}"
+ }
+ assert {
+ condition = github_organization_settings.organization_settings.dependency_graph_enabled_for_new_repositories == var.github_organization_enable_dependancy_graph
+ error_message = "The dependency graph is not enabled. Expected: ${var.github_organization_enable_dependancy_graph}, got: ${github_organization_settings.organization_settings.dependency_graph_enabled_for_new_repositories}"
+ }
+ assert {
+ condition = github_organization_settings.organization_settings.secret_scanning_enabled_for_new_repositories == var.github_organization_enable_secret_scanning
+ error_message = "The secret scanning is not enabled. Expected: ${var.github_organization_enable_secret_scanning}, got: ${github_organization_settings.organization_settings.secret_scanning_enabled_for_new_repositories}"
+ }
+ assert {
+ condition = github_organization_settings.organization_settings.secret_scanning_push_protection_enabled_for_new_repositories == var.github_organization_enable_secret_scanning_push_protection
+ error_message = "The secret scanning push protection is not enabled. Expected: ${var.github_organization_enable_secret_scanning_push_protection}, got: ${github_organization_settings.organization_settings.secret_scanning_push_protection_enabled_for_new_repositories}"
+ }
+
+ #Organization pages
+ assert {
+ condition = github_organization_settings.organization_settings.members_can_create_pages == true
+ error_message = "The members can create pages is not enabled. Expected: true, got: ${github_organization_settings.organization_settings.members_can_create_pages}"
+ }
+ assert {
+ condition = github_organization_settings.organization_settings.members_can_create_public_pages == var.github_organization_pages_settings.members_can_create_public
+ error_message = "The members can create public pages is not enabled. Expected: ${var.github_organization_pages_settings.members_can_create_public}, got: ${github_organization_settings.organization_settings.members_can_create_public_pages}"
+ }
+ assert {
+ condition = github_organization_settings.organization_settings.members_can_create_private_pages == var.github_organization_pages_settings.members_can_create_private
+ error_message = "The members can create private pages is not enabled. Expected: ${var.github_organization_pages_settings.members_can_create_private}, got: ${github_organization_settings.organization_settings.members_can_create_private_pages}"
+ }
+
+ #Organization Repository settings
+ assert {
+ condition = github_organization_settings.organization_settings.members_can_create_repositories == true
+ error_message = "The members can create repositories is not enabled. Expected: true, got: ${github_organization_settings.organization_settings.members_can_create_repositories}"
+ }
+ assert {
+ condition = github_organization_settings.organization_settings.members_can_create_public_repositories == var.github_organization_repository_settings.members_can_create_public
+ error_message = "The members can create public repositories is not enabled. Expected: ${var.github_organization_repository_settings.members_can_create_public}, got: ${github_organization_settings.organization_settings.members_can_create_public_repositories}"
+ }
+ assert {
+ condition = github_organization_settings.organization_settings.members_can_create_internal_repositories == var.github_organization_repository_settings.members_can_create_internal
+ error_message = "The members can create internal repositories is not enabled. Expected: ${var.github_organization_repository_settings.members_can_create_internal}, got: ${github_organization_settings.organization_settings.members_can_create_internal_repositories}"
+ }
+ assert {
+ condition = github_organization_settings.organization_settings.members_can_create_private_repositories == var.github_organization_repository_settings.members_can_create_private
+ error_message = "The members can create private repositories is not enabled. Expected: ${var.github_organization_repository_settings.members_can_create_private}, got: ${github_organization_settings.organization_settings.members_can_create_private_repositories}"
+ }
+ assert {
+ condition = github_organization_settings.organization_settings.default_repository_permission == "none"
+ error_message = "The default repository permission is not set correctly. Expected: none, got: ${github_organization_settings.organization_settings.default_repository_permission}"
+ }
+ assert {
+ condition = github_organization_settings.organization_settings.members_can_fork_private_repositories == false
+ error_message = "The members can fork private repositories is not set correctly. Expected: false, got: ${github_organization_settings.organization_settings.members_can_fork_private_repositories}"
+ }
+}
diff --git a/modules/private_repository/README.md b/modules/private_repository/README.md
index bec2428..dd33f99 100644
--- a/modules/private_repository/README.md
+++ b/modules/private_repository/README.md
@@ -35,22 +35,22 @@ No resources.
| [dependabot\_secrets](#input\_dependabot\_secrets) | An (Optional) map of Dependabot secrets to create for this repository. The key is the name of the secret and the value is the encrypted value. | `map(string)` | `{}` | no |
| [dependabot\_security\_updates](#input\_dependabot\_security\_updates) | Enables dependabot security updates. Only works when `has_vulnerability_alerts` is set because that is required to enable dependabot for the repository. | `bool` | `true` | no |
| [description](#input\_description) | The description to give to the repository. Defaults to `""` | `string` | `""` | no |
-| [environments](#input\_environments) | Environments to create for the repository. | map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
})) | `{}` | no |
+| [environments](#input\_environments) | Environments to create for the repository. | map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
})) | `{}` | no |
| [has\_ghas\_license](#input\_has\_ghas\_license) | If the organization owning the repository has a GitHub Advanced Security license or not. Defaults to false. | `bool` | `false` | no |
| [homepage](#input\_homepage) | The homepage for the repository | `string` | `""` | no |
| [license\_template](#input\_license\_template) | The (Optional) license template to use for the repository | `string` | `null` | no |
| [merge\_commit\_message](#input\_merge\_commit\_message) | (Optional) Can be `PR_BODY`, `PR_TITLE`, or `BLANK` for a default merge commit message. Applicable only if allow\_merge\_commit is `true`. | `string` | `"PR_TITLE"` | no |
| [merge\_commit\_title](#input\_merge\_commit\_title) | (Optional) Can be `PR_TITLE` or `MERGE_MESSAGE` for a default merge commit title. Applicable only if allow\_merge\_commit is `true`. | `string` | `"MERGE_MESSAGE"` | no |
| [name](#input\_name) | The name of the repository to create/import. | `string` | n/a | yes |
-| [pages](#input\_pages) | The (Optional) configuration for GitHub Pages for the repository | object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
}) | `null` | no |
-| [protected\_branches](#input\_protected\_branches) | A list of ref names or patterns that should be protected. Defaults `["main"]` | `list(string)` | [| no | +| [pages](#input\_pages) | The (Optional) configuration for GitHub Pages for the repository |
"main"
]
object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
}) | `null` | no |
+| [protected\_branches](#input\_protected\_branches) | A list of ref names or patterns that should be protected. Defaults `["main"]` | `list(string)` | [| no | | [repository\_team\_permissions](#input\_repository\_team\_permissions) | A map where the keys are github team slugs and the value is the permissions the team should have in the repository | `map(string)` | n/a | yes | | [repository\_user\_permissions](#input\_repository\_user\_permissions) | A map where the keys are github usernames and the value is the permissions the user should have in the repository | `map(string)` | n/a | yes | | [requires\_web\_commit\_signing](#input\_requires\_web\_commit\_signing) | If set commit signatures are required for commits to the organization. Defaults to `false`. | `bool` | `false` | no | -| [rulesets](#input\_rulesets) | n/a |
"main"
]
map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_deployment_environments = optional(list(string))
})
target = string
enforcement = string
})) | `{}` | no |
+| [rulesets](#input\_rulesets) | n/a | map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_deployment_environments = optional(list(string))
})
target = string
enforcement = string
})) | `{}` | no |
| [squash\_merge\_commit\_message](#input\_squash\_merge\_commit\_message) | (Optional) Can be `PR_BODY`, `COMMIT_MESSAGES`, or `BLANK` for a default squash merge commit message. Applicable only if allow\_squash\_merge is `true`. | `string` | `"PR_BODY"` | no |
| [squash\_merge\_commit\_title](#input\_squash\_merge\_commit\_title) | (Optional) Can be `PR_TITLE` or `COMMIT_OR_PR_TITLE` for a default squash merge commit title. Applicable only if allow\_squash\_merge is `true`. | `string` | `"PR_TITLE"` | no |
-| [template\_repository](#input\_template\_repository) | A (Optional) list of template repositories to use for the repository | object({
owner = string
repository = string
include_all_branches = bool
}) | `null` | no |
+| [template\_repository](#input\_template\_repository) | A (Optional) list of template repositories to use for the repository | object({
owner = string
repository = string
include_all_branches = bool
}) | `null` | no |
| [topics](#input\_topics) | The topics to apply to the repository | `list(string)` | `[]` | no |
## Outputs
diff --git a/modules/private_repository/repository.tftest.hcl b/modules/private_repository/repository.tftest.hcl
new file mode 100644
index 0000000..ef1650c
--- /dev/null
+++ b/modules/private_repository/repository.tftest.hcl
@@ -0,0 +1,65 @@
+mock_provider "github" {}
+
+variables {
+ name = "github-foundations-modules"
+ description = "A collection of terraform modules used in the Github Foundations framework."
+ visibility = "public"
+ has_downloads = true
+ has_issues = true
+ has_projects = true
+ has_wiki = true
+ has_discussions = true
+ has_vulnerability_alerts = true
+ topics = ["terraform", "github", "foundations"]
+ homepage = "myhomepage"
+ delete_head_on_merge = false
+ allow_auto_merge = true
+ allow_squash_merge = false
+ squash_merge_commit_message = "COMMIT_MESSAGES"
+ squash_merge_commit_title = "COMMIT_OR_PR_TITLE"
+ allow_merge_commit = false
+ merge_commit_message = "PR_BODY"
+ merge_commit_title = "PR_TITLE"
+ allow_rebase_merge = true
+ requires_web_commit_signing = false
+ license_template = "mit"
+ dependabot_security_updates = true
+ advance_security = true
+ secret_scanning = true
+ secret_scanning_on_push = true
+
+ default_branch = "main"
+ protected_branches = ["main", "develop"]
+
+ template_repository = {
+ owner = "owner"
+ repository = "template_repository"
+ include_all_branches = true
+ }
+
+ pages = {
+ source = {
+ branch = "main"
+ path = "path"
+ }
+ cname = "cname"
+ }
+
+ repository_team_permissions = {
+ repo_team1 = "push"
+ repo_team2 = "admin"
+ }
+ repository_user_permissions = {
+ user1 = "push"
+ user2 = "admin"
+ }
+}
+
+run "create_test" {
+ command = apply
+
+ assert {
+ condition = module.repository_base.id != null
+ error_message = "The repository was not created"
+ }
+}
diff --git a/modules/public_repository/README.md b/modules/public_repository/README.md
index 1c4157a..5fc5ef1 100644
--- a/modules/public_repository/README.md
+++ b/modules/public_repository/README.md
@@ -35,21 +35,21 @@ No resources.
| [dependabot\_secrets](#input\_dependabot\_secrets) | An (Optional) map of Dependabot secrets to create for this repository. The key is the name of the secret and the value is the encrypted value. | `map(string)` | `{}` | no |
| [dependabot\_security\_updates](#input\_dependabot\_security\_updates) | Enables dependabot security updates. Only works when `has_vulnerability_alerts` is set because that is required to enable dependabot for the repository. | `bool` | `true` | no |
| [description](#input\_description) | The description to give to the repository. Defaults to `""` | `string` | `""` | no |
-| [environments](#input\_environments) | Environments to create for the repository. | map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
})) | `{}` | no |
+| [environments](#input\_environments) | Environments to create for the repository. | map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
})) | `{}` | no |
| [homepage](#input\_homepage) | The homepage for the repository | `string` | `""` | no |
| [license\_template](#input\_license\_template) | The (Optional) license template to apply to the repository | `string` | `null` | no |
| [merge\_commit\_message](#input\_merge\_commit\_message) | (Optional) Can be `PR_BODY`, `PR_TITLE`, or `BLANK` for a default merge commit message. Applicable only if allow\_merge\_commit is `true`. | `string` | `"PR_TITLE"` | no |
| [merge\_commit\_title](#input\_merge\_commit\_title) | (Optional) Can be `PR_TITLE` or `MERGE_MESSAGE` for a default merge commit title. Applicable only if allow\_merge\_commit is `true`. | `string` | `"MERGE_MESSAGE"` | no |
| [name](#input\_name) | The name of the repository to create/import. | `string` | n/a | yes |
-| [pages](#input\_pages) | The (Optional) configuration for GitHub Pages for the repository | object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
}) | `null` | no |
-| [protected\_branches](#input\_protected\_branches) | A list of ref names or patterns that should be protected. Defaults `["main"]` | `list(string)` | [| no | +| [pages](#input\_pages) | The (Optional) configuration for GitHub Pages for the repository |
"main"
]
object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
}) | `null` | no |
+| [protected\_branches](#input\_protected\_branches) | A list of ref names or patterns that should be protected. Defaults `["main"]` | `list(string)` | [| no | | [repository\_team\_permissions](#input\_repository\_team\_permissions) | A map where the keys are github team slugs and the value is the permissions the team should have in the repository | `map(string)` | n/a | yes | | [repository\_user\_permissions](#input\_repository\_user\_permissions) | A map where the keys are github usernames and the value is the permissions the user should have in the repository | `map(string)` | n/a | yes | | [requires\_web\_commit\_signing](#input\_requires\_web\_commit\_signing) | If set commit signatures are required for commits to the organization. Defaults to `false`. | `bool` | `false` | no | -| [rulesets](#input\_rulesets) | n/a |
"main"
]
map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_deployment_environments = optional(list(string))
})
target = string
enforcement = string
})) | `{}` | no |
+| [rulesets](#input\_rulesets) | n/a | map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_deployment_environments = optional(list(string))
})
target = string
enforcement = string
})) | `{}` | no |
| [squash\_merge\_commit\_message](#input\_squash\_merge\_commit\_message) | (Optional) Can be `PR_BODY`, `COMMIT_MESSAGES`, or `BLANK` for a default squash merge commit message. Applicable only if allow\_squash\_merge is `true`. | `string` | `"PR_BODY"` | no |
| [squash\_merge\_commit\_title](#input\_squash\_merge\_commit\_title) | (Optional) Can be `PR_TITLE` or `COMMIT_OR_PR_TITLE` for a default squash merge commit title. Applicable only if allow\_squash\_merge is `true`. | `string` | `"PR_TITLE"` | no |
-| [template\_repository](#input\_template\_repository) | A (Optional) list of template repositories to use for the repository | object({
owner = string
repository = string
include_all_branches = bool
}) | `null` | no |
+| [template\_repository](#input\_template\_repository) | A (Optional) list of template repositories to use for the repository | object({
owner = string
repository = string
include_all_branches = bool
}) | `null` | no |
| [topics](#input\_topics) | The topics to apply to the repository | `list(string)` | `[]` | no |
## Outputs
diff --git a/modules/public_repository/repository.tftest.hcl b/modules/public_repository/repository.tftest.hcl
new file mode 100644
index 0000000..7ea88a3
--- /dev/null
+++ b/modules/public_repository/repository.tftest.hcl
@@ -0,0 +1,65 @@
+mock_provider "github" {}
+
+variables {
+ name = "github-foundations-modules"
+ description = "A collection of terraform modules used in the Github Foundations framework."
+ visibility = "public"
+ has_downloads = true
+ has_issues = true
+ has_projects = true
+ has_wiki = true
+ has_discussions = true
+ has_vulnerability_alerts = true
+ topics = ["terraform", "github", "foundations"]
+ homepage = "myhomepage"
+ delete_head_on_merge = false
+ allow_auto_merge = true
+ allow_squash_merge = false
+ squash_merge_commit_message = "COMMIT_MESSAGES"
+ squash_merge_commit_title = "COMMIT_OR_PR_TITLE"
+ allow_merge_commit = false
+ merge_commit_message = "PR_BODY"
+ merge_commit_title = "PR_TITLE"
+ allow_rebase_merge = true
+ requires_web_commit_signing = false
+ license_template = "mit"
+ dependabot_security_updates = true
+ advance_security = true
+ secret_scanning = true
+ secret_scanning_on_push = true
+
+ default_branch = "main"
+ protected_branches = ["main", "develop"]
+
+ template_repository = {
+ owner = "owner"
+ repository = "template_repository"
+ include_all_branches = true
+ }
+
+ pages = {
+ source = {
+ branch = "main"
+ path = "path"
+ }
+ cname = "cname"
+ }
+
+ repository_team_permissions = {
+ "repo_team1" = "push"
+ "repo_team2" = "admin"
+ }
+ repository_user_permissions = {
+ "user1" = "push"
+ "user2" = "admin"
+ }
+}
+
+run "create_test" {
+ command = apply
+
+ assert {
+ condition = module.repository_base.id != null
+ error_message = "The repository was not created"
+ }
+}
diff --git a/modules/repository_base/README.md b/modules/repository_base/README.md
index 3f039aa..494de8d 100644
--- a/modules/repository_base/README.md
+++ b/modules/repository_base/README.md
@@ -52,7 +52,7 @@
| [dependabot\_secrets](#input\_dependabot\_secrets) | An (Optional) map of Dependabot secrets to create for this repository. The key is the name of the secret and the value is the encrypted value. | `map(string)` | `{}` | no |
| [dependabot\_security\_updates](#input\_dependabot\_security\_updates) | Enables dependabot security updates. Only works when `has_vulnerability_alerts` is set because that is required to enable dependabot for the repository. | `bool` | `true` | no |
| [description](#input\_description) | The description to give to the repository. Defaults to `""` | `string` | `""` | no |
-| [environments](#input\_environments) | An (Optional) map of environments to create for the repository. The key is the name of the environment and the value is the environment configuration. | map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
})) | `{}` | no |
+| [environments](#input\_environments) | An (Optional) map of environments to create for the repository. The key is the name of the environment and the value is the environment configuration. | map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
})) | `{}` | no |
| [has\_discussions](#input\_has\_discussions) | Enables Github Discussions. | `bool` | `true` | no |
| [has\_downloads](#input\_has\_downloads) | Enables downloads for the repository | `bool` | `false` | no |
| [has\_issues](#input\_has\_issues) | Enables Github Issues for the repository | `bool` | `true` | no |
@@ -64,17 +64,17 @@
| [merge\_commit\_message](#input\_merge\_commit\_message) | (Optional) Can be `PR_BODY`, `PR_TITLE`, or `BLANK` for a default merge commit message. Applicable only if allow\_merge\_commit is `true`. | `string` | `"PR_TITLE"` | no |
| [merge\_commit\_title](#input\_merge\_commit\_title) | (Optional) Can be `PR_TITLE` or `MERGE_MESSAGE` for a default merge commit title. Applicable only if allow\_merge\_commit is `true`. | `string` | `"MERGE_MESSAGE"` | no |
| [name](#input\_name) | The name of the repository to create/import. | `string` | n/a | yes |
-| [pages](#input\_pages) | The (Optional) configuration for GitHub Pages for the repository | object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
}) | `null` | no |
-| [protected\_branches](#input\_protected\_branches) | A list of ref names or patterns that should be protected. Setting to `[]` means no protection. Defaults `["~DEFAULT_BRANCH"]` | `list(string)` | [| no | +| [pages](#input\_pages) | The (Optional) configuration for GitHub Pages for the repository |
"~DEFAULT_BRANCH"
]
object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
}) | `null` | no |
+| [protected\_branches](#input\_protected\_branches) | A list of ref names or patterns that should be protected. Setting to `[]` means no protection. Defaults `["~DEFAULT_BRANCH"]` | `list(string)` | [| no | | [repository\_team\_permissions](#input\_repository\_team\_permissions) | A map where the keys are github team slugs and the value is the permissions the team should have in the repository | `map(string)` | n/a | yes | | [repository\_user\_permissions](#input\_repository\_user\_permissions) | A map where the keys are github usernames and the value is the permissions the user should have in the repository | `map(string)` | n/a | yes | | [requires\_web\_commit\_signing](#input\_requires\_web\_commit\_signing) | If set commit signatures are required for commits to the organization. Defaults to `false`. | `bool` | `false` | no | -| [rulesets](#input\_rulesets) | n/a |
"~DEFAULT_BRANCH"
]
map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_deployment_environments = optional(list(string))
})
target = string
enforcement = string
})) | `{}` | no |
+| [rulesets](#input\_rulesets) | n/a | map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_deployment_environments = optional(list(string))
})
target = string
enforcement = string
})) | `{}` | no |
| [secret\_scanning](#input\_secret\_scanning) | Enables secret scanning for the repository. If repository is private `advance_security` must also be enabled. | `bool` | `true` | no |
| [secret\_scanning\_on\_push](#input\_secret\_scanning\_on\_push) | Enables secret scanning push protection for the repository. If repository is private `advance_security` must also be enabled. | `bool` | `true` | no |
| [squash\_merge\_commit\_message](#input\_squash\_merge\_commit\_message) | (Optional) Can be `PR_BODY`, `COMMIT_MESSAGES`, or `BLANK` for a default squash merge commit message. Applicable only if allow\_squash\_merge is `true`. | `string` | `"PR_BODY"` | no |
| [squash\_merge\_commit\_title](#input\_squash\_merge\_commit\_title) | (Optional) Can be `PR_TITLE` or `COMMIT_OR_PR_TITLE` for a default squash merge commit title. Applicable only if allow\_squash\_merge is `true`. | `string` | `"PR_TITLE"` | no |
-| [template\_repository](#input\_template\_repository) | A (Optional) list of template repositories to use for the repository | object({
owner = string
repository = string
include_all_branches = bool
}) | `null` | no |
+| [template\_repository](#input\_template\_repository) | A (Optional) list of template repositories to use for the repository | object({
owner = string
repository = string
include_all_branches = bool
}) | `null` | no |
| [topics](#input\_topics) | The topics to apply to the repository | `list(string)` | `[]` | no |
| [visibility](#input\_visibility) | Sets the visibility property of a repository. Defaults to "private" | `string` | `"private"` | no |
diff --git a/modules/repository_base/collaborators.tftest.hcl b/modules/repository_base/collaborators.tftest.hcl
new file mode 100644
index 0000000..43f2886
--- /dev/null
+++ b/modules/repository_base/collaborators.tftest.hcl
@@ -0,0 +1,32 @@
+mock_provider "github" {}
+
+variables {
+ name = "github-foundations-modules"
+ description = "A collection of terraform modules used in the Github Foundations framework."
+
+ repository_team_permissions = {
+ "repo_team1" = "push"
+ "repo_team2" = "admin"
+ }
+ repository_user_permissions = {
+ "user1" = "push"
+ "user2" = "admin"
+ }
+}
+
+run "collaborators_test" {
+ command = apply
+
+ assert {
+ condition = github_repository_collaborators.collaborators.repository == var.name
+ error_message = "The repository id value is incorrect. Expected ${var.name}, got ${github_repository_collaborators.collaborators.repository}"
+ }
+ assert {
+ condition = length(github_repository_collaborators.collaborators.team) == length(var.repository_team_permissions)
+ error_message = "The number of teams is incorrect. Expected ${length(var.repository_team_permissions)}, got ${length(github_repository_collaborators.collaborators.team)}"
+ }
+ assert {
+ condition = length(github_repository_collaborators.collaborators.user) == length(var.repository_user_permissions)
+ error_message = "The number of users is incorrect. Expected ${length(var.repository_user_permissions)}, got ${length(github_repository_collaborators.collaborators.user)}"
+ }
+}
diff --git a/modules/repository_base/environments.tftest.hcl b/modules/repository_base/environments.tftest.hcl
new file mode 100644
index 0000000..e60edf5
--- /dev/null
+++ b/modules/repository_base/environments.tftest.hcl
@@ -0,0 +1,95 @@
+mock_provider "github" {}
+
+variables {
+ name = "github-foundations-modules"
+ description = "A collection of terraform modules used in the Github Foundations framework."
+
+ repository_team_permissions = {
+ "repo_team1" = "push"
+ "repo_team2" = "admin"
+ }
+ repository_user_permissions = {
+ "user1" = "push"
+ "user2" = "admin"
+ }
+ environments = {
+ "env1" = {
+ wait_timer = 10
+ can_admins_bypass = true
+ prevent_self_review = false
+ action_secrets = {
+ "action_secret1" = "dmFsdWUxCg==" # base64 encoded
+ }
+ reviewers = {
+ teams = [111, 222]
+ users = [55, 66]
+ }
+ deployment_branch_policy = {
+ protected_branches = true
+ custom_branch_policies = false
+ branch_patterns = ["main"]
+ }
+ }
+ }
+
+}
+
+run "create_environment_test" {
+ command = apply
+
+ assert {
+ condition = github_repository_environment.environment["env1"].repository == var.name
+ error_message = "The repository id value is incorrect. Expected ${var.name}, got ${github_repository_environment.environment["env1"].repository}"
+ }
+ assert {
+ condition = github_repository_environment.environment["env1"].environment == "env1"
+ error_message = "The environment name value is incorrect. Expected env1, got ${github_repository_environment.environment["env1"].environment}"
+ }
+ assert {
+ condition = github_repository_environment.environment["env1"].wait_timer == var.environments["env1"].wait_timer
+ error_message = "The wait timer value is incorrect. Expected ${var.environments["env1"].wait_timer}, got ${github_repository_environment.environment["env1"].wait_timer}"
+ }
+ assert {
+ condition = github_repository_environment.environment["env1"].can_admins_bypass == var.environments["env1"].can_admins_bypass
+ error_message = "The can admins bypass value is incorrect. Expected ${var.environments["env1"].can_admins_bypass}, got ${github_repository_environment.environment["env1"].can_admins_bypass}"
+ }
+ assert {
+ condition = github_repository_environment.environment["env1"].prevent_self_review == var.environments["env1"].prevent_self_review
+ error_message = "The prevent self review value is incorrect. Expected ${var.environments["env1"].prevent_self_review}, got ${github_repository_environment.environment["env1"].prevent_self_review}"
+ }
+ assert {
+ condition = length(github_repository_environment.environment["env1"].reviewers["0"].teams) == length(var.environments["env1"].reviewers.teams)
+ error_message = "The reviewers teams value is incorrect. Expected ${length(var.environments["env1"].reviewers.teams)}, got ${length(github_repository_environment.environment["env1"].reviewers["0"].teams)}"
+ }
+ assert {
+ condition = length(github_repository_environment.environment["env1"].reviewers["0"].users) == length(var.environments["env1"].reviewers.users)
+ error_message = "The reviewers users value is incorrect. Expected ${length(var.environments["env1"].reviewers.users)}, got ${length(github_repository_environment.environment["env1"].reviewers["0"].users)}"
+ }
+ assert {
+ condition = github_repository_environment.environment["env1"].deployment_branch_policy["0"].protected_branches == var.environments["env1"].deployment_branch_policy.protected_branches
+ error_message = "The deployment branch policy protected branches value is incorrect. Expected ${var.environments["env1"].deployment_branch_policy.protected_branches}, got ${github_repository_environment.environment["env1"].deployment_branch_policy["0"].protected_branches}"
+ }
+ assert {
+ condition = github_repository_environment.environment["env1"].deployment_branch_policy["0"].custom_branch_policies == var.environments["env1"].deployment_branch_policy.custom_branch_policies
+ error_message = "The deployment branch policy custom branch policies value is incorrect. Expected ${var.environments["env1"].deployment_branch_policy.custom_branch_policies}, got ${github_repository_environment.environment["env1"].deployment_branch_policy["0"].custom_branch_policies}"
+ }
+ assert {
+ condition = length(github_repository_environment_deployment_policy.deployment_policy) == length(var.environments["env1"].deployment_branch_policy.branch_patterns)
+ error_message = "The deployment policy length is incorrect. Expected ${length(var.environments["env1"].deployment_branch_policy.branch_patterns)}, got ${length(github_repository_environment_deployment_policy.deployment_policy)}"
+ }
+}
+
+run "deployment_policy_test" {
+ assert {
+ condition = github_repository_environment_deployment_policy.deployment_policy["env1:main"].repository == var.name
+ error_message = "The repository id value is incorrect. Expected ${var.name}, got ${github_repository_environment_deployment_policy.deployment_policy["env1:main"].repository}"
+ }
+ assert {
+ condition = github_repository_environment_deployment_policy.deployment_policy["env1:main"].environment == "env1"
+ error_message = "The environment name value is incorrect. Expected env1, got ${github_repository_environment_deployment_policy.deployment_policy["env1:main"].environment}"
+ }
+ assert {
+ condition = github_repository_environment_deployment_policy.deployment_policy["env1:main"].branch_pattern == "main"
+ error_message = "The branch pattern value is incorrect. Expected main, got ${github_repository_environment_deployment_policy.deployment_policy["env1:main"].branch_pattern}"
+ }
+}
diff --git a/modules/repository_base/repository.tftest.hcl b/modules/repository_base/repository.tftest.hcl
new file mode 100644
index 0000000..8c77e5b
--- /dev/null
+++ b/modules/repository_base/repository.tftest.hcl
@@ -0,0 +1,280 @@
+mock_provider "github" {}
+
+variables {
+ name = "github-foundations-modules"
+ description = "A collection of terraform modules used in the Github Foundations framework."
+ visibility = "public"
+ has_downloads = true
+ has_issues = true
+ has_projects = true
+ has_wiki = true
+ has_discussions = true
+ has_vulnerability_alerts = true
+ topics = ["terraform", "github", "foundations"]
+ homepage = "myhomepage"
+ delete_head_on_merge = false
+ allow_auto_merge = true
+ allow_squash_merge = false
+ squash_merge_commit_message = "COMMIT_MESSAGES"
+ squash_merge_commit_title = "COMMIT_OR_PR_TITLE"
+ allow_merge_commit = false
+ merge_commit_message = "PR_BODY"
+ merge_commit_title = "PR_TITLE"
+ allow_rebase_merge = true
+ requires_web_commit_signing = false
+ license_template = "mit"
+ dependabot_security_updates = true
+ advance_security = true
+ secret_scanning = true
+ secret_scanning_on_push = true
+
+ default_branch = "main"
+ protected_branches = ["main", "develop"]
+
+ template_repository = {
+ owner = "owner"
+ repository = "template_repository"
+ include_all_branches = true
+ }
+
+ pages = {
+ source = {
+ branch = "main"
+ path = "path"
+ }
+ cname = "cname"
+ }
+
+ repository_team_permissions = {
+ "repo_team1" = "push"
+ "repo_team2" = "admin"
+ }
+ repository_user_permissions = {
+ "user1" = "push"
+ "user2" = "admin"
+ }
+}
+
+run "repository_test" {
+ assert {
+ condition = github_repository.repository.name == var.name
+ error_message = "Repository name does not match. Expected: ${var.name}, Actual: ${github_repository.repository.name}"
+ }
+ assert {
+ condition = github_repository.repository.description == var.description
+ error_message = "Repository description does not match. Expected: ${var.description}, Actual: ${github_repository.repository.description}"
+ }
+ assert {
+ condition = github_repository.repository.visibility == var.visibility
+ error_message = "Repository visibility does not match. Expected: ${var.visibility}, Actual: ${github_repository.repository.visibility}"
+ }
+ assert {
+ condition = github_repository.repository.auto_init == true
+ error_message = "Repository auto_init does not match. Expected: true, Actual: ${github_repository.repository.auto_init}"
+ }
+ assert {
+ condition = github_repository.repository.archive_on_destroy == false
+ error_message = "Repository archive_on_destroy does not match. Expected: false, Actual: ${github_repository.repository.archive_on_destroy}"
+ }
+ assert {
+ condition = github_repository.repository.has_downloads == var.has_downloads
+ error_message = "Repository has_downloads does not match. Expected: ${var.has_downloads}, Actual: ${github_repository.repository.has_downloads}"
+ }
+ assert {
+ condition = github_repository.repository.has_issues == var.has_issues
+ error_message = "Repository has_issues does not match. Expected: ${var.has_issues}, Actual: ${github_repository.repository.has_issues}"
+ }
+ assert {
+ condition = github_repository.repository.has_projects == var.has_projects
+ error_message = "Repository has_projects does not match. Expected: ${var.has_projects}, Actual: ${github_repository.repository.has_projects}"
+ }
+ assert {
+ condition = github_repository.repository.has_wiki == var.has_wiki
+ error_message = "Repository has_wiki does not match. Expected: ${var.has_wiki}, Actual: ${github_repository.repository.has_wiki}"
+ }
+ assert {
+ condition = github_repository.repository.has_discussions == var.has_discussions
+ error_message = "Repository has_discussions does not match. Expected: ${var.has_discussions}, Actual: ${github_repository.repository.has_discussions}"
+ }
+ assert {
+ condition = github_repository.repository.vulnerability_alerts == var.has_vulnerability_alerts
+ error_message = "Repository vulnerability_alerts does not match. Expected: ${var.has_vulnerability_alerts}, Actual: ${github_repository.repository.vulnerability_alerts}"
+ }
+ assert {
+ condition = length(github_repository.repository.topics) == length(var.topics)
+ error_message = "Repository topics length does not match. Expected: ${length(var.topics)}, Actual: ${length(github_repository.repository.topics)}"
+ }
+ assert {
+ condition = github_repository.repository.homepage_url == var.homepage
+ error_message = "Repository homepage does not match. Expected: ${var.homepage}, Actual: ${github_repository.repository.homepage_url}"
+ }
+ assert {
+ condition = github_repository.repository.delete_branch_on_merge == var.delete_head_on_merge
+ error_message = "Repository delete_branch_on_merge does not match. Expected: ${var.delete_head_on_merge}, Actual: ${github_repository.repository.delete_branch_on_merge}"
+ }
+ assert {
+ condition = github_repository.repository.allow_auto_merge == var.allow_auto_merge
+ error_message = "Repository allow_auto_merge does not match. Expected: ${var.allow_auto_merge}, Actual: ${github_repository.repository.allow_auto_merge}"
+ }
+ assert {
+ condition = github_repository.repository.allow_squash_merge == var.allow_squash_merge
+ error_message = "Repository allow_squash_merge does not match. Expected: ${var.allow_squash_merge}, Actual: ${github_repository.repository.allow_squash_merge}"
+ }
+ assert {
+ condition = github_repository.repository.squash_merge_commit_message == var.squash_merge_commit_message
+ error_message = "Repository squash_merge_commit_message does not match. Expected: ${var.squash_merge_commit_message}, Actual: ${github_repository.repository.squash_merge_commit_message}"
+ }
+ assert {
+ condition = github_repository.repository.squash_merge_commit_title == var.squash_merge_commit_title
+ error_message = "Repository squash_merge_commit_title does not match. Expected: ${var.squash_merge_commit_title}, Actual: ${github_repository.repository.squash_merge_commit_title}"
+ }
+ assert {
+ condition = github_repository.repository.allow_merge_commit == var.allow_merge_commit
+ error_message = "Repository allow_merge_commit does not match. Expected: ${var.allow_merge_commit}, Actual: ${github_repository.repository.allow_merge_commit}"
+ }
+ assert {
+ condition = github_repository.repository.merge_commit_message == var.merge_commit_message
+ error_message = "Repository merge_commit_message does not match. Expected: ${var.merge_commit_message}, Actual: ${github_repository.repository.merge_commit_message}"
+ }
+ assert {
+ condition = github_repository.repository.merge_commit_title == var.merge_commit_title
+ error_message = "Repository merge_commit_title does not match. Expected: ${var.merge_commit_title}, Actual: ${github_repository.repository.merge_commit_title}"
+ }
+ assert {
+ condition = github_repository.repository.allow_rebase_merge == var.allow_rebase_merge
+ error_message = "Repository allow_rebase_merge does not match. Expected: ${var.allow_rebase_merge}, Actual: ${github_repository.repository.allow_rebase_merge}"
+ }
+ assert {
+ condition = github_repository.repository.web_commit_signoff_required == var.requires_web_commit_signing
+ error_message = "Repository web_commit_signoff_required does not match. Expected: ${var.requires_web_commit_signing}, Actual: ${github_repository.repository.web_commit_signoff_required}"
+ }
+ assert {
+ condition = github_repository.repository.license_template == var.license_template
+ error_message = "Repository license_template does not match. Expected: ${var.license_template}, Actual: ${github_repository.repository.license_template}"
+ }
+ assert {
+ condition = github_repository.repository.security_and_analysis[0].advanced_security[0].status == "enabled"
+ error_message = "Repository advanced_security status does not match. Expected: enabled, Actual: ${github_repository.repository.security_and_analysis[0].advanced_security[0].status}"
+ }
+ assert {
+ condition = github_repository.repository.security_and_analysis[0].secret_scanning[0].status == "enabled"
+ error_message = "Repository secret_scanning status does not match. Expected: enabled, Actual: ${github_repository.repository.security_and_analysis[0].secret_scanning[0].status}"
+ }
+ assert {
+ condition = github_repository.repository.security_and_analysis[0].secret_scanning_push_protection[0].status == "enabled"
+ error_message = "Repository secret_scanning_push_protection status does not match. Expected: enabled, Actual: ${github_repository.repository.security_and_analysis[0].secret_scanning_push_protection[0].status}"
+ }
+ assert {
+ condition = github_repository.repository.template[0].owner == var.template_repository.owner
+ error_message = "Repository template owner does not match. Expected: ${var.template_repository.owner}, Actual: ${github_repository.repository.template[0].owner}"
+ }
+ assert {
+ condition = github_repository.repository.template[0].repository == var.template_repository.repository
+ error_message = "Repository template repository does not match. Expected: ${var.template_repository.repository}, Actual: ${github_repository.repository.template[0].repository}"
+ }
+ assert {
+ condition = github_repository.repository.template[0].include_all_branches == var.template_repository.include_all_branches
+ error_message = "Repository template include_all_branches does not match. Expected: ${var.template_repository.include_all_branches}, Actual: ${github_repository.repository.template[0].include_all_branches}"
+ }
+ assert {
+ condition = github_repository.repository.pages[0].source[0].branch == var.pages.source.branch
+ error_message = "Repository pages source branch does not match. Expected: ${var.pages.source.branch}, Actual: ${github_repository.repository.pages[0].source[0].branch}"
+ }
+ assert {
+ condition = github_repository.repository.pages[0].source[0].path == var.pages.source.path
+ error_message = "Repository pages source path does not match. Expected: ${var.pages.source.path}, Actual: ${github_repository.repository.pages[0].source[0].path}"
+ }
+ assert {
+ condition = github_repository.repository.pages[0].cname == var.pages.cname
+ error_message = "Repository pages cname does not match. Expected: ${var.pages.cname}, Actual: ${github_repository.repository.pages[0].cname}"
+ }
+}
+
+run "automated_security_fixes_test" {
+ assert {
+ condition = length(github_repository_dependabot_security_updates.automated_security_fixes) == 1
+ error_message = "Repository automated_security_fixes count does not match. Expected: 1, Actual: ${length(github_repository_dependabot_security_updates.automated_security_fixes)}"
+ }
+ assert {
+ condition = github_repository_dependabot_security_updates.automated_security_fixes[0].repository == var.name
+ error_message = "Repository automated_security_fixes repository does not match. Expected: ${var.name}, Actual: ${github_repository_dependabot_security_updates.automated_security_fixes[0].repository}"
+ }
+ assert {
+ condition = github_repository_dependabot_security_updates.automated_security_fixes[0].enabled == var.dependabot_security_updates
+ error_message = "Repository automated_security_fixes enabled does not match. Expected: ${var.dependabot_security_updates}, Actual: ${github_repository_dependabot_security_updates.automated_security_fixes[0].enabled}"
+ }
+}
+
+run "default_branch_test" {
+ assert {
+ condition = github_branch_default.default_branch.repository == var.name
+ error_message = "Repository default_branch repository does not match. Expected: ${var.name}, Actual: ${github_branch_default.default_branch.repository}"
+ }
+ assert {
+ condition = github_branch_default.default_branch.branch == var.default_branch
+ error_message = "Repository default_branch branch does not match. Expected: ${var.default_branch}, Actual: ${github_branch_default.default_branch.branch}"
+ }
+}
+
+run "protected_branch_base_rules_test" {
+ assert {
+ condition = github_repository_ruleset.protected_branch_base_rules[0].name == "protected_branch_base_ruleset"
+ error_message = "Repository protected_branch_base_rules name does not match. Expected: protected_branch_base_ruleset, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].name}"
+ }
+ assert {
+ condition = github_repository_ruleset.protected_branch_base_rules[0].repository == var.name
+ error_message = "Repository protected_branch_base_rules repository does not match. Expected: ${var.name}, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].repository}"
+ }
+ assert {
+ condition = github_repository_ruleset.protected_branch_base_rules[0].target == "branch"
+ error_message = "Repository protected_branch_base_rules target does not match. Expected: branch, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].target}"
+ }
+ assert {
+ condition = github_repository_ruleset.protected_branch_base_rules[0].enforcement == "active"
+ error_message = "Repository protected_branch_base_rules enforcement does not match. Expected: active, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].enforcement}"
+ }
+ # test the rules
+ assert {
+ condition = github_repository_ruleset.protected_branch_base_rules[0].rules[0].deletion == true
+ error_message = "Repository protected_branch_base_rules rules deletion does not match. Expected: true, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].rules[0].deletion}"
+ }
+ assert {
+ condition = github_repository_ruleset.protected_branch_base_rules[0].rules[0].creation == false
+ error_message = "Repository protected_branch_base_rules rules creation does not match. Expected: false, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].rules[0].creation}"
+ }
+ assert {
+ condition = github_repository_ruleset.protected_branch_base_rules[0].rules[0].update == false
+ error_message = "Repository protected_branch_base_rules rules update does not match. Expected: false, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].rules[0].update}"
+ }
+ # Rule Pull Request
+ assert {
+ condition = github_repository_ruleset.protected_branch_base_rules[0].rules[0].pull_request[0].dismiss_stale_reviews_on_push == true
+ error_message = "Repository protected_branch_base_rules rules pull_request.dismiss_stale_reviews_on_push does not match. Expected: true, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].rules[0].pull_request[0].dismiss_stale_reviews_on_push}"
+ }
+ assert {
+ condition = github_repository_ruleset.protected_branch_base_rules[0].rules[0].pull_request[0].require_last_push_approval == true
+ error_message = "Repository protected_branch_base_rules rules pull_request.require_last_push_approval does not match. Expected: true, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].rules[0].pull_request[0].require_last_push_approval}"
+ }
+ assert {
+ condition = github_repository_ruleset.protected_branch_base_rules[0].rules[0].pull_request[0].required_approving_review_count == 1
+ error_message = "Repository protected_branch_base_rules rules pull_request.required_approving_review_count does not match. Expected: 1, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].rules[0].pull_request[0].required_approving_review_count}"
+ }
+
+ assert {
+ condition = github_repository_ruleset.protected_branch_base_rules[0].rules[0].non_fast_forward == true
+ error_message = "Repository protected_branch_base_rules rules non_fast_forward does not match. Expected: true, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].rules[0].non_fast_forward}"
+ }
+ # Test conditions
+ assert {
+ condition = length(github_repository_ruleset.protected_branch_base_rules[0].conditions[0].ref_name[0].exclude) == 0
+ error_message = "Repository protected_branch_base_rules conditions ref_name.exclude length does not match. Expected: 0, Actual: ${length(github_repository_ruleset.protected_branch_base_rules[0].conditions[0].ref_name[0].exclude)}"
+ }
+ assert {
+ condition = github_repository_ruleset.protected_branch_base_rules[0].conditions[0].ref_name[0].include[0] == "refs/heads/${var.protected_branches[1]}"
+ error_message = "Repository protected_branch_base_rules conditions ref_name.include does not match. Expected: refs/heads/${var.protected_branches[1]}, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].conditions[0].ref_name[0].include[0]}"
+ }
+ assert {
+ condition = github_repository_ruleset.protected_branch_base_rules[0].conditions[0].ref_name[0].include[1] == "refs/heads/${var.protected_branches[0]}"
+ error_message = "Repository protected_branch_base_rules conditions ref_name.include does not match. Expected: refs/heads/${var.protected_branches[0]}, Actual: ${github_repository_ruleset.protected_branch_base_rules[0].conditions[0].ref_name[0].include[1]}"
+ }
+}
diff --git a/modules/repository_base/rulesets.tftest.hcl b/modules/repository_base/rulesets.tftest.hcl
new file mode 100644
index 0000000..cc6d6d8
--- /dev/null
+++ b/modules/repository_base/rulesets.tftest.hcl
@@ -0,0 +1,70 @@
+mock_provider "github" {}
+
+variables {
+ name = "github-foundations-modules"
+ description = "A collection of terraform modules used in the Github Foundations framework."
+
+ repository_team_permissions = {
+ "repo_team1" = "push"
+ "repo_team2" = "admin"
+ }
+ repository_user_permissions = {
+ "user1" = "push"
+ "user2" = "admin"
+ }
+
+ rulesets = {
+ ruleset1 = {
+ target = "branch"
+ enforcement = "evaluate"
+ conditions = {
+ ref_name = {
+ include = ["main"]
+ exclude = ["feature/*"]
+ }
+ }
+ rules = {
+ branch_name_pattern = {
+ operator = "regex"
+ pattern = "main"
+ name = "branch_name_pattern"
+ negate = false
+ }
+ commit_author_email_pattern = {
+ operator = "regex"
+ pattern = "pattern"
+ name = "commit_author_email_pattern"
+ negate = false
+ }
+ commit_message_pattern = {
+ operator = "regex"
+ pattern = "pattern"
+ name = "commit_message_pattern"
+ negate = false
+ }
+ committer_email_pattern = {
+ operator = "regex"
+ pattern = "pattern"
+ name = "committer_email_pattern"
+ negate = false
+ }
+ creation = true
+ deletion = false
+ update = true
+ non_fast_forward = false
+ required_linear_history = true
+ required_signatures = false
+ update_allows_fetch_and_merge = true
+ }
+ }
+ }
+}
+
+run "rulesets_test" {
+ command = apply
+
+ assert {
+ condition = module.ruleset.ruleset1 != null
+ error_message = "The ruleset name is null"
+ }
+}
diff --git a/modules/repository_base/secrets.tftest.hcl b/modules/repository_base/secrets.tftest.hcl
new file mode 100644
index 0000000..1d34046
--- /dev/null
+++ b/modules/repository_base/secrets.tftest.hcl
@@ -0,0 +1,65 @@
+mock_provider "github" {}
+
+variables {
+ name = "github-foundations-modules"
+ description = "A collection of terraform modules used in the Github Foundations framework."
+
+ repository_team_permissions = {
+ "repo_team1" = "push"
+ "repo_team2" = "admin"
+ }
+ repository_user_permissions = {
+ "user1" = "push"
+ "user2" = "admin"
+ }
+
+ action_secrets = {
+ "action_secret1" = "dmFsdWUxCg=="
+ }
+
+ codespace_secrets = {
+ "codespace_secret1" = "dmFsdWUxCg=="
+ }
+
+ dependabot_secrets = {
+ "dependabot_secret1" = "dmFsdWUxCg=="
+ }
+}
+
+run "actions_secret_test" {
+ command = apply
+
+ assert {
+ condition = github_actions_secret.actions_secret["action_secret1"].secret_name == "action_secret1"
+ error_message = "The secret name value is incorrect. Expected \"action_secret1\", got ${github_actions_secret.actions_secret["action_secret1"].secret_name}"
+ }
+ assert {
+ condition = github_actions_secret.actions_secret["action_secret1"].encrypted_value == "dmFsdWUxCg==" # "value1" base64 encoded
+ error_message = "The encrypted value is incorrect. Expected \"dmFsdWUxCg==\", got ${nonsensitive(github_actions_secret.actions_secret["action_secret1"].encrypted_value)}"
+ }
+
+}
+
+run "namespaced_secrets_test" {
+
+ assert {
+ condition = github_codespaces_secret.codespaces_secret["codespace_secret1"].secret_name == "codespace_secret1"
+ error_message = "The secret name value is incorrect. Expected \"codespace_secret1\", got ${github_codespaces_secret.codespaces_secret["codespace_secret1"].secret_name}"
+ }
+ assert {
+ condition = github_codespaces_secret.codespaces_secret["codespace_secret1"].encrypted_value == "dmFsdWUxCg=="
+ error_message = "The encrypted value is incorrect. Expected \"dmFsdWUxCg==\", got ${nonsensitive(github_codespaces_secret.codespaces_secret["codespace_secret1"].encrypted_value)}"
+ }
+}
+
+run "dependabot_secrets_test" {
+
+ assert {
+ condition = github_dependabot_secret.dependabot_secret["dependabot_secret1"].secret_name == "dependabot_secret1"
+ error_message = "The secret name value is incorrect. Expected \"dependabot_secret1\", got ${github_dependabot_secret.dependabot_secret["dependabot_secret1"].secret_name}"
+ }
+ assert {
+ condition = github_dependabot_secret.dependabot_secret["dependabot_secret1"].encrypted_value == "dmFsdWUxCg=="
+ error_message = "The encrypted value is incorrect. Expected \"dmFsdWUxCg==\", got ${nonsensitive(github_dependabot_secret.dependabot_secret["dependabot_secret1"].encrypted_value)}"
+ }
+}
diff --git a/modules/repository_set/README.md b/modules/repository_set/README.md
index 5a74929..13eacbe 100644
--- a/modules/repository_set/README.md
+++ b/modules/repository_set/README.md
@@ -32,9 +32,9 @@
|------|-------------|------|---------|:--------:|
| [default\_repository\_team\_permissions](#input\_default\_repository\_team\_permissions) | A map where the keys are github team slugs and the value is the permissions the team should have by default for every repository. If an entry exists in `repository_team_permissions_override` for a repository then that will take precedence over this default. Defaults to `{}` giving no team access to the repositories. | `map(string)` | `{}` | no |
| [has\_ghas\_license](#input\_has\_ghas\_license) | If the organization owning the repositories has a GitHub Advanced Security license or not. Defaults to false. | `bool` | `false` | no |
-| [private\_repositories](#input\_private\_repositories) | A map of private repositories where the key is the repository name and the value is the configuration | map(object({
description = string
default_branch = string
protected_branches = list(string)
advance_security = bool
has_vulnerability_alerts = bool
topics = list(string)
homepage = string
delete_head_on_merge = bool
requires_web_commit_signing = bool
dependabot_security_updates = bool
allow_auto_merge = optional(bool)
allow_squash_merge = optional(bool)
allow_rebase_merge = optional(bool)
allow_merge_commit = optional(bool)
squash_merge_commit_title = optional(string)
squash_merge_commit_message = optional(string)
merge_commit_title = optional(string)
merge_commit_message = optional(string)
repository_team_permissions_override = optional(map(string))
user_permissions = optional(map(string))
organization_action_secrets = optional(list(string))
organization_codespace_secrets = optional(list(string))
organization_dependabot_secrets = optional(list(string))
action_secrets = optional(map(string))
codespace_secrets = optional(map(string))
dependabot_secrets = optional(map(string))
environments = optional(map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
})))
template_repository = optional(object({
owner = string
repository = string
include_all_branches = bool
}))
license_template = optional(string)
pages = optional(object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
}))
})) | n/a | yes |
-| [public\_repositories](#input\_public\_repositories) | A map of public repositories where the key is the repository name and the value is the configuration | map(object({
description = string
default_branch = string
protected_branches = list(string)
advance_security = bool
topics = list(string)
homepage = string
delete_head_on_merge = bool
dependabot_security_updates = bool
requires_web_commit_signing = bool
allow_auto_merge = optional(bool)
allow_squash_merge = optional(bool)
allow_rebase_merge = optional(bool)
allow_merge_commit = optional(bool)
squash_merge_commit_title = optional(string)
squash_merge_commit_message = optional(string)
merge_commit_title = optional(string)
merge_commit_message = optional(string)
repository_team_permissions_override = optional(map(string))
user_permissions = optional(map(string))
organization_action_secrets = optional(list(string))
organization_codespace_secrets = optional(list(string))
organization_dependabot_secrets = optional(list(string))
action_secrets = optional(map(string))
codespace_secrets = optional(map(string))
dependabot_secrets = optional(map(string))
environments = optional(map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
})))
template_repository = optional(object({
owner = string
repository = string
include_all_branches = bool
}))
license_template = optional(string)
pages = optional(object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
}))
})) | n/a | yes |
-| [rulesets](#input\_rulesets) | n/a | map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_deployment_environments = optional(list(string))
})
target = string
enforcement = string
repositories = list(string)
})) | `{}` | no |
+| [private\_repositories](#input\_private\_repositories) | A map of private repositories where the key is the repository name and the value is the configuration | map(object({
description = string
default_branch = string
protected_branches = list(string)
advance_security = bool
has_vulnerability_alerts = bool
topics = list(string)
homepage = string
delete_head_on_merge = bool
requires_web_commit_signing = bool
dependabot_security_updates = bool
allow_auto_merge = optional(bool)
allow_squash_merge = optional(bool)
allow_rebase_merge = optional(bool)
allow_merge_commit = optional(bool)
squash_merge_commit_title = optional(string)
squash_merge_commit_message = optional(string)
merge_commit_title = optional(string)
merge_commit_message = optional(string)
repository_team_permissions_override = optional(map(string))
user_permissions = optional(map(string))
organization_action_secrets = optional(list(string))
organization_codespace_secrets = optional(list(string))
organization_dependabot_secrets = optional(list(string))
action_secrets = optional(map(string))
codespace_secrets = optional(map(string))
dependabot_secrets = optional(map(string))
environments = optional(map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
})))
template_repository = optional(object({
owner = string
repository = string
include_all_branches = bool
}))
license_template = optional(string)
pages = optional(object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
}))
})) | n/a | yes |
+| [public\_repositories](#input\_public\_repositories) | A map of public repositories where the key is the repository name and the value is the configuration | map(object({
description = string
default_branch = string
protected_branches = list(string)
advance_security = bool
topics = list(string)
homepage = string
delete_head_on_merge = bool
dependabot_security_updates = bool
requires_web_commit_signing = bool
allow_auto_merge = optional(bool)
allow_squash_merge = optional(bool)
allow_rebase_merge = optional(bool)
allow_merge_commit = optional(bool)
squash_merge_commit_title = optional(string)
squash_merge_commit_message = optional(string)
merge_commit_title = optional(string)
merge_commit_message = optional(string)
repository_team_permissions_override = optional(map(string))
user_permissions = optional(map(string))
organization_action_secrets = optional(list(string))
organization_codespace_secrets = optional(list(string))
organization_dependabot_secrets = optional(list(string))
action_secrets = optional(map(string))
codespace_secrets = optional(map(string))
dependabot_secrets = optional(map(string))
environments = optional(map(object({
wait_timer = optional(number)
can_admins_bypass = optional(bool)
prevent_self_review = optional(bool)
action_secrets = optional(map(string))
reviewers = optional(object({
teams = optional(list(string))
users = optional(list(string))
}))
deployment_branch_policy = optional(object({
protected_branches = bool
custom_branch_policies = bool
branch_patterns = list(string)
}))
})))
template_repository = optional(object({
owner = string
repository = string
include_all_branches = bool
}))
license_template = optional(string)
pages = optional(object({
source = optional(object({
branch = string
path = optional(string)
}))
build_type = optional(string)
cname = optional(string)
}))
})) | n/a | yes |
+| [rulesets](#input\_rulesets) | n/a | map(object({
bypass_actors = optional(object({
repository_roles = optional(list(object({
role = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user = string
always_bypass = optional(bool)
})))
}))
conditions = optional(object({
ref_name = object({
include = list(string)
exclude = list(string)
})
}))
rules = object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_deployment_environments = optional(list(string))
})
target = string
enforcement = string
repositories = list(string)
})) | `{}` | no |
## Outputs
diff --git a/modules/repository_set/organization_secrets.tftest.hcl b/modules/repository_set/organization_secrets.tftest.hcl
new file mode 100644
index 0000000..f9d8495
--- /dev/null
+++ b/modules/repository_set/organization_secrets.tftest.hcl
@@ -0,0 +1,75 @@
+mock_provider "github" {}
+
+variables {
+ public_repositories = {
+
+ "github-foundations-modules" = {
+ # required
+ description = "A collection of terraform modules used in the Github Foundations framework."
+ default_branch = "main"
+ protected_branches = ["main"]
+ advance_security = false
+ has_vulnerability_alerts = true
+ topics = ["terraform", "github", "foundations"]
+ homepage = "myhomepage"
+ delete_head_on_merge = false
+ dependabot_security_updates = true
+ requires_web_commit_signing = false
+
+ # secrets
+ organization_action_secrets = ["org_secret1", "org_secret2"]
+ organization_codespace_secrets = ["org_codespace_secret1", "org_codespace_secret2"]
+ organization_dependabot_secrets = ["org_dependabot_secret1", "org_dependabot_secret2"]
+
+ }
+ }
+
+ private_repositories = {}
+}
+
+run "organization_actions_secrets_test" {
+ command = apply
+
+ assert {
+ condition = github_actions_organization_secret_repositories.org__action_secret_repo_access["org_secret1"].secret_name == var.public_repositories["github-foundations-modules"].organization_action_secrets[0]
+ error_message = "The repository id value is incorrect. Expected ${var.public_repositories["github-foundations-modules"].organization_action_secrets[0]}, got ${github_actions_organization_secret_repositories.org__action_secret_repo_access["org_secret1"].secret_name}"
+ }
+ assert {
+ condition = github_actions_organization_secret_repositories.org__action_secret_repo_access["org_secret2"].secret_name == var.public_repositories["github-foundations-modules"].organization_action_secrets[1]
+ error_message = "The repository id value is incorrect. Expected ${var.public_repositories["github-foundations-modules"].organization_action_secrets[1]}, got ${github_actions_organization_secret_repositories.org__action_secret_repo_access["org_secret2"].secret_name}"
+ }
+ assert {
+ condition = tolist(github_actions_organization_secret_repositories.org__action_secret_repo_access["org_secret1"].selected_repository_ids)[0] == 0
+ error_message = "The repository id value is incorrect. Expected [0], got ${tolist(github_actions_organization_secret_repositories.org__action_secret_repo_access["org_secret1"].selected_repository_ids)[0]}"
+ }
+}
+
+run "organization_codespaces_secrets_test" {
+ assert {
+ condition = github_codespaces_organization_secret_repositories.org__codespace_secret_repo_access["org_codespace_secret1"].secret_name == var.public_repositories["github-foundations-modules"].organization_codespace_secrets[0]
+ error_message = "The repository id value is incorrect. Expected ${var.public_repositories["github-foundations-modules"].organization_codespace_secrets[0]}, got ${github_codespaces_organization_secret_repositories.org__codespace_secret_repo_access["org_codespace_secret1"].secret_name}"
+ }
+ assert {
+ condition = github_codespaces_organization_secret_repositories.org__codespace_secret_repo_access["org_codespace_secret2"].secret_name == var.public_repositories["github-foundations-modules"].organization_codespace_secrets[1]
+ error_message = "The repository id value is incorrect. Expected ${var.public_repositories["github-foundations-modules"].organization_codespace_secrets[1]}, got ${github_codespaces_organization_secret_repositories.org__codespace_secret_repo_access["org_codespace_secret2"].secret_name}"
+ }
+ assert {
+ condition = tolist(github_codespaces_organization_secret_repositories.org__codespace_secret_repo_access["org_codespace_secret1"].selected_repository_ids)[0] == 0
+ error_message = "The repository id value is incorrect. Expected [0], got ${tolist(github_codespaces_organization_secret_repositories.org__codespace_secret_repo_access["org_codespace_secret1"].selected_repository_ids)[0]}"
+ }
+}
+
+run "organization_dependabot_secrets_test" {
+ assert {
+ condition = github_dependabot_organization_secret_repositories.org__dependabot_secret_repo_access["org_dependabot_secret1"].secret_name == var.public_repositories["github-foundations-modules"].organization_dependabot_secrets[0]
+ error_message = "The repository id value is incorrect. Expected ${var.public_repositories["github-foundations-modules"].organization_dependabot_secrets[0]}, got ${github_dependabot_organization_secret_repositories.org__dependabot_secret_repo_access["org_dependabot_secret1"].secret_name}"
+ }
+ assert {
+ condition = github_dependabot_organization_secret_repositories.org__dependabot_secret_repo_access["org_dependabot_secret2"].secret_name == var.public_repositories["github-foundations-modules"].organization_dependabot_secrets[1]
+ error_message = "The repository id value is incorrect. Expected ${var.public_repositories["github-foundations-modules"].organization_dependabot_secrets[1]}, got ${github_dependabot_organization_secret_repositories.org__dependabot_secret_repo_access["org_dependabot_secret2"].secret_name}"
+ }
+ assert {
+ condition = tolist(github_dependabot_organization_secret_repositories.org__dependabot_secret_repo_access["org_dependabot_secret1"].selected_repository_ids)[0] == 0
+ error_message = "The repository id value is incorrect. Expected [0], got ${tolist(github_dependabot_organization_secret_repositories.org__dependabot_secret_repo_access["org_dependabot_secret1"].selected_repository_ids)[0]}"
+ }
+}
diff --git a/modules/repository_set/repositories.tftest.hcl b/modules/repository_set/repositories.tftest.hcl
new file mode 100644
index 0000000..d0ff2de
--- /dev/null
+++ b/modules/repository_set/repositories.tftest.hcl
@@ -0,0 +1,190 @@
+mock_provider "github" {}
+
+variables {
+ public_repositories = {
+
+ "github-foundations-modules" = {
+ description = "A collection of terraform modules used in the Github Foundations framework."
+ default_branch = "main"
+ protected_branches = ["main"]
+ advance_security = false
+ has_vulnerability_alerts = true
+ topics = ["terraform", "github", "foundations"]
+ homepage = "myhomepage"
+ delete_head_on_merge = false
+ dependabot_security_updates = true
+ requires_web_commit_signing = false
+ allow_auto_merge = true
+ allow_squash_merge = false
+ allow_rebase_merge = true
+ allow_merge_commit = false
+ squash_merge_commit_title = "COMMIT_OR_PR_TITLE"
+ squash_merge_commit_message = "COMMIT_MESSAGES"
+ merge_commit_title = "PR_TITLE"
+ merge_commit_message = "PR_BODY"
+ repository_team_permissions_override = {
+ "repo_team1" = "push"
+ "repo_team2" = "admin"
+ }
+ user_permissions = {
+ "user1" = "push"
+ "user2" = "admin"
+ }
+
+
+ action_secrets = {
+ "action_secret1" = "value1"
+ }
+
+ codespace_secrets = {
+ "codespace_secret1" = "value1"
+ }
+
+ dependabot_secrets = {
+ "dependabot_secret1" = "value1"
+ }
+
+ environments = {
+ "env1" = {
+ wait_timer = 10
+ can_admins_bypass = true
+ prevent_self_review = false
+ action_secrets = {
+ "action_secret1" = "dmFsdWUxCg==" # base64 encoded
+ }
+ reviewers = {
+ teams = [111, 222]
+ users = [55, 66]
+ }
+ deployment_branch_policy = {
+ protected_branches = true
+ custom_branch_policies = false
+ branch_patterns = ["main"]
+ }
+ }
+ }
+
+ template_repository = {
+ owner = "owner"
+ repository = "template_repository"
+ include_all_branches = true
+ }
+
+ license_template = "mit"
+
+ pages = {
+ source = {
+ branch = "main"
+ path = "path"
+ }
+ # build_type = "build_type"
+ cname = "cname"
+ }
+ }
+ }
+
+ private_repositories = {
+ private-github-foundations-modules = {
+ description = "A collection of terraform modules used in the Github Foundations framework."
+ default_branch = "main"
+ protected_branches = ["main"]
+ advance_security = false
+ has_vulnerability_alerts = true
+ topics = ["terraform", "github", "foundations"]
+ homepage = "myhomepage"
+ delete_head_on_merge = false
+ dependabot_security_updates = true
+ requires_web_commit_signing = false
+ allow_auto_merge = true
+ allow_squash_merge = false
+ allow_rebase_merge = true
+ allow_merge_commit = false
+ squash_merge_commit_title = "COMMIT_OR_PR_TITLE"
+ squash_merge_commit_message = "COMMIT_MESSAGES"
+ merge_commit_title = "PR_TITLE"
+ merge_commit_message = "PR_BODY"
+ repository_team_permissions_override = {
+ "repo_team1" = "push"
+ "repo_team2" = "admin"
+ }
+ user_permissions = {
+ "user1" = "push"
+ "user2" = "admin"
+ }
+
+
+ action_secrets = {
+ "action_secret1" = "value1"
+ }
+
+ codespace_secrets = {
+ "codespace_secret1" = "value1"
+ }
+
+ dependabot_secrets = {
+ "dependabot_secret1" = "value1"
+ }
+
+ environments = {
+ "env1" = {
+ wait_timer = 10
+ can_admins_bypass = true
+ prevent_self_review = false
+ action_secrets = {
+ "action_secret1" = "dmFsdWUxCg==" # base64 encoded
+ }
+ reviewers = {
+ teams = [111, 222]
+ users = [55, 66]
+ }
+ deployment_branch_policy = {
+ protected_branches = true
+ custom_branch_policies = false
+ branch_patterns = ["main"]
+ }
+ }
+ }
+
+ template_repository = {
+ owner = "owner"
+ repository = "template_repository"
+ include_all_branches = true
+ }
+
+ license_template = "mit"
+
+ pages = {
+ source = {
+ branch = "main"
+ path = "path"
+ }
+ cname = "cname"
+ }
+ }
+ }
+
+ has_ghas_license = true
+
+ default_repository_team_permissions = {
+ GhFoundationsAdmins = "admin"
+ GhFoundationsDevelopers = "push"
+ }
+}
+
+run "apply" {
+ command = apply
+}
+
+run "public_repository_test" {
+ assert {
+ condition = module.public_repositories["github-foundations-modules"].id != null
+ error_message = "The repository id value is incorrect. Expected not null, got ${module.public_repositories["github-foundations-modules"].id}"
+ }
+}
+
+run "private_repository_test" {
+ assert {
+ condition = module.private_repositories["private-github-foundations-modules"].id != null
+ error_message = "The repository id value is incorrect. Expected not null, got ${module.private_repositories["private-github-foundations-modules"].id}"
+ }
+}
diff --git a/modules/ruleset/README.md b/modules/ruleset/README.md
index a50dc9d..21f8990 100644
--- a/modules/ruleset/README.md
+++ b/modules/ruleset/README.md
@@ -26,14 +26,14 @@ No modules.
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
-| [bypass\_actors](#input\_bypass\_actors) | An object containing fields for role, team, organization admin, and integration bypass actors. Defaults to `{}` | object({
repository_roles = optional(list(object({
role_id = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team_id = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user_id = string
always_bypass = optional(bool)
})))
}) | `{}` | no |
+| [bypass\_actors](#input\_bypass\_actors) | An object containing fields for role, team, organization admin, and integration bypass actors. Defaults to `{}` | object({
repository_roles = optional(list(object({
role_id = string
always_bypass = optional(bool)
})))
teams = optional(list(object({
team_id = string
always_bypass = optional(bool)
})))
integrations = optional(list(object({
installation_id = number
always_bypass = optional(bool)
})))
organization_admins = optional(list(object({
user_id = string
always_bypass = optional(bool)
})))
}) | `{}` | no |
| [enforcement](#input\_enforcement) | The enforcement level of the ruleset. Should be one of either `active`, `evaluate` or `disabled`. Defaults to `active` | `string` | `"active"` | no |
| [name](#input\_name) | The name of the ruleset. | `string` | n/a | yes |
| [ref\_name\_exclusions](#input\_ref\_name\_exclusions) | A list of ref names or patterns to exclude. Defaults to an empty list. If set and `ruleset_type` is set to `organization` then either `repository_name_inclusions` or `repository_name_exclusions` must be set to a list of atleast 1 string. | `list(string)` | `[]` | no |
| [ref\_name\_inclusions](#input\_ref\_name\_inclusions) | A list of ref names or patterns to include. Defaults to an empty list. If set and `ruleset_type` is set to `organization` then either `repository_name_inclusions` or `repository_name_exclusions` must be set to a list of atleast 1 string. | `list(string)` | `[]` | no |
| [repository\_name\_exclusions](#input\_repository\_name\_exclusions) | A list of repository names or patterns to exclude. If `ruleset_type` is set to `repository` then this field is ignored. | `list(string)` | `[]` | no |
| [repository\_name\_inclusions](#input\_repository\_name\_inclusions) | A list of repository names or patterns to include. If `ruleset_type` is set to `repository` then this field is ignored. | `list(string)` | `[]` | no |
-| [rules](#input\_rules) | An object containing fields for all the rule definitions the ruleset should enforce. | object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_workflows = optional(object({
required_workflows = list(object({
repository_id = number
path = string
ref = optional(string)
}))
}))
required_deployment_environments = optional(list(string))
}) | n/a | yes |
+| [rules](#input\_rules) | An object containing fields for all the rule definitions the ruleset should enforce. | object({
branch_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
tag_name_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_author_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
commit_message_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
committer_email_pattern = optional(object({
operator = string
pattern = string
name = optional(string)
negate = optional(bool)
}))
creation = optional(bool)
deletion = optional(bool)
update = optional(bool)
non_fast_forward = optional(bool)
required_linear_history = optional(bool)
required_signatures = optional(bool)
update_allows_fetch_and_merge = optional(bool)
pull_request = optional(object({
dismiss_stale_reviews_on_push = optional(bool)
require_code_owner_review = optional(bool)
require_last_push_approval = optional(bool)
required_approving_review_count = optional(number)
required_review_thread_resolution = optional(bool)
}))
required_status_checks = optional(object({
required_check = list(object({
context = string
integration_id = optional(number)
}))
strict_required_status_check_policy = optional(bool)
}))
required_workflows = optional(object({
required_workflows = list(object({
repository_id = number
path = string
ref = optional(string)
}))
}))
required_deployment_environments = optional(list(string))
}) | n/a | yes |
| [ruleset\_type](#input\_ruleset\_type) | The type of rulset to make. Should be one of ether `organization` or `repository`. | `string` | n/a | yes |
| [target](#input\_target) | The target of the ruleset. Should be one of either `branch` or `tag`. | `string` | n/a | yes |
diff --git a/modules/ruleset/organization_ruleset.tftest.hcl b/modules/ruleset/organization_ruleset.tftest.hcl
new file mode 100644
index 0000000..a066e93
--- /dev/null
+++ b/modules/ruleset/organization_ruleset.tftest.hcl
@@ -0,0 +1,360 @@
+mock_provider "github" {}
+
+
+variables {
+
+ name = "ruleset_name"
+ ruleset_type = "organization"
+ target = "tag"
+ enforcement = "active"
+
+ ref_name_inclusions = ["main"]
+ ref_name_exclusions = ["feature/*"]
+
+ repository_name_inclusions = ["repo1"]
+ repository_name_exclusions = ["repo2"]
+
+ rules = {
+ creation = true
+ update = false
+ deletion = true
+ non_fast_forward = false
+ required_linear_history = true
+ required_signatures = false
+ update_allows_fetch_and_merge = true
+
+ tag_name_pattern = {
+ operator = "regex"
+ pattern = "pattern"
+ name = "tag_name"
+ negate = false
+ }
+ commit_author_email_pattern = {
+ operator = "regex"
+ pattern = "pattern"
+ name = "commit_author_email"
+ negate = false
+ }
+ commit_message_pattern = {
+ operator = "regex"
+ pattern = "pattern"
+ name = "commit_message"
+ negate = false
+ }
+ committer_email_pattern = {
+ operator = "regex"
+ pattern = "pattern"
+ name = "committer_email"
+ negate = false
+ }
+ pull_request = {
+ dismiss_stale_reviews_on_push = true
+ require_code_owner_review = false
+ require_last_push_approval = true
+ required_approving_review_count = 2
+ required_review_thread_resolution = false
+ }
+ required_status_checks = {
+ required_check = [
+ {
+ context = "context"
+ integration_id = 555555
+ },
+ {
+ context = "context2"
+ integration_id = 666666
+ }
+ ]
+ strict_required_status_check_policy = true
+ }
+ required_workflows = {
+ required_workflows = [{
+ repository_id = 777777
+ path = "path"
+ ref = "main"
+ },
+ {
+ repository_id = 888888
+ path = "path2"
+ ref = "main"
+ }]
+ }
+ }
+
+ bypass_actors = {
+ repository_roles = [
+ {
+ role_id = 111111
+ always_bypass = false
+ }
+ ]
+ teams = [
+ {
+ team_id = 222222
+ always_bypass = true
+ }
+ ]
+ integrations = [
+ {
+ installation_id = 333333
+ always_bypass = false
+ }
+ ]
+ organization_admins = [
+ {
+ user_id = 444444
+ always_bypass = true
+ }
+ ]
+ }
+}
+
+# Test the ruleset creation
+run "create_ruleset_test" {
+ command = apply
+
+ assert {
+ condition = github_organization_ruleset.ruleset[0].name == var.name
+ error_message = "The ruleset name is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].target == var.target
+ error_message = "The target type is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].enforcement == var.enforcement
+ error_message = "The enforcement mode is incorrect."
+ }
+}
+
+# Test the ruleset ref name conditions
+run "ruleset_ref_name_conditions_test" {
+ assert {
+ condition = github_organization_ruleset.ruleset[0].conditions[0].ref_name[0].include[0] == var.ref_name_inclusions[0]
+ error_message = "The ref name inclusion is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].conditions[0].ref_name[0].exclude[0] == var.ref_name_exclusions[0]
+ error_message = "The ref name exclusion is incorrect."
+ }
+}
+
+# Test the ruleset repository name conditions
+run "ruleset_repository_conditions_test" {
+ assert {
+ condition = github_organization_ruleset.ruleset[0].conditions[0].repository_name[0].include[0] == var.repository_name_inclusions[0]
+ error_message = "The repository name inclusion is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].conditions[0].repository_name[0].exclude[0] == var.repository_name_exclusions[0]
+ error_message = "The repository name exclusion is incorrect."
+ }
+}
+# Test the ruleset rules
+run "ruleset_rules_test" {
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].creation == var.rules.creation
+ error_message = "The creation rule is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].update == var.rules.update
+ error_message = "The update rule is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].deletion == var.rules.deletion
+ error_message = "The deletion rule is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].non_fast_forward == var.rules.non_fast_forward
+ error_message = "The non-fast-forward rule is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].required_linear_history == var.rules.required_linear_history
+ error_message = "The required linear history rule is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].required_signatures == var.rules.required_signatures
+ error_message = "The required signatures rule is incorrect."
+ }
+}
+
+# Test the tag name pattern rule
+run "ruleset_tag_name_pattern_test" {
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].tag_name_pattern[0].operator == var.rules.tag_name_pattern.operator
+ error_message = "The tag name pattern operator is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].tag_name_pattern[0].pattern == var.rules.tag_name_pattern.pattern
+ error_message = "The tag name pattern is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].tag_name_pattern[0].name == var.rules.tag_name_pattern.name
+ error_message = "The tag name pattern name is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].tag_name_pattern[0].negate == var.rules.tag_name_pattern.negate
+ error_message = "The tag name pattern negate is incorrect."
+ }
+}
+
+# Test the commit author email pattern rule
+run "ruleset_commit_author_email_pattern_test" {
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].commit_author_email_pattern[0].operator == var.rules.commit_author_email_pattern.operator
+ error_message = "The commit author email pattern operator is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].commit_author_email_pattern[0].pattern == var.rules.commit_author_email_pattern.pattern
+ error_message = "The commit author email pattern is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].commit_author_email_pattern[0].name == var.rules.commit_author_email_pattern.name
+ error_message = "The commit author email pattern name is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].commit_author_email_pattern[0].negate == var.rules.commit_author_email_pattern.negate
+ error_message = "The commit author email pattern negate is incorrect."
+ }
+}
+
+# Test the commit message pattern rule
+run "ruleset_commit_message_pattern_test" {
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].commit_message_pattern[0].operator == var.rules.commit_message_pattern.operator
+ error_message = "The commit message pattern operator is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].commit_message_pattern[0].pattern == var.rules.commit_message_pattern.pattern
+ error_message = "The commit message pattern is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].commit_message_pattern[0].name == var.rules.commit_message_pattern.name
+ error_message = "The commit message pattern name is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].commit_message_pattern[0].negate == var.rules.commit_message_pattern.negate
+ error_message = "The commit message pattern negate is incorrect."
+ }
+}
+
+# Test the committer email pattern rule
+run "ruleset_committer_email_pattern_test" {
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].committer_email_pattern[0].operator == var.rules.committer_email_pattern.operator
+ error_message = "The committer email pattern operator is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].committer_email_pattern[0].pattern == var.rules.committer_email_pattern.pattern
+ error_message = "The committer email pattern is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].committer_email_pattern[0].name == var.rules.committer_email_pattern.name
+ error_message = "The committer email pattern name is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].committer_email_pattern[0].negate == var.rules.committer_email_pattern.negate
+ error_message = "The committer email pattern negate is incorrect."
+ }
+}
+
+# Test the pull request rule
+run "ruleset_pull_request_test" {
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].pull_request[0].dismiss_stale_reviews_on_push == var.rules.pull_request.dismiss_stale_reviews_on_push
+ error_message = "The pull request dismiss stale reviews on push rule is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].pull_request[0].require_code_owner_review == var.rules.pull_request.require_code_owner_review
+ error_message = "The pull request require code owner reviews rule is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].pull_request[0].require_last_push_approval == var.rules.pull_request.require_last_push_approval
+ error_message = "The pull request require last push approval rule is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].pull_request[0].required_approving_review_count == var.rules.pull_request.required_approving_review_count
+ error_message = "The pull request required approving review count rule is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].pull_request[0].required_review_thread_resolution == var.rules.pull_request.required_review_thread_resolution
+ error_message = "The pull request required review thread resolution rule is incorrect."
+ }
+}
+
+# Test the required status checks rule
+run "ruleset_required_status_checks_test" {
+ # Can't test the required checks because the tf test framework doesn't
+ # support lists of objects yet.
+ assert {
+ condition = github_organization_ruleset.ruleset[0].rules[0].required_status_checks[0].strict_required_status_checks_policy == var.rules.required_status_checks.strict_required_status_check_policy
+ error_message = "The required status checks context is incorrect."
+ }
+}
+
+# Tests for the required workflows rule not currently supported by the
+# tf test framework, as it doesn't support lists of objects yet.
+
+# Test the repository bypass actors
+run "bypass_actor_repository_roles_test" {
+ assert {
+ condition = github_organization_ruleset.ruleset[0].bypass_actors[0].actor_id == tonumber(var.bypass_actors.repository_roles[0].role_id)
+ error_message = "The bypass actor role id is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].bypass_actors[0].bypass_mode == "pull_request"
+ error_message = "The bypass actor role bypass mode is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].bypass_actors[0].actor_type == "RepositoryRole"
+ error_message = "The bypass actor type is incorrect."
+ }
+}
+
+# Test the Team bypass actors
+run "bypass_actor_teams_test" {
+ assert {
+ condition = github_organization_ruleset.ruleset[0].bypass_actors[1].actor_id == tonumber(var.bypass_actors.teams[0].team_id)
+ error_message = "The bypass actor team id is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].bypass_actors[1].bypass_mode == "always"
+ error_message = "The bypass actor team bypass mode is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].bypass_actors[1].actor_type == "Team"
+ error_message = "The bypass actor type is incorrect."
+ }
+}
+
+# Test the Integration bypass actors
+run "bypass_actor_integrations_test" {
+ assert {
+ condition = github_organization_ruleset.ruleset[0].bypass_actors[2].actor_id == tonumber(var.bypass_actors.integrations[0].installation_id)
+ error_message = "The bypass actor integration id is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].bypass_actors[2].bypass_mode == "pull_request"
+ error_message = "The bypass actor integration bypass mode is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].bypass_actors[2].actor_type == "Integration"
+ error_message = "The bypass actor type is incorrect."
+ }
+}
+
+# Test the Organization Admin bypass actors
+run "bypass_actor_organization_admins_test" {
+ assert {
+ condition = github_organization_ruleset.ruleset[0].bypass_actors[3].actor_id == tonumber(var.bypass_actors.organization_admins[0].user_id)
+ error_message = "The bypass actor organization admin id is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].bypass_actors[3].bypass_mode == "always"
+ error_message = "The bypass actor organization admin bypass mode is incorrect."
+ }
+ assert {
+ condition = github_organization_ruleset.ruleset[0].bypass_actors[3].actor_type == "OrganizationAdmin"
+ error_message = "The bypass actor type is incorrect."
+ }
+}
diff --git a/modules/ruleset/repository_ruleset.tftest.hcl b/modules/ruleset/repository_ruleset.tftest.hcl
new file mode 100644
index 0000000..77e7197
--- /dev/null
+++ b/modules/ruleset/repository_ruleset.tftest.hcl
@@ -0,0 +1,348 @@
+mock_provider "github" {}
+
+
+variables {
+
+ name = "ruleset_name"
+ ruleset_type = "repository"
+ target = "tag"
+ enforcement = "disabled"
+
+ ref_name_inclusions = ["main"]
+ ref_name_exclusions = ["feature/*"]
+
+ rules = {
+ creation = true
+ update = false
+ deletion = true
+ non_fast_forward = false
+ required_linear_history = true
+ required_signatures = false
+ update_allows_fetch_and_merge = true
+
+ branch_name_pattern = {
+ operator = "regex"
+ pattern = "pattern"
+ name = "branch_name"
+ negate = false
+ }
+ commit_author_email_pattern = {
+ operator = "regex"
+ pattern = "pattern"
+ name = "commit_author_email"
+ negate = false
+ }
+ commit_message_pattern = {
+ operator = "regex"
+ pattern = "pattern"
+ name = "commit_message"
+ negate = false
+ }
+ committer_email_pattern = {
+ operator = "regex"
+ pattern = "pattern"
+ name = "committer_email"
+ negate = false
+ }
+ pull_request = {
+ dismiss_stale_reviews_on_push = true
+ require_code_owner_review = false
+ require_last_push_approval = true
+ required_approving_review_count = 2
+ required_review_thread_resolution = false
+ }
+ required_status_checks = {
+ required_check = [
+ {
+ context = "context"
+ integration_id = 555555
+ },
+ {
+ context = "context2"
+ integration_id = 666666
+ }
+ ]
+ strict_required_status_check_policy = true
+ }
+ required_deployment_environments = ["env1", "env2"]
+ }
+
+ bypass_actors = {
+ repository_roles = [
+ {
+ role_id = 111111
+ bypass_mode = false
+ }
+ ]
+ teams = [
+ {
+ team_id = 222222
+ always_bypass = true
+ }
+ ]
+ integrations = [
+ {
+ installation_id = 333333
+ always_bypass = false
+ }
+ ]
+ organization_admins = [
+ {
+ user_id = 444444
+ always_bypass = true
+ }
+ ]
+ }
+}
+
+# Test the ruleset creation
+run "create_ruleset_test" {
+ command = apply
+
+ assert {
+ condition = github_repository_ruleset.ruleset[0].name == var.name
+ error_message = "The ruleset name is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].target == var.target
+ error_message = "The target type is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].enforcement == var.enforcement
+ error_message = "The enforcement mode is incorrect."
+ }
+}
+
+# Test the ruleset conditions
+run "ruleset_conditions_test" {
+ assert {
+ condition = github_repository_ruleset.ruleset[0].conditions[0].ref_name[0].include[0] == var.ref_name_inclusions[0]
+ error_message = "The ref name inclusion is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].conditions[0].ref_name[0].exclude[0] == var.ref_name_exclusions[0]
+ error_message = "The ref name exclusion is incorrect."
+ }
+}
+
+# Test the ruleset rules
+run "ruleset_rules_test" {
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].creation == var.rules.creation
+ error_message = "The creation rule is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].update == var.rules.update
+ error_message = "The update rule is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].deletion == var.rules.deletion
+ error_message = "The deletion rule is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].non_fast_forward == var.rules.non_fast_forward
+ error_message = "The non-fast-forward rule is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].required_linear_history == var.rules.required_linear_history
+ error_message = "The required linear history rule is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].required_signatures == var.rules.required_signatures
+ error_message = "The required signatures rule is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].update_allows_fetch_and_merge == var.rules.update_allows_fetch_and_merge
+ error_message = "The update allows fetch and merge rule is incorrect."
+ }
+}
+
+# Test the branch name pattern rule
+run "ruleset_branch_name_pattern_test" {
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].branch_name_pattern[0].operator == var.rules.branch_name_pattern.operator
+ error_message = "The branch name pattern operator is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].branch_name_pattern[0].pattern == var.rules.branch_name_pattern.pattern
+ error_message = "The branch name pattern is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].branch_name_pattern[0].name == var.rules.branch_name_pattern.name
+ error_message = "The branch name pattern name is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].branch_name_pattern[0].negate == var.rules.branch_name_pattern.negate
+ error_message = "The branch name pattern negate is incorrect."
+ }
+}
+
+# Test the commit author email pattern rule
+run "ruleset_commit_author_email_pattern_test" {
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].commit_author_email_pattern[0].operator == var.rules.commit_author_email_pattern.operator
+ error_message = "The commit author email pattern operator is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].commit_author_email_pattern[0].pattern == var.rules.commit_author_email_pattern.pattern
+ error_message = "The commit author email pattern is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].commit_author_email_pattern[0].name == var.rules.commit_author_email_pattern.name
+ error_message = "The commit author email pattern name is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].commit_author_email_pattern[0].negate == var.rules.commit_author_email_pattern.negate
+ error_message = "The commit author email pattern negate is incorrect."
+ }
+}
+
+# Test the commit message pattern rule
+run "ruleset_commit_message_pattern_test" {
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].commit_message_pattern[0].operator == var.rules.commit_message_pattern.operator
+ error_message = "The commit message pattern operator is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].commit_message_pattern[0].pattern == var.rules.commit_message_pattern.pattern
+ error_message = "The commit message pattern is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].commit_message_pattern[0].name == var.rules.commit_message_pattern.name
+ error_message = "The commit message pattern name is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].commit_message_pattern[0].negate == var.rules.commit_message_pattern.negate
+ error_message = "The commit message pattern negate is incorrect."
+ }
+}
+
+# Test the committer email pattern rule
+run "ruleset_committer_email_pattern_test" {
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].committer_email_pattern[0].operator == var.rules.committer_email_pattern.operator
+ error_message = "The committer email pattern operator is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].committer_email_pattern[0].pattern == var.rules.committer_email_pattern.pattern
+ error_message = "The committer email pattern is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].committer_email_pattern[0].name == var.rules.committer_email_pattern.name
+ error_message = "The committer email pattern name is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].committer_email_pattern[0].negate == var.rules.committer_email_pattern.negate
+ error_message = "The committer email pattern negate is incorrect."
+ }
+}
+
+# Test the pull request rule
+run "ruleset_pull_request_test" {
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].pull_request[0].dismiss_stale_reviews_on_push == var.rules.pull_request.dismiss_stale_reviews_on_push
+ error_message = "The pull request dismiss stale reviews on push rule is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].pull_request[0].require_code_owner_review == var.rules.pull_request.require_code_owner_review
+ error_message = "The pull request require code owner reviews rule is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].pull_request[0].require_last_push_approval == var.rules.pull_request.require_last_push_approval
+ error_message = "The pull request require last push approval rule is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].pull_request[0].required_approving_review_count == var.rules.pull_request.required_approving_review_count
+ error_message = "The pull request required approving review count rule is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].pull_request[0].required_review_thread_resolution == var.rules.pull_request.required_review_thread_resolution
+ error_message = "The pull request required review thread resolution rule is incorrect."
+ }
+}
+
+# Test the required status checks rule
+run "ruleset_required_status_checks_test" {
+ # Can't test the required checks because the tf test framework doesn't
+ # support lists of objects yet.
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].required_status_checks[0].strict_required_status_checks_policy == var.rules.required_status_checks.strict_required_status_check_policy
+ error_message = "The required status checks context is incorrect."
+ }
+}
+
+# Test the required deployments rule
+run "ruleset_required_deployments_test" {
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].required_deployments[0].required_deployment_environments[0] == var.rules.required_deployment_environments[0]
+ error_message = "The required deployment environments is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].rules[0].required_deployments[0].required_deployment_environments[1] == var.rules.required_deployment_environments[1]
+ error_message = "The required deployment environments is incorrect."
+ }
+}
+
+# Test the repository bypass actors
+run "bypass_actor_repository_roles_test" {
+ assert {
+ condition = github_repository_ruleset.ruleset[0].bypass_actors[0].actor_id == tonumber(var.bypass_actors.repository_roles[0].role_id)
+ error_message = "The bypass actor role id is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].bypass_actors[0].bypass_mode == "pull_request"
+ error_message = "The bypass actor role bypass mode is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].bypass_actors[0].actor_type == "RepositoryRole"
+ error_message = "The bypass actor type is incorrect."
+ }
+}
+
+# Test the Team bypass actors
+run "bypass_actor_teams_test" {
+ assert {
+ condition = github_repository_ruleset.ruleset[0].bypass_actors[1].actor_id == tonumber(var.bypass_actors.teams[0].team_id)
+ error_message = "The bypass actor team id is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].bypass_actors[1].bypass_mode == "always"
+ error_message = "The bypass actor team bypass mode is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].bypass_actors[1].actor_type == "Team"
+ error_message = "The bypass actor type is incorrect."
+ }
+}
+
+# Test the Integration bypass actors
+run "bypass_actor_integrations_test" {
+ assert {
+ condition = github_repository_ruleset.ruleset[0].bypass_actors[2].actor_id == tonumber(var.bypass_actors.integrations[0].installation_id)
+ error_message = "The bypass actor integration id is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].bypass_actors[2].bypass_mode == "pull_request"
+ error_message = "The bypass actor integration bypass mode is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].bypass_actors[2].actor_type == "Integration"
+ error_message = "The bypass actor type is incorrect."
+ }
+}
+
+# Test the Organization Admin bypass actors
+run "bypass_actor_organization_admins_test" {
+ assert {
+ condition = github_repository_ruleset.ruleset[0].bypass_actors[3].actor_id == tonumber(var.bypass_actors.organization_admins[0].user_id)
+ error_message = "The bypass actor organization admin id is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].bypass_actors[3].bypass_mode == "always"
+ error_message = "The bypass actor organization admin bypass mode is incorrect."
+ }
+ assert {
+ condition = github_repository_ruleset.ruleset[0].bypass_actors[3].actor_type == "OrganizationAdmin"
+ error_message = "The bypass actor type is incorrect."
+ }
+}
diff --git a/modules/team/team.tftest.hcl b/modules/team/team.tftest.hcl
new file mode 100644
index 0000000..3372213
--- /dev/null
+++ b/modules/team/team.tftest.hcl
@@ -0,0 +1,46 @@
+mock_provider "github" {
+}
+
+variables {
+ team_name = "team 1"
+ team_description = "This is a test team"
+ privacy = "closed"
+ team_maintainers = ["user1", "user2"]
+ team_members = ["user3", "user4", "user5"]
+ team_id = "888"
+ parent_id = "777"
+}
+
+run "team_test" {
+
+ command = apply
+
+ assert {
+ condition = github_team_membership.maintainers[var.team_maintainers[0]].team_id == var.team_id
+ error_message = "The maintainer's team id is incorrect. Expected: ${var.team_id}, Actual: ${github_team_membership.maintainers[var.team_maintainers[0]].team_id}"
+ }
+ assert {
+ condition = github_team_membership.maintainers[var.team_maintainers[0]].username == var.team_maintainers[0]
+ error_message = "The maintainer's username is incorrect. Expected: ${var.team_maintainers[0]}, Actual: ${github_team_membership.maintainers[var.team_maintainers[0]].username}"
+ }
+ assert {
+ condition = github_team_membership.maintainers[var.team_maintainers[0]].role == "maintainer"
+ error_message = "The maintainer's role is incorrect. Expected: maintainer, Actual: ${github_team_membership.maintainers[var.team_maintainers[0]].role}"
+ }
+}
+
+run "team_member_test" {
+ assert {
+ condition = github_team_membership.members[var.team_members[0]].team_id == var.team_id
+ error_message = "The member's team id is incorrect. Expected: ${var.team_id}, Actual: ${github_team_membership.members[var.team_members[0]].team_id}"
+ }
+ assert {
+ condition = github_team_membership.members[var.team_members[0]].username == var.team_members[0]
+ error_message = "The member's username is incorrect. Expected: ${var.team_members[0]}, Actual: ${github_team_membership.members[var.team_members[0]].username}"
+ }
+ assert {
+ condition = github_team_membership.members[var.team_members[0]].role == "member"
+ error_message = "The member's role is incorrect. Expected: member, Actual: ${github_team_membership.members[var.team_members[0]].role}"
+ }
+
+}
diff --git a/modules/team_set/README.md b/modules/team_set/README.md
index fcf9e7a..3ffd7da 100644
--- a/modules/team_set/README.md
+++ b/modules/team_set/README.md
@@ -28,8 +28,8 @@
| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
-| [preexisting\_teams](#input\_preexisting\_teams) | A map of existing teams where the key is the team name and the value is the configuration. If the team does not have a parent team, the parent\_id should be an empty string. | map(object({
bucket = string
prefix = string
output_name = string
maintainers = list(string)
members = list(string)
parent_id = string
})) | `{}` | no |
-| [teams](#input\_teams) | A map of teams to create where the key is the team name and the value is the configuration. If the team does not have a parent team, the parent\_id should be an empty string. | map(object({
description = string
privacy = string
maintainers = list(string)
members = list(string)
parent_id = string
})) | n/a | yes |
+| [preexisting\_teams](#input\_preexisting\_teams) | A map of existing teams where the key is the team name and the value is the configuration. If the team does not have a parent team, the parent\_id should be an empty string. | map(object({
bucket = string
prefix = string
output_name = string
maintainers = list(string)
members = list(string)
parent_id = string
})) | `{}` | no |
+| [teams](#input\_teams) | A map of teams to create where the key is the team name and the value is the configuration. If the team does not have a parent team, the parent\_id should be an empty string. | map(object({
description = string
privacy = string
maintainers = list(string)
members = list(string)
parent_id = string
})) | n/a | yes |
## Outputs
diff --git a/modules/team_set/teams.tftest.hcl b/modules/team_set/teams.tftest.hcl
new file mode 100644
index 0000000..0f06f6b
--- /dev/null
+++ b/modules/team_set/teams.tftest.hcl
@@ -0,0 +1,56 @@
+mock_provider "github" {
+ mock_resource "github_team" {
+ defaults = {
+ slug = "team-1"
+ }
+
+ }
+}
+
+variables {
+ teams = {
+ "team-1" = {
+ maintainers = ["user1"]
+ members = ["user2"]
+ description = "This is a test team"
+ privacy = "closed"
+ parent_id = 777
+ },
+ "team-2" = {
+ maintainers = ["user1", "user2"]
+ members = ["user3"]
+ description = "This is another test team"
+ privacy = "closed"
+ parent_id = 888
+ },
+ "team-3" = {
+ maintainers = ["user1", "user2", "user3"]
+ members = ["user4", "user5"]
+ description = "This is a third test team"
+ privacy = "secret"
+ parent_id = 999
+ }
+ }
+}
+
+run "create_teams" {
+
+ command = apply
+
+ assert {
+ condition = module.team.team-1.name == "team-1"
+ error_message = "The team name is incorrect. Expected team-1 but got ${module.team.team-1.name}"
+ }
+ assert {
+ condition = module.team.team-1.slug == "team-1"
+ error_message = "The team slug is incorrect. Expected team-1 but got ${module.team.team-1.slug}"
+ }
+ assert {
+ condition = module.team.team-2.name == "team-2"
+ error_message = "The team name is incorrect. Expected team-2 but got ${module.team.team-2.name}"
+ }
+ assert {
+ condition = module.team["team-3"].name == "team-3"
+ error_message = "The team name is incorrect. Expected team-3 but got ${module.team.team-3.name}"
+ }
+}