diff --git a/modules/organization/README.md b/modules/organization/README.md index 7166a13..6cee7f2 100644 --- a/modules/organization/README.md +++ b/modules/organization/README.md @@ -3,41 +3,51 @@ | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.3 | -| [github](#requirement\_github) | 5.42.0 | +| [github](#requirement\_github) | 6.1.0 | ## Providers | Name | Version | |------|---------| -| [github](#provider\_github) | 5.42.0 | +| [github](#provider\_github) | 6.1.0 | ## Modules -No modules. +| Name | Source | Version | +|------|--------|---------| +| [base\_default\_branch\_protection](#module\_base\_default\_branch\_protection) | ../ruleset | n/a | +| [dismiss\_stale\_reviews](#module\_dismiss\_stale\_reviews) | ../ruleset | n/a | +| [minimum\_approvals](#module\_minimum\_approvals) | ../ruleset | n/a | +| [require\_signatures](#module\_require\_signatures) | ../ruleset | n/a | +| [ruleset](#module\_ruleset) | ../ruleset | n/a | ## Resources | Name | Type | |------|------| -| [github_actions_organization_secret.action_secret](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/actions_organization_secret) | resource | -| [github_codespaces_organization_secret.codespace_secret](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/codespaces_organization_secret) | resource | -| [github_dependabot_organization_secret.dependabot_secret](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/dependabot_organization_secret) | resource | -| [github_membership.membership_for_user](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/membership) | resource | -| [github_organization_block.blocked_user](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/organization_block) | resource | -| [github_organization_custom_role.community_manager_role](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/organization_custom_role) | resource | -| [github_organization_custom_role.contractor_role](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/organization_custom_role) | resource | -| [github_organization_custom_role.custom_repository_role](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/organization_custom_role) | resource | -| [github_organization_custom_role.security_engineer_role](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/organization_custom_role) | resource | -| [github_organization_settings.organization_settings](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/organization_settings) | resource | +| [github_actions_organization_secret.action_secret](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/actions_organization_secret) | resource | +| [github_codespaces_organization_secret.codespace_secret](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/codespaces_organization_secret) | resource | +| [github_dependabot_organization_secret.dependabot_secret](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/dependabot_organization_secret) | resource | +| [github_membership.membership_for_user](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/membership) | resource | +| [github_organization_block.blocked_user](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/organization_block) | resource | +| [github_organization_custom_role.community_manager_role](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/organization_custom_role) | resource | +| [github_organization_custom_role.contractor_role](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/organization_custom_role) | resource | +| [github_organization_custom_role.custom_repository_role](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/organization_custom_role) | resource | +| [github_organization_custom_role.security_engineer_role](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/organization_custom_role) | resource | +| [github_organization_settings.organization_settings](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/organization_settings) | resource | +| [github_organization_custom_role.branch_ruleset_bypasser](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/data-sources/organization_custom_role) | data source | +| [github_team.branch_ruleset_bypasser](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/data-sources/team) | data source | +| [github_user.branch_ruleset_bypasser](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/data-sources/user) | data source | ## Inputs | 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 | +| [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 | -| [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 | +| [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 | @@ -57,6 +67,7 @@ No modules. | [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 | ## Outputs diff --git a/modules/organization/rulesets.tf b/modules/organization/rulesets.tf new file mode 100644 index 0000000..a51af05 --- /dev/null +++ b/modules/organization/rulesets.tf @@ -0,0 +1,249 @@ +locals { + all_team_bypassers = toset(concat( + coalesce(try(var.default_branch_protection_rulesets.bypass_actors.teams, []), []), + [ + for _, ruleset_config in var.rulesets : coalesce(try(ruleset_config.bypass_actors.teams, []), []) + ]... + )) + + all_admin_bypassers = toset(concat( + coalesce(try(var.default_branch_protection_rulesets.bypass_actors.organization_admins, []), []), + [ + for _, ruleset_config in var.rulesets : coalesce(try(ruleset_config.bypass_actors.organization_admins, []), []) + ]... + )) + + all_repository_roles_bypassers = toset(concat( + coalesce(try(var.default_branch_protection_rulesets.bypass_actors.repository_roles, []), []), + [ + for _, ruleset_config in var.rulesets : coalesce(try(ruleset_config.bypass_actors.repository_roles, []), []) + ]... + )) + + github_base_role_ids = { + "maintain" = 2 + "write" = 4 + "admin" = 5 + } +} + + +data "github_team" "branch_ruleset_bypasser" { + for_each = { + for bypasser in local.all_team_bypassers : bypasser.team => bypasser.team + } + + slug = each.value + summary_only = true +} + +data "github_user" "branch_ruleset_bypasser" { + for_each = { + for bypasser in local.all_admin_bypassers : bypasser.user => bypasser.user + } + + username = each.value +} + +data "github_organization_custom_role" "branch_ruleset_bypasser" { + for_each = { + for bypasser in local.all_repository_roles_bypassers : bypasser.role => bypasser.role + } + + name = each.value +} + +module "ruleset" { + source = "../ruleset" + + for_each = var.rulesets + + name = each.key + target = each.value.target + enforcement = each.value.enforcement + + ruleset_type = "organization" + + rules = { + creation = each.value.rules.creation + update = each.value.rules.update + deletion = each.value.rules.deletion + non_fast_forward = each.value.rules.non_fast_forward + required_linear_history = each.value.rules.required_linear_history + required_signatures = each.value.rules.required_signatures + + branch_name_pattern = each.value.rules.branch_name_pattern + tag_name_pattern = each.value.rules.tag_name_pattern + commit_author_email_pattern = each.value.rules.commit_author_email_pattern + commit_message_pattern = each.value.rules.commit_message_pattern + committer_email_pattern = each.value.rules.committer_email_pattern + pull_request = each.value.rules.pull_request + required_status_checks = each.value.rules.required_status_checks + required_workflows = each.value.rules.required_workflows + } + + bypass_actors = { + repository_roles = [for bypasser in try(toset(coalesce(each.value.bypass_actors.repository_roles, [])), []) : { + role_id = lookup(local.github_base_role_ids, bypasser.role, data.github_organization_custom_role.branch_ruleset_bypasser["${bypasser.role}"].id) + always_bypass = bypasser.always_bypass + }] + teams = [for bypasser in try(toset(coalesce(each.value.bypass_actors.teams, [])), []) : { + team_id = data.github_team.branch_ruleset_bypasser["${bypasser.team}"].id + always_bypass = bypasser.always_bypass + }] + organization_admins = [for bypasser in try(toset(coalesce(each.value.bypass_actors.organization_admins, [])), []) : { + user_id = data.github_user.branch_ruleset_bypasser["${bypasser.user}"].id + always_bypass = bypasser.always_bypass + }] + integrations = try(each.value.bypass_actors.integrations, []) + } + + ref_name_inclusions = each.value.conditions.ref_name.include + ref_name_exclusions = each.value.conditions.ref_name.exclude + repository_name_inclusions = each.value.conditions.repository_name.include + repository_name_exclusions = each.value.conditions.repository_name.exclude +} + +module "base_default_branch_protection" { + source = "../ruleset" + count = var.default_branch_protection_rulesets.base_protection != null ? 1 : 0 + + name = "base_default_branch_protection" + target = "branch" + enforcement = var.default_branch_protection_rulesets.base_protection.enforcement + + ruleset_type = "organization" + + ref_name_inclusions = ["~DEFAULT_BRANCH"] + repository_name_inclusions = ["~ALL"] + + rules = { + deletion = true + non_fast_forward = true + pull_request = {} + } + + bypass_actors = { + repository_roles = [for bypasser in try(toset(coalesce(var.default_branch_protection_rulesets.bypass_actors.repository_roles, [])), []) : { + role_id = lookup(local.github_base_role_ids, bypasser.role, data.github_organization_custom_role.branch_ruleset_bypasser["${bypasser.role}"].id) + always_bypass = bypasser.always_bypass + }] + teams = [for bypasser in try(toset(coalesce(var.default_branch_protection_rulesets.bypass_actors.teams, [])), []) : { + team_id = data.github_team.branch_ruleset_bypasser["${bypasser.team}"].id + always_bypass = bypasser.always_bypass + }] + organization_admins = [for bypasser in try(toset(coalesce(var.default_branch_protection_rulesets.bypass_actors.organization_admins, [])), []) : { + user_id = data.github_user.branch_ruleset_bypasser["${bypasser.user}"].id + always_bypass = bypasser.always_bypass + }] + integrations = try(var.default_branch_protection_rulesets.bypass_actors.integrations, []) + } +} + +module "minimum_approvals" { + source = "../ruleset" + count = var.default_branch_protection_rulesets.minimum_approvals != null ? 1 : 0 + + name = "minimum_approvals" + target = "branch" + enforcement = var.default_branch_protection_rulesets.minimum_approvals.enforcement + + ruleset_type = "organization" + + ref_name_inclusions = ["~DEFAULT_BRANCH"] + repository_name_inclusions = ["~ALL"] + + rules = { + pull_request = { + require_last_push_approval = true + required_approving_review_count = var.default_branch_protection_rulesets.minimum_approvals.approvals_required + } + } + + bypass_actors = { + repository_roles = [for bypasser in try(toset(coalesce(var.default_branch_protection_rulesets.bypass_actors.repository_roles, [])), []) : { + role_id = lookup(local.github_base_role_ids, bypasser.role, data.github_organization_custom_role.branch_ruleset_bypasser["${bypasser.role}"].id) + always_bypass = bypasser.always_bypass + }] + teams = [for bypasser in try(toset(coalesce(var.default_branch_protection_rulesets.bypass_actors.teams, [])), []) : { + team_id = data.github_team.branch_ruleset_bypasser["${bypasser.team}"].id + always_bypass = bypasser.always_bypass + }] + organization_admins = [for bypasser in try(toset(coalesce(var.default_branch_protection_rulesets.bypass_actors.organization_admins, [])), []) : { + user_id = data.github_user.branch_ruleset_bypasser["${bypasser.user}"].id + always_bypass = bypasser.always_bypass + }] + integrations = try(var.default_branch_protection_rulesets.bypass_actors.integrations, []) + } +} + + +module "dismiss_stale_reviews" { + source = "../ruleset" + count = var.default_branch_protection_rulesets.dismiss_stale_reviews != null ? 1 : 0 + + name = "dismiss_stale_reviews" + target = "branch" + enforcement = var.default_branch_protection_rulesets.dismiss_stale_reviews.enforcement + + ruleset_type = "organization" + + ref_name_inclusions = ["~DEFAULT_BRANCH"] + repository_name_inclusions = ["~ALL"] + + rules = { + pull_request = { + dismiss_stale_reviews_on_push = true + } + } + + bypass_actors = { + repository_roles = [for bypasser in try(toset(coalesce(var.default_branch_protection_rulesets.bypass_actors.repository_roles, [])), []) : { + role_id = lookup(local.github_base_role_ids, bypasser.role, data.github_organization_custom_role.branch_ruleset_bypasser["${bypasser.role}"].id) + always_bypass = bypasser.always_bypass + }] + teams = [for bypasser in try(toset(coalesce(var.default_branch_protection_rulesets.bypass_actors.teams, [])), []) : { + team_id = data.github_team.branch_ruleset_bypasser["${bypasser.team}"].id + always_bypass = bypasser.always_bypass + }] + organization_admins = [for bypasser in try(toset(coalesce(var.default_branch_protection_rulesets.bypass_actors.organization_admins, [])), []) : { + user_id = data.github_user.branch_ruleset_bypasser["${bypasser.user}"].id + always_bypass = bypasser.always_bypass + }] + integrations = try(var.default_branch_protection_rulesets.bypass_actors.integrations, []) + } +} + +module "require_signatures" { + source = "../ruleset" + count = var.default_branch_protection_rulesets.require_signatures != null ? 1 : 0 + + name = "require_signatures" + target = "branch" + enforcement = var.default_branch_protection_rulesets.require_signatures.enforcement + + ruleset_type = "organization" + + ref_name_inclusions = ["~DEFAULT_BRANCH"] + repository_name_inclusions = ["~ALL"] + + rules = { + required_signatures = true + } + + bypass_actors = { + repository_roles = [for bypasser in try(toset(coalesce(var.default_branch_protection_rulesets.bypass_actors.repository_roles, [])), []) : { + role_id = lookup(local.github_base_role_ids, bypasser.role, data.github_organization_custom_role.branch_ruleset_bypasser["${bypasser.role}"].id) + always_bypass = bypasser.always_bypass + }] + teams = [for bypasser in try(toset(coalesce(var.default_branch_protection_rulesets.bypass_actors.teams, [])), []) : { + team_id = data.github_team.branch_ruleset_bypasser["${bypasser.team}"].id + always_bypass = bypasser.always_bypass + }] + organization_admins = [for bypasser in try(toset(coalesce(var.default_branch_protection_rulesets.bypass_actors.organization_admins, [])), []) : { + user_id = data.github_user.branch_ruleset_bypasser["${bypasser.user}"].id + always_bypass = bypasser.always_bypass + }] + integrations = try(var.default_branch_protection_rulesets.bypass_actors.integrations, []) + } +} \ No newline at end of file diff --git a/modules/organization/variables.tf b/modules/organization/variables.tf index 271739b..3708ac1 100644 --- a/modules/organization/variables.tf +++ b/modules/organization/variables.tf @@ -135,8 +135,8 @@ variable "custom_repository_roles" { variable "actions_secrets" { type = map(object({ - encrypted_value = string - visibility = string + encrypted_value = string + visibility = string })) description = "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." default = {} @@ -144,8 +144,8 @@ variable "actions_secrets" { variable "codespaces_secrets" { type = map(object({ - encrypted_value = string - visibility = string + encrypted_value = string + visibility = string })) description = "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." default = {} @@ -153,10 +153,141 @@ variable "codespaces_secrets" { variable "dependabot_secrets" { type = map(object({ - encrypted_value = string - visibility = string + encrypted_value = string + visibility = string })) description = "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." default = {} } +variable "default_branch_protection_rulesets" { + type = 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) + }))) + })) + }) + default = {} +} + +variable "rulesets" { + type = 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 + })) + default = {} +} \ No newline at end of file diff --git a/modules/organization/versions.tf b/modules/organization/versions.tf index 99d757e..d32b1bb 100644 --- a/modules/organization/versions.tf +++ b/modules/organization/versions.tf @@ -3,7 +3,7 @@ terraform { required_providers { github = { source = "integrations/github" - version = "5.42.0" + version = "6.1.0" } } } \ No newline at end of file diff --git a/modules/private_repository/README.md b/modules/private_repository/README.md index f516dee..f318c8a 100644 --- a/modules/private_repository/README.md +++ b/modules/private_repository/README.md @@ -2,8 +2,8 @@ | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.7.1 | -| [github](#requirement\_github) | 5.42.0 | +| [terraform](#requirement\_terraform) | >= 1.3 | +| [github](#requirement\_github) | 6.1.0 | ## Providers @@ -38,6 +38,7 @@ No resources. | [name](#input\_name) | The name of the repository to create/import. | `string` | n/a | yes | | [protected\_branches](#input\_protected\_branches) | A list of ref names or patterns that should be protected. Defaults `["main"]` | `list(string)` |
[
"main"
]
| 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 | +| [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 | | [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 | diff --git a/modules/private_repository/outputs.tf b/modules/private_repository/outputs.tf index 0d1a523..c3adac2 100644 --- a/modules/private_repository/outputs.tf +++ b/modules/private_repository/outputs.tf @@ -1,4 +1,4 @@ output "id" { - value = module.repository_base.id + value = module.repository_base.id description = "The ID of the repository" } \ No newline at end of file diff --git a/modules/private_repository/repository.tf b/modules/private_repository/repository.tf index e95445b..45e1cc1 100644 --- a/modules/private_repository/repository.tf +++ b/modules/private_repository/repository.tf @@ -33,4 +33,6 @@ module "repository_base" { template_repository = var.template_repository license_template = var.license_template + + rulesets = var.rulesets } diff --git a/modules/private_repository/variables.tf b/modules/private_repository/variables.tf index b9a111a..ec040b5 100644 --- a/modules/private_repository/variables.tf +++ b/modules/private_repository/variables.tf @@ -65,19 +65,19 @@ variable "advance_security" { variable "action_secrets" { description = "An (Optional) map of GitHub Actions secrets to create for this repository. The key is the name of the secret and the value is the encrypted value." type = map(string) - default = {} + default = {} } variable "codespace_secrets" { description = "An (Optional) map of Github Codespace secrets to create for this repository. The key is the name of the secret and the value is the encrypted value." type = map(string) - default = {} + default = {} } variable "dependabot_secrets" { description = "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." type = map(string) - default = {} + default = {} } variable "environments" { @@ -90,7 +90,7 @@ variable "environments" { variable "template_repository" { description = "A (Optional) list of template repositories to use for the repository" - type = object({ + type = object({ owner = string repository = string include_all_branches = bool @@ -102,4 +102,90 @@ variable "license_template" { description = "The (Optional) license template to use for the repository" type = string default = null +} + +variable "rulesets" { + type = 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 + })) + default = {} } \ No newline at end of file diff --git a/modules/private_repository/versions.tf b/modules/private_repository/versions.tf index d6e8a27..d32b1bb 100644 --- a/modules/private_repository/versions.tf +++ b/modules/private_repository/versions.tf @@ -1,9 +1,9 @@ terraform { - required_version = ">= 1.7.1" + required_version = ">= 1.3" required_providers { github = { source = "integrations/github" - version = "5.42.0" + version = "6.1.0" } } } \ No newline at end of file diff --git a/modules/public_repository/README.md b/modules/public_repository/README.md index 038a54e..77eb62a 100644 --- a/modules/public_repository/README.md +++ b/modules/public_repository/README.md @@ -2,8 +2,8 @@ | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.7.1 | -| [github](#requirement\_github) | 5.42.0 | +| [terraform](#requirement\_terraform) | >= 1.3 | +| [github](#requirement\_github) | 6.1.0 | ## Providers @@ -38,6 +38,7 @@ No resources. | [name](#input\_name) | The name of the repository to create/import. | `string` | n/a | yes | | [protected\_branches](#input\_protected\_branches) | A list of ref names or patterns that should be protected. Defaults `["main"]` | `list(string)` |
[
"main"
]
| 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 | +| [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 | | [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 | diff --git a/modules/public_repository/outputs.tf b/modules/public_repository/outputs.tf index 0d1a523..c3adac2 100644 --- a/modules/public_repository/outputs.tf +++ b/modules/public_repository/outputs.tf @@ -1,4 +1,4 @@ output "id" { - value = module.repository_base.id + value = module.repository_base.id description = "The ID of the repository" } \ No newline at end of file diff --git a/modules/public_repository/repository.tf b/modules/public_repository/repository.tf index 7eb7e5e..7acc30e 100644 --- a/modules/public_repository/repository.tf +++ b/modules/public_repository/repository.tf @@ -30,7 +30,9 @@ module "repository_base" { action_secrets = var.action_secrets environments = var.environments - + template_repository = var.template_repository license_template = var.license_template + + rulesets = var.rulesets } \ No newline at end of file diff --git a/modules/public_repository/variables.tf b/modules/public_repository/variables.tf index 3e84e32..ba047b8 100644 --- a/modules/public_repository/variables.tf +++ b/modules/public_repository/variables.tf @@ -65,19 +65,19 @@ variable "advance_security" { variable "action_secrets" { description = "An (Optional) map of GitHub Actions secrets to create for this repository. The key is the name of the secret and the value is the encrypted value." type = map(string) - default = {} + default = {} } variable "codespace_secrets" { description = "An (Optional) map of GitHub Codespace secrets to create for this repository. The key is the name of the secret and the value is the encrypted value." type = map(string) - default = {} + default = {} } variable "dependabot_secrets" { description = "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." type = map(string) - default = {} + default = {} } variable "environments" { @@ -90,7 +90,7 @@ variable "environments" { variable "template_repository" { description = "A (Optional) list of template repositories to use for the repository" - type = object({ + type = object({ owner = string repository = string include_all_branches = bool @@ -102,4 +102,90 @@ variable "license_template" { description = "The (Optional) license template to apply to the repository" type = string default = null +} + +variable "rulesets" { + type = 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 + })) + default = {} } \ No newline at end of file diff --git a/modules/public_repository/versions.tf b/modules/public_repository/versions.tf index d6e8a27..d32b1bb 100644 --- a/modules/public_repository/versions.tf +++ b/modules/public_repository/versions.tf @@ -1,9 +1,9 @@ terraform { - required_version = ">= 1.7.1" + required_version = ">= 1.3" required_providers { github = { source = "integrations/github" - version = "5.42.0" + version = "6.1.0" } } } \ No newline at end of file diff --git a/modules/repository_base/README.md b/modules/repository_base/README.md index f357a5d..d05a568 100644 --- a/modules/repository_base/README.md +++ b/modules/repository_base/README.md @@ -2,33 +2,38 @@ | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.7.1 | -| [github](#requirement\_github) | 5.42.0 | +| [terraform](#requirement\_terraform) | >= 1.3 | +| [github](#requirement\_github) | 6.1.0 | ## Providers | Name | Version | |------|---------| -| [github](#provider\_github) | 5.42.0 | +| [github](#provider\_github) | 6.1.0 | ## Modules -No modules. +| Name | Source | Version | +|------|--------|---------| +| [ruleset](#module\_ruleset) | ../ruleset | n/a | ## Resources | Name | Type | |------|------| -| [github_actions_environment_secret.environment_secret](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/actions_environment_secret) | resource | -| [github_actions_secret.actions_secret](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/actions_secret) | resource | -| [github_branch_default.default_branch](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/branch_default) | resource | -| [github_codespaces_secret.codespaces_secret](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/codespaces_secret) | resource | -| [github_dependabot_secret.dependabot_secret](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/dependabot_secret) | resource | -| [github_repository.repository](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/repository) | resource | -| [github_repository_collaborators.collaborators](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/repository_collaborators) | resource | -| [github_repository_dependabot_security_updates.automated_security_fixes](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/repository_dependabot_security_updates) | resource | -| [github_repository_environment.environment](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/repository_environment) | resource | -| [github_repository_ruleset.protected_branch_base_rules](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/repository_ruleset) | resource | +| [github_actions_environment_secret.environment_secret](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/actions_environment_secret) | resource | +| [github_actions_secret.actions_secret](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/actions_secret) | resource | +| [github_branch_default.default_branch](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/branch_default) | resource | +| [github_codespaces_secret.codespaces_secret](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/codespaces_secret) | resource | +| [github_dependabot_secret.dependabot_secret](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/dependabot_secret) | resource | +| [github_repository.repository](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/repository) | resource | +| [github_repository_collaborators.collaborators](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/repository_collaborators) | resource | +| [github_repository_dependabot_security_updates.automated_security_fixes](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/repository_dependabot_security_updates) | resource | +| [github_repository_environment.environment](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/repository_environment) | resource | +| [github_repository_ruleset.protected_branch_base_rules](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/repository_ruleset) | resource | +| [github_organization_custom_role.branch_ruleset_bypasser](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/data-sources/organization_custom_role) | data source | +| [github_team.branch_ruleset_bypasser](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/data-sources/team) | data source | +| [github_user.branch_ruleset_bypasser](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/data-sources/user) | data source | ## Inputs @@ -55,6 +60,7 @@ No modules. | [name](#input\_name) | The name of the repository to create/import. | `string` | n/a | yes | | [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)` |
[
"~DEFAULT_BRANCH"
]
| 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 | +| [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 | | [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 | diff --git a/modules/repository_base/outputs.tf b/modules/repository_base/outputs.tf index 33078a8..1d91b91 100644 --- a/modules/repository_base/outputs.tf +++ b/modules/repository_base/outputs.tf @@ -1,4 +1,4 @@ output "id" { - value = github_repository.repository.repo_id + value = github_repository.repository.repo_id description = "The ID of the repository" } \ No newline at end of file diff --git a/modules/repository_base/rulesets.tf b/modules/repository_base/rulesets.tf new file mode 100644 index 0000000..a199f5d --- /dev/null +++ b/modules/repository_base/rulesets.tf @@ -0,0 +1,101 @@ +locals { + all_team_bypassers = toset(flatten( + [ + for _, ruleset_config in var.rulesets : coalesce(try(ruleset_config.bypass_actors.teams, []), []) + ] + )) + + all_admin_bypassers = toset(flatten( + [ + for _, ruleset_config in var.rulesets : coalesce(try(ruleset_config.bypass_actors.organization_admins, []), []) + ] + )) + + all_repository_roles_bypassers = toset(flatten( + [ + for _, ruleset_config in var.rulesets : coalesce(try(ruleset_config.bypass_actors.repository_roles, []), []) + ] + )) + + github_base_role_ids = { + "maintain" = 2 + "write" = 4 + "admin" = 5 + } +} + + +data "github_team" "branch_ruleset_bypasser" { + for_each = { + for bypasser in local.all_team_bypassers : bypasser.team => bypasser.team + } + + slug = each.value + summary_only = true +} + +data "github_user" "branch_ruleset_bypasser" { + for_each = { + for bypasser in local.all_admin_bypassers : bypasser.user => bypasser.user + } + + username = each.value +} + +data "github_organization_custom_role" "branch_ruleset_bypasser" { + for_each = { + for bypasser in local.all_repository_roles_bypassers : bypasser.role => bypasser.role + } + + name = each.value +} + +module "ruleset" { + source = "../ruleset" + + for_each = var.rulesets + + name = each.key + target = each.value.target + enforcement = each.value.enforcement + + ruleset_type = "repository" + + rules = { + creation = each.value.rules.creation + update = each.value.rules.update + deletion = each.value.rules.deletion + non_fast_forward = each.value.rules.non_fast_forward + required_linear_history = each.value.rules.required_linear_history + required_signatures = each.value.rules.required_signatures + update_allows_fetch_and_merge = each.value.rules.update_allows_fetch_and_merge + + branch_name_pattern = each.value.rules.branch_name_pattern + tag_name_pattern = each.value.rules.tag_name_pattern + commit_author_email_pattern = each.value.rules.commit_author_email_pattern + commit_message_pattern = each.value.rules.commit_message_pattern + committer_email_pattern = each.value.rules.committer_email_pattern + pull_request = each.value.rules.pull_request + required_status_checks = each.value.rules.required_status_checks + required_deployment_environments = each.value.rules.required_deployment_environments + } + + bypass_actors = { + repository_roles = [for bypasser in try(toset(coalesce(each.value.bypass_actors.repository_roles, [])), []) : { + role_id = lookup(local.github_base_role_ids, bypasser.role, data.github_organization_custom_role.branch_ruleset_bypasser["${bypasser.role}"].id) + always_bypass = bypasser.always_bypass + }] + teams = [for bypasser in try(toset(coalesce(each.value.bypass_actors.teams, [])), []) : { + team_id = data.github_team.branch_ruleset_bypasser["${bypasser.team}"].id + always_bypass = bypasser.always_bypass + }] + organization_admins = [for bypasser in try(toset(coalesce(each.value.bypass_actors.organization_admins, [])), []) : { + user_id = data.github_user.branch_ruleset_bypasser["${bypasser.user}"].id + always_bypass = bypasser.always_bypass + }] + integrations = try(each.value.bypass_actors.repository_roles, []) + } + + ref_name_inclusions = each.value.conditions.ref_name.include + ref_name_exclusions = each.value.conditions.ref_name.exclude +} \ No newline at end of file diff --git a/modules/repository_base/variables.tf b/modules/repository_base/variables.tf index a9a2f0d..9a49ac2 100644 --- a/modules/repository_base/variables.tf +++ b/modules/repository_base/variables.tf @@ -144,7 +144,7 @@ variable "environments" { variable "template_repository" { description = "A (Optional) list of template repositories to use for the repository" - type = object({ + type = object({ owner = string repository = string include_all_branches = bool @@ -156,4 +156,90 @@ variable "license_template" { description = "The (Optional) license template to use for the repository" type = string default = null +} + +variable "rulesets" { + type = 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 + })) + default = {} } \ No newline at end of file diff --git a/modules/repository_base/versions.tf b/modules/repository_base/versions.tf index d6e8a27..d32b1bb 100644 --- a/modules/repository_base/versions.tf +++ b/modules/repository_base/versions.tf @@ -1,9 +1,9 @@ terraform { - required_version = ">= 1.7.1" + required_version = ">= 1.3" required_providers { github = { source = "integrations/github" - version = "5.42.0" + version = "6.1.0" } } } \ No newline at end of file diff --git a/modules/repository_set/README.md b/modules/repository_set/README.md index 534a87f..6f09872 100644 --- a/modules/repository_set/README.md +++ b/modules/repository_set/README.md @@ -2,14 +2,14 @@ | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.7.1 | -| [github](#requirement\_github) | 5.42.0 | +| [terraform](#requirement\_terraform) | >= 1.3 | +| [github](#requirement\_github) | 6.1.0 | ## Providers | Name | Version | |------|---------| -| [github](#provider\_github) | 5.42.0 | +| [github](#provider\_github) | 6.1.0 | ## Modules @@ -22,9 +22,9 @@ | Name | Type | |------|------| -| [github_actions_organization_secret_repositories.org__action_secret_repo_access](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/actions_organization_secret_repositories) | resource | -| [github_codespaces_organization_secret_repositories.org__codespace_secret_repo_access](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/codespaces_organization_secret_repositories) | resource | -| [github_dependabot_organization_secret_repositories.org__dependabot_secret_repo_access](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/dependabot_organization_secret_repositories) | resource | +| [github_actions_organization_secret_repositories.org__action_secret_repo_access](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/actions_organization_secret_repositories) | resource | +| [github_codespaces_organization_secret_repositories.org__codespace_secret_repo_access](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/codespaces_organization_secret_repositories) | resource | +| [github_dependabot_organization_secret_repositories.org__dependabot_secret_repo_access](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/dependabot_organization_secret_repositories) | resource | ## Inputs @@ -33,6 +33,7 @@ | [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. | `map(string)` | n/a | yes | | [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
repository_team_permissions_override = map(string)
protected_branches = list(string)
advance_security = bool
has_vulnerability_alerts = bool
topics = list(string)
homepage = string
delete_head_on_merge = bool
allow_auto_merge = bool
dependabot_security_updates = bool
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({
action_secrets = optional(map(string))
})))
template_repository = optional(object({
owner = string
repository = string
include_all_branches = bool
}))
license_template = 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
repository_team_permissions_override = map(string)
protected_branches = list(string)
advance_security = bool
topics = list(string)
homepage = string
delete_head_on_merge = bool
allow_auto_merge = bool
dependabot_security_updates = bool
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({
action_secrets = optional(map(string))
})))
template_repository = optional(object({
owner = string
repository = string
include_all_branches = bool
}))
license_template = 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.tf b/modules/repository_set/organization-secrets.tf index 12c0127..599a08c 100644 --- a/modules/repository_set/organization-secrets.tf +++ b/modules/repository_set/organization-secrets.tf @@ -1,5 +1,5 @@ locals { - coalesced_public_repositories = coalesce(var.public_repositories, {}) + coalesced_public_repositories = coalesce(var.public_repositories, {}) coalesced_private_repositories = coalesce(var.private_repositories, {}) organization_action_secrets = distinct(flatten(concat( @@ -42,20 +42,20 @@ locals { resource "github_actions_organization_secret_repositories" "org__action_secret_repo_access" { for_each = local.organization_action_secrets_repository_id_list - secret_name = each.key + secret_name = each.key selected_repository_ids = each.value } resource "github_codespaces_organization_secret_repositories" "org__codespace_secret_repo_access" { for_each = local.codespace_secrets_repository_id_list - secret_name = each.key + secret_name = each.key selected_repository_ids = each.value } resource "github_dependabot_organization_secret_repositories" "org__dependabot_secret_repo_access" { for_each = local.dependabot_secrets_id_list - secret_name = each.key + secret_name = each.key selected_repository_ids = each.value } \ No newline at end of file diff --git a/modules/repository_set/repositories.tf b/modules/repository_set/repositories.tf index af80a83..4a390b9 100644 --- a/modules/repository_set/repositories.tf +++ b/modules/repository_set/repositories.tf @@ -1,3 +1,16 @@ +locals { + rulesets_by_public_repository = { + for repo_name, repo_config in var.public_repositories : repo_name => { + for ruleset_name, ruleset_config in var.rulesets : ruleset_name => ruleset_config if contains(ruleset_config.repositories, repo_name) + } + } + rulesets_by_private_repository = { + for repo_name, repo_config in var.private_repositories : repo_name => { + for ruleset_name, ruleset_config in var.rulesets : ruleset_name => ruleset_config if contains(ruleset_config.repositories, repo_name) + } + } +} + module "public_repositories" { source = "../public_repository" @@ -20,6 +33,7 @@ module "public_repositories" { environments = each.value.environments template_repository = each.value.template_repository license_template = each.value.license_template + rulesets = lookup(local.rulesets_by_public_repository, each.key, {}) } module "private_repositories" { @@ -44,4 +58,6 @@ module "private_repositories" { environments = each.value.environments template_repository = each.value.template_repository license_template = each.value.license_template + rulesets = lookup(local.rulesets_by_private_repository, each.key, {}) + } diff --git a/modules/repository_set/variables.tf b/modules/repository_set/variables.tf index aac97f7..5d7a6ab 100644 --- a/modules/repository_set/variables.tf +++ b/modules/repository_set/variables.tf @@ -65,3 +65,90 @@ variable "default_repository_team_permissions" { type = map(string) description = "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." } + +variable "rulesets" { + type = 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) + })) + default = {} +} \ No newline at end of file diff --git a/modules/repository_set/versions.tf b/modules/repository_set/versions.tf index d6e8a27..d32b1bb 100644 --- a/modules/repository_set/versions.tf +++ b/modules/repository_set/versions.tf @@ -1,9 +1,9 @@ terraform { - required_version = ">= 1.7.1" + required_version = ">= 1.3" required_providers { github = { source = "integrations/github" - version = "5.42.0" + version = "6.1.0" } } } \ No newline at end of file diff --git a/modules/ruleset/README.md b/modules/ruleset/README.md new file mode 100644 index 0000000..1809db2 --- /dev/null +++ b/modules/ruleset/README.md @@ -0,0 +1,44 @@ +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3 | +| [github](#requirement\_github) | 6.1.0 | + +## Providers + +| Name | Version | +|------|---------| +| [github](#provider\_github) | 6.1.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [github_organization_ruleset.ruleset](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/organization_ruleset) | resource | +| [github_repository_ruleset.ruleset](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/repository_ruleset) | resource | + +## Inputs + +| 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 | +| [conditions](#input\_conditions) | An object that describes what branches and repositories the ruleset should apply to. If `ruleset_type` is set to `repository` then this field is optional and repository\_name is ignored. |
object({
ref_name = optional(object({
include = list(string)
exclude = list(string)
}))
repository_name = optional(object({
include = list(string)
exclude = list(string)
}))
})
| `{}` | 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](#input\_repository) | The repository to create the ruleset under. Only applicable if `ruleset_type` is set to `repository`. Defaults to "" | `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 | +| [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 | + +## Outputs + +No outputs. \ No newline at end of file diff --git a/modules/ruleset/organization_ruleset.tf b/modules/ruleset/organization_ruleset.tf new file mode 100644 index 0000000..b2b531f --- /dev/null +++ b/modules/ruleset/organization_ruleset.tf @@ -0,0 +1,172 @@ +resource "github_organization_ruleset" "ruleset" { + count = var.ruleset_type == "organization" ? 1 : 0 + + name = var.name + target = var.target + enforcement = var.enforcement + + dynamic "conditions" { + for_each = length(concat(var.ref_name_inclusions, var.ref_name_exclusions)) > 0 ? [1] : [] + content { + ref_name { + include = var.ref_name_inclusions + exclude = var.ref_name_exclusions + } + + repository_name { + include = var.repository_name_inclusions + exclude = var.repository_name_exclusions + } + } + } + + rules { + creation = var.rules.creation + update = var.rules.update + deletion = var.rules.deletion + non_fast_forward = var.rules.non_fast_forward + required_linear_history = var.rules.required_linear_history + required_signatures = var.rules.required_signatures + + dynamic "branch_name_pattern" { + for_each = var.rules.branch_name_pattern != null ? [var.rules.branch_name_pattern] : [] + + content { + operator = branch_name_pattern.value.operator + pattern = branch_name_pattern.value.pattern + name = branch_name_pattern.value.name + negate = coalesce(branch_name_pattern.value.negate, false) + } + } + + dynamic "tag_name_pattern" { + for_each = var.rules.tag_name_pattern != null ? [var.rules.tag_name_pattern] : [] + + content { + operator = tag_name_pattern.value.operator + pattern = tag_name_pattern.value.pattern + name = tag_name_pattern.value.name + negate = coalesce(tag_name_pattern.value.negate, false) + } + } + + dynamic "commit_author_email_pattern" { + for_each = var.rules.commit_author_email_pattern != null ? [var.rules.commit_author_email_pattern] : [] + + content { + operator = commit_author_email_pattern.value.operator + pattern = commit_author_email_pattern.value.pattern + name = commit_author_email_pattern.value.name + negate = coalesce(commit_author_email_pattern.value.negate, false) + } + } + + dynamic "commit_message_pattern" { + for_each = var.rules.commit_message_pattern != null ? [var.rules.commit_message_pattern] : [] + + content { + operator = commit_message_pattern.value.operator + pattern = commit_message_pattern.value.pattern + name = commit_message_pattern.value.name + negate = coalesce(commit_message_pattern.value.negate, false) + } + } + + dynamic "committer_email_pattern" { + for_each = var.rules.committer_email_pattern != null ? [var.rules.committer_email_pattern] : [] + + content { + operator = committer_email_pattern.value.operator + pattern = committer_email_pattern.value.pattern + name = committer_email_pattern.value.name + negate = coalesce(committer_email_pattern.value.negate, false) + } + } + + dynamic "pull_request" { + for_each = var.rules.pull_request != null ? [var.rules.pull_request] : [] + + content { + dismiss_stale_reviews_on_push = coalesce(pull_request.value.dismiss_stale_reviews_on_push, false) + require_code_owner_review = coalesce(pull_request.value.require_code_owner_review, false) + require_last_push_approval = coalesce(pull_request.value.require_last_push_approval, false) + required_approving_review_count = coalesce(pull_request.value.required_approving_review_count, 0) + required_review_thread_resolution = coalesce(pull_request.value.required_review_thread_resolution, false) + } + } + + dynamic "required_status_checks" { + for_each = var.rules.required_status_checks != null ? [var.rules.required_status_checks] : [] + + content { + dynamic "required_check" { + for_each = required_status_checks.value.required_check + + content { + context = required_check.value.context + integration_id = required_check.value.integration_id + } + } + + strict_required_status_checks_policy = required_status_checks.value.strict_required_status_check_policy + } + } + + dynamic "required_workflows" { + for_each = var.rules.required_workflows != null ? [var.rules.required_workflows] : [] + + content { + + dynamic "required_workflow" { + for_each = required_workflows.value.required_workflows + + content { + repository_id = required_workflow.value.repository_id + path = required_workflow.value.path + ref = coalesce(required_workflow.value.ref, "main") + } + } + } + } + } + + dynamic "bypass_actors" { + for_each = var.bypass_actors != null ? toset(coalesce(var.bypass_actors.repository_roles, [])) : [] + + content { + actor_id = bypass_actors.value.role_id + actor_type = "RepositoryRole" + bypass_mode = coalesce(bypass_actors.value.always_bypass, false) ? "always" : "pull_request" + } + } + + dynamic "bypass_actors" { + for_each = var.bypass_actors != null ? toset(coalesce(var.bypass_actors.teams, [])) : [] + + content { + actor_id = bypass_actors.value.team_id + actor_type = "Team" + bypass_mode = coalesce(bypass_actors.value.always_bypass, false) ? "always" : "pull_request" + } + } + + dynamic "bypass_actors" { + for_each = var.bypass_actors != null ? toset(coalesce(var.bypass_actors.integrations, [])) : [] + + content { + actor_id = bypass_actors.value.installation_id + actor_type = "Integration" + bypass_mode = coalesce(bypass_actors.value.always_bypass, false) ? "always" : "pull_request" + } + } + + dynamic "bypass_actors" { + for_each = var.bypass_actors != null ? toset(coalesce(var.bypass_actors.organization_admins, [])) : [] + + content { + actor_id = bypass_actors.value.user_id + actor_type = "OrganizationAdmin" + bypass_mode = coalesce(bypass_actors.value.always_bypass, false) ? "always" : "pull_request" + } + } +} \ No newline at end of file diff --git a/modules/ruleset/repository_ruleset.tf b/modules/ruleset/repository_ruleset.tf new file mode 100644 index 0000000..bcaa162 --- /dev/null +++ b/modules/ruleset/repository_ruleset.tf @@ -0,0 +1,157 @@ +resource "github_repository_ruleset" "ruleset" { + count = var.ruleset_type == "repository" ? 1 : 0 + name = var.name + target = var.target + enforcement = var.enforcement + + dynamic "conditions" { + for_each = length(concat(var.ref_name_inclusions, var.ref_name_exclusions)) > 0 ? [1] : [] + content { + ref_name { + include = var.ref_name_inclusions + exclude = var.ref_name_exclusions + } + } + } + + rules { + creation = var.rules.creation + update = var.rules.update + deletion = var.rules.deletion + non_fast_forward = var.rules.non_fast_forward + required_linear_history = var.rules.required_linear_history + required_signatures = var.rules.required_signatures + update_allows_fetch_and_merge = var.rules.update_allows_fetch_and_merge + + dynamic "branch_name_pattern" { + for_each = var.rules.branch_name_pattern != null ? [var.rules.branch_name_pattern] : [] + + content { + operator = branch_name_pattern.value.operator + pattern = branch_name_pattern.value.pattern + name = branch_name_pattern.value.name + negate = coalesce(branch_name_pattern.value.negate, false) + } + } + + dynamic "tag_name_pattern" { + for_each = var.rules.tag_name_pattern != null ? [var.rules.tag_name_pattern] : [] + + content { + operator = tag_name_pattern.value.operator + pattern = tag_name_pattern.value.pattern + name = tag_name_pattern.value.name + negate = coalesce(tag_name_pattern.value.negate, false) + } + } + + dynamic "commit_author_email_pattern" { + for_each = var.rules.commit_author_email_pattern != null ? [var.rules.commit_author_email_pattern] : [] + + content { + operator = commit_author_email_pattern.value.operator + pattern = commit_author_email_pattern.value.pattern + name = commit_author_email_pattern.value.name + negate = coalesce(commit_author_email_pattern.value.negate, false) + } + } + + dynamic "commit_message_pattern" { + for_each = var.rules.commit_message_pattern != null ? [var.rules.commit_message_pattern] : [] + + content { + operator = commit_message_pattern.value.operator + pattern = commit_message_pattern.value.pattern + name = commit_message_pattern.value.name + negate = coalesce(commit_message_pattern.value.negate, false) + } + } + + dynamic "committer_email_pattern" { + for_each = var.rules.committer_email_pattern != null ? [var.rules.committer_email_pattern] : [] + + content { + operator = committer_email_pattern.value.operator + pattern = committer_email_pattern.value.pattern + name = committer_email_pattern.value.name + negate = coalesce(committer_email_pattern.value.negate, false) + } + } + + dynamic "pull_request" { + for_each = var.rules.pull_request != null ? [var.rules.pull_request] : [] + + content { + dismiss_stale_reviews_on_push = coalesce(pull_request.value.dismiss_stale_reviews_on_push, false) + require_code_owner_review = coalesce(pull_request.value.require_code_owner_review, false) + require_last_push_approval = coalesce(pull_request.value.require_last_push_approval, false) + required_approving_review_count = coalesce(pull_request.value.required_approving_review_count, 0) + required_review_thread_resolution = coalesce(pull_request.value.required_review_thread_resolution, false) + } + } + + dynamic "required_status_checks" { + for_each = var.rules.required_status_checks != null ? [var.rules.required_status_checks] : [] + + content { + dynamic "required_check" { + for_each = required_status_checks.value.required_check + + content { + context = required_check.value.context + integration_id = required_check.value.integration_id + } + } + + strict_required_status_checks_policy = required_status_checks.value.strict_required_status_check_policy + } + } + + dynamic "required_deployments" { + for_each = var.rules.required_deployment_environments != null ? [var.rules.required_deployment_environments] : [] + + content { + required_deployment_environments = required_deployments.value + } + } + } + + dynamic "bypass_actors" { + for_each = var.bypass_actors.repository_roles != null ? toset(coalesce(var.bypass_actors.repository_roles, [])) : [] + + content { + actor_id = bypass_actors.value.role_id + actor_type = "RepositoryRole" + bypass_mode = coalesce(bypass_actors.value.always_bypass, false) ? "always" : "pull_request" + } + } + dynamic "bypass_actors" { + for_each = var.bypass_actors != null ? toset(coalesce(var.bypass_actors.teams, [])) : [] + + content { + actor_id = bypass_actors.value.team_id + actor_type = "Team" + bypass_mode = coalesce(bypass_actors.value.always_bypass, false) ? "always" : "pull_request" + } + } + + dynamic "bypass_actors" { + for_each = var.bypass_actors != null ? toset(coalesce(var.bypass_actors.integrations, [])) : [] + + content { + actor_id = bypass_actors.value.installation_id + actor_type = "Integration" + bypass_mode = coalesce(bypass_actors.value.always_bypass, false) ? "always" : "pull_request" + } + } + + dynamic "bypass_actors" { + for_each = var.bypass_actors != null ? toset(coalesce(var.bypass_actors.organization_admins, [])) : [] + + content { + actor_id = bypass_actors.value.user_id + actor_type = "OrganizationAdmin" + bypass_mode = coalesce(bypass_actors.value.always_bypass, false) ? "always" : "pull_request" + } + } +} \ No newline at end of file diff --git a/modules/ruleset/variables.tf b/modules/ruleset/variables.tf new file mode 100644 index 0000000..1c245d9 --- /dev/null +++ b/modules/ruleset/variables.tf @@ -0,0 +1,165 @@ +variable "name" { + type = string + description = "The name of the ruleset." +} + +variable "bypass_actors" { + type = 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) + }))) + }) + default = {} + description = "An object containing fields for role, team, organization admin, and integration bypass actors. Defaults to `{}`" +} + +variable "rules" { + type = 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)) + }) + description = "An object containing fields for all the rule definitions the ruleset should enforce." +} + +variable "ref_name_inclusions" { + type = list(string) + description = "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." + default = [] +} + +variable "ref_name_exclusions" { + type = list(string) + description = "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." + default = [] +} + +variable "repository_name_inclusions" { + type = list(string) + description = "A list of repository names or patterns to include. If `ruleset_type` is set to `repository` then this field is ignored." + default = [] +} + +variable "repository_name_exclusions" { + type = list(string) + description = "A list of repository names or patterns to exclude. If `ruleset_type` is set to `repository` then this field is ignored." + default = [] +} + +variable "conditions" { + type = object({ + ref_name = optional(object({ + include = list(string) + exclude = list(string) + })) + repository_name = optional(object({ + include = list(string) + exclude = list(string) + })) + }) + description = "An object that describes what branches and repositories the ruleset should apply to. If `ruleset_type` is set to `repository` then this field is optional and repository_name is ignored." + default = {} +} + +variable "target" { + type = string + description = "The target of the ruleset. Should be one of either `branch` or `tag`." + validation { + condition = can(regex("branch|tag", var.target)) + error_message = "The target must be either `branch` or `tag`." + } +} + +variable "ruleset_type" { + type = string + description = "The type of rulset to make. Should be one of ether `organization` or `repository`." + validation { + condition = can(regex("organization|repository", var.ruleset_type)) + error_message = "The ruleset type must be either `organization` or `repository`." + } +} + +variable "enforcement" { + type = string + description = "The enforcement level of the ruleset. Should be one of either `active`, `evaluate` or `disabled`. Defaults to `active`" + default = "active" + validation { + condition = can(regex("active|evaluate|disabled", var.enforcement)) + error_message = "The enforcement level must be either `active`, `evaluate` or `disabled`." + } +} + +variable "repository" { + type = string + description = "The repository to create the ruleset under. Only applicable if `ruleset_type` is set to `repository`. Defaults to \"\"" + default = "" +} \ No newline at end of file diff --git a/modules/ruleset/versions.tf b/modules/ruleset/versions.tf new file mode 100644 index 0000000..d32b1bb --- /dev/null +++ b/modules/ruleset/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.3" + required_providers { + github = { + source = "integrations/github" + version = "6.1.0" + } + } +} \ No newline at end of file diff --git a/modules/team/README.md b/modules/team/README.md index ff3387c..0991dfc 100644 --- a/modules/team/README.md +++ b/modules/team/README.md @@ -3,13 +3,13 @@ | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.3 | -| [github](#requirement\_github) | 5.42.0 | +| [github](#requirement\_github) | 6.1.0 | ## Providers | Name | Version | |------|---------| -| [github](#provider\_github) | 5.42.0 | +| [github](#provider\_github) | 6.1.0 | ## Modules @@ -19,9 +19,9 @@ No modules. | Name | Type | |------|------| -| [github_team.team](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/team) | resource | -| [github_team_membership.maintainers](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/team_membership) | resource | -| [github_team_membership.members](https://registry.terraform.io/providers/integrations/github/5.42.0/docs/resources/team_membership) | resource | +| [github_team.team](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/team) | resource | +| [github_team_membership.maintainers](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/team_membership) | resource | +| [github_team_membership.members](https://registry.terraform.io/providers/integrations/github/6.1.0/docs/resources/team_membership) | resource | ## Inputs diff --git a/modules/team/versions.tf b/modules/team/versions.tf index 99d757e..d32b1bb 100644 --- a/modules/team/versions.tf +++ b/modules/team/versions.tf @@ -3,7 +3,7 @@ terraform { required_providers { github = { source = "integrations/github" - version = "5.42.0" + version = "6.1.0" } } } \ No newline at end of file diff --git a/modules/team_set/README.md b/modules/team_set/README.md index e209b1f..468ce69 100644 --- a/modules/team_set/README.md +++ b/modules/team_set/README.md @@ -3,7 +3,7 @@ | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.3 | -| [github](#requirement\_github) | 5.42.0 | +| [github](#requirement\_github) | 6.1.0 | ## Providers diff --git a/modules/team_set/versions.tf b/modules/team_set/versions.tf index a369b06..95d7d87 100644 --- a/modules/team_set/versions.tf +++ b/modules/team_set/versions.tf @@ -3,7 +3,7 @@ terraform { required_providers { github = { source = "integrations/github" - version = "5.42.0" + version = "6.1.0" } } }