Skip to content

Commit 0103e5a

Browse files
authored
feat: Refactor module to single-repo pattern with configurable branch protection, and autolink support (#7)
* feat: Refactor module to single-repo pattern with configurable branch protection, and autolink support * chore: Remove unnecessary args in pre-commit-config
1 parent 03b9929 commit 0103e5a

File tree

4 files changed

+330
-118
lines changed

4 files changed

+330
-118
lines changed

README.md

Lines changed: 57 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,80 @@
11
# Terraform GitHub Repository
22

3-
Creates a GitHub repository.
3+
Creates a GitHub repository with strong, opinionated branch protection for the default branch.
44

5-
This module is very opinionated and possibly not suitable for other use cases.
5+
By default, this module:
66

7-
It creates one or more GitHub repositories with a ruleset protecting the main branch.
7+
- Blocks force pushes and branch deletions
8+
- Requires a linear commit history and signed commits
9+
- Optionally enforces pull request reviews (see variables)
10+
11+
If your use case requires custom or no branch protection, set `enable_default_ruleset = false` and use the [`github_repository_ruleset`](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository_ruleset) resource directly in your configuration.
812

913
## Usage
1014

1115
```hcl
12-
module "repositories" {
16+
module "repository" {
1317
source = "terraform-github-repository"
18+
name = "example-repo"
1419
15-
repositories = [
16-
{
17-
name = "test"
18-
},
19-
{
20-
name = "test2"
21-
repo_visibility = "public"
22-
}
23-
]
20+
pr_require_code_owner_review = true
21+
pr_required_approving_review_count = 1
2422
}
25-
2623
```
2724

25+
**Note:** If you set `pr_require_code_owner_review = true`, you must add a `CODEOWNERS` file to your repository or PR merges will be blocked.
26+
2827
<!-- BEGIN_TF_DOCS -->
2928
***
3029

3130
## Inputs
3231

3332
| Name | Description | Type | Default | Required |
3433
|------|-------------|------|---------|:--------:|
35-
| repo_name | Name of repository. | `string` | n/a | yes |
36-
| allow_auto_merge | value | `bool` | `false` | no |
37-
| allow_merge_commit | value | `bool` | `false` | no |
38-
| allow_rebase_merge | value | `bool` | `false` | no |
39-
| allow_squash_merge | value | `bool` | `true` | no |
40-
| allow_update_branch | value | `bool` | `true` | no |
41-
| archived | value | `bool` | `false` | no |
42-
| auto_init | value | `bool` | `false` | no |
43-
| default_branch_name | Name of default branch. | `string` | `"main"` | no |
44-
| delete_branch_on_merge | value | `bool` | `true` | no |
45-
| has_discussions | value | `bool` | `false` | no |
46-
| has_downloads | value | `bool` | `false` | no |
47-
| has_issues | value | `bool` | `true` | no |
48-
| has_projects | value | `bool` | `false` | no |
49-
| has_wiki | value | `bool` | `false` | no |
50-
| is_template | value | `bool` | `false` | no |
51-
| repo_description | Description of the repository. Displayed in right-hand column on repo home page. | `string` | `""` | no |
52-
| repo_visibility | Whether repo should be `public` or `private`. | `string` | `"private"` | no |
53-
| vulnerability_alerts | value | `bool` | `true` | no |
54-
| web_commit_signoff_required | value | `bool` | `false` | no |
34+
| name | Name of the repository. | `string` | n/a | yes |
35+
| allow_auto_merge | Whether auto-merge is allowed. | `bool` | `false` | no |
36+
| allow_merge_commit | Whether merge commits are allowed. | `bool` | `false` | no |
37+
| allow_rebase_merge | Whether rebase merges are allowed. | `bool` | `false` | no |
38+
| allow_squash_merge | Whether squash merges are allowed. | `bool` | `true` | no |
39+
| allow_update_branch | Whether branches can be updated automatically. | `bool` | `true` | no |
40+
| archived | Whether the repository is archived. | `bool` | `false` | no |
41+
| auto_init | Whether an initial commit is automatically produced in the repository. | `bool` | `false` | no |
42+
| autolink_reference_prefix | This prefix appended by a number generates a link any time it is found in an issue, PR, or commit. Required if `autolink_reference_url_template` is set. | `string` | `""` | no |
43+
| autolink_reference_url_template | The target URL template for the autolink reference. Use `<num>` as a placeholder for the number. Required if `autolink_reference_prefix` is set. | `string` | `""` | no |
44+
| default_branch_name | Name of the default branch. | `string` | `"main"` | no |
45+
| delete_branch_on_merge | Whether to delete branch on merge. | `bool` | `true` | no |
46+
| description | Description of the repository. | `string` | `""` | no |
47+
| enable_default_ruleset | Whether to create a secure default branch protection ruleset. | `bool` | `true` | no |
48+
| has_discussions | Whether the repository has discussions enabled. | `bool` | `false` | no |
49+
| has_downloads | Whether the repository has downloads enabled. | `bool` | `false` | no |
50+
| has_issues | Whether the repository has issues enabled. | `bool` | `true` | no |
51+
| has_projects | Whether the repository has projects enabled. | `bool` | `false` | no |
52+
| has_wiki | Whether the repository has wiki enabled. | `bool` | `false` | no |
53+
| is_template | Whether the repository is a template. | `bool` | `false` | no |
54+
| pr_dismiss_stale_reviews_on_push | Whether to dismiss stale reviews on a PR when new commits are pushed. | `bool` | `true` | no |
55+
| pr_require_code_owner_review | Whether to require code owner review on a PR before merging. | `bool` | `false` | no |
56+
| pr_require_last_push_approval | Whether the most recent pusher to a PR must approve the PR. | `bool` | `false` | no |
57+
| pr_required_approving_review_count | Number of required PR approving reviews. | `number` | `0` | no |
58+
| pr_required_review_thread_resolution | Whether to require all review threads on a PR to be resolved before merging. | `bool` | `false` | no |
59+
| rules_creation | If true, only allows users with bypass permission to create matching refs. | `bool` | `false` | no |
60+
| rules_deletion | If true, only allows users with bypass permissions to delete matching refs. | `bool` | `true` | no |
61+
| rules_non_fast_forward | If true, prevents users with push access from force pushing to branches. | `bool` | `true` | no |
62+
| rules_required_linear_history | If true, prevents merge commits from being pushed to matching branches. In other words, any PRs merged into the branch must use a squash merge or a rebase merge. | `bool` | `true` | no |
63+
| rules_required_signatures | If true, commits pushed to matching branches must have verified signatures. | `bool` | `true` | no |
64+
| rules_update | If true, only allows users with bypass permission to update matching refs. | `bool` | `false` | no |
65+
| rules_update_allows_fetch_and_merge | If true, the branch can pull changes from its upstream repository. This is only applicable to forked repositories. Requires `update` to be set to true. | `bool` | `false` | no |
66+
| topics | A list of topics for the repository. | `list(string)` | `[]` | no |
67+
| visibility | Repository visibility: private, public, or internal. | `string` | `"private"` | no |
68+
| vulnerability_alerts | Whether to enable vulnerability alerts. | `bool` | `true` | no |
69+
| web_commit_signoff_required | Whether contributor signoff is required on web commits. | `bool` | `false` | no |
5570

5671
## Outputs
5772

5873
| Name | Description |
5974
|------|-------------|
60-
| kms_key_arn | The ARN for the CMK KMS key used for CloudWatch encryption. |
61-
| log_group_arns | The ARN for each Log Group |
75+
| repository_id | Repository ID |
76+
| repository_name | Repository name |
77+
| repository_url | Repository URL |
6278

6379
## Providers
6480

@@ -77,23 +93,22 @@ module "repositories" {
7793

7894
| Name | Type |
7995
|------|------|
80-
| [github_branch.this](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/branch) | resource |
81-
| [github_branch_default.this](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/branch_default) | resource |
82-
| [github_repository.this](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository) | resource |
83-
| [github_repository_ruleset.this](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository_ruleset) | resource |
96+
| [github_repository.repository](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository) | resource |
97+
| [github_repository_autolink_reference.autolink_reference](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository_autolink_reference) | resource |
98+
| [github_repository_ruleset.ruleset](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository_ruleset) | resource |
8499

85100
***
86101
<!-- END_TF_DOCS -->
87102

88-
### Developer Setup
103+
## Developer Setup
89104

90105
- [Pre-Commit](https://pre-commit.com/)
91106
- [TFenv](https://github.com/tfutils/tfenv)
92107
- [Terraform-Docs](https://terraform-docs.io/)
93108
- [TFLint](https://github.com/terraform-linters/tflint)
94109
- [Trivy](https://trivy.dev/)
95110

96-
Install dependencies (macOS)
111+
### Install Dependencies
97112

98113
```shell
99114
brew install pre-commit tfenv terraform-docs tflint trivy

main.tf

Lines changed: 57 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,48 @@
1-
resource "github_repository" "repository" {
2-
for_each = { for repo in var.repositories : repo.name => repo }
1+
locals {
2+
github_repository_role = {
3+
org_admin = 1
4+
maintain = 2
5+
write = 4
6+
admin = 5
7+
}
8+
}
39

4-
allow_auto_merge = each.value.allow_auto_merge
5-
allow_merge_commit = each.value.allow_merge_commit
6-
allow_rebase_merge = each.value.allow_rebase_merge
7-
allow_squash_merge = each.value.allow_squash_merge
8-
allow_update_branch = each.value.allow_update_branch
9-
archived = each.value.archived
10-
auto_init = each.value.auto_init
11-
delete_branch_on_merge = each.value.delete_branch_on_merge
12-
description = each.value.repo_description
13-
has_discussions = each.value.has_discussions
14-
has_downloads = each.value.has_downloads
15-
has_issues = each.value.has_issues
16-
has_projects = each.value.has_projects
17-
has_wiki = each.value.has_wiki
18-
is_template = each.value.is_template
19-
merge_commit_message = "PR_BODY"
20-
merge_commit_title = "PR_TITLE"
21-
name = each.value.name
22-
squash_merge_commit_message = "COMMIT_MESSAGES"
23-
squash_merge_commit_title = "PR_TITLE"
24-
topics = []
25-
visibility = each.value.repo_visibility
26-
vulnerability_alerts = each.value.vulnerability_alerts
27-
web_commit_signoff_required = each.value.web_commit_signoff_required
10+
resource "github_repository" "repository" {
11+
allow_auto_merge = var.allow_auto_merge
12+
allow_merge_commit = var.allow_merge_commit
13+
allow_rebase_merge = var.allow_rebase_merge
14+
allow_squash_merge = var.allow_squash_merge
15+
allow_update_branch = var.allow_update_branch
16+
archived = var.archived
17+
auto_init = var.auto_init
18+
delete_branch_on_merge = var.delete_branch_on_merge
19+
description = var.description
20+
has_discussions = var.has_discussions
21+
has_downloads = var.has_downloads
22+
has_issues = var.has_issues
23+
has_projects = var.has_projects
24+
has_wiki = var.has_wiki
25+
is_template = var.is_template
26+
merge_commit_message = var.allow_merge_commit ? "PR_BODY" : null
27+
merge_commit_title = var.allow_merge_commit ? "PR_TITLE" : null
28+
name = var.name
29+
squash_merge_commit_message = var.allow_squash_merge ? "COMMIT_MESSAGES" : null
30+
squash_merge_commit_title = var.allow_squash_merge ? "PR_TITLE" : null
31+
topics = var.topics
32+
visibility = var.visibility
33+
vulnerability_alerts = var.vulnerability_alerts
34+
web_commit_signoff_required = var.web_commit_signoff_required
2835
}
2936

3037
resource "github_repository_ruleset" "ruleset" {
31-
for_each = { for repo in var.repositories : repo.name => repo }
38+
count = var.enable_default_ruleset ? 1 : 0
3239
enforcement = "active"
3340
name = var.default_branch_name
34-
repository = github_repository.repository[each.key].name
41+
repository = github_repository.repository.name
3542
target = "branch"
3643

37-
# https://registry.terraform.io/providers/integrations/github/latest/docs/resources/repository_ruleset#bypass_actors
38-
# Org Admin = 1, maintain = 2, write = 4, admin = 5
3944
bypass_actors {
40-
actor_id = 5
45+
actor_id = local.github_repository_role.admin
4146
actor_type = "RepositoryRole"
4247
bypass_mode = "always"
4348
}
@@ -51,21 +56,30 @@ resource "github_repository_ruleset" "ruleset" {
5156
}
5257
}
5358

59+
# Available rules for rulesets:
60+
# https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets
5461
rules {
55-
creation = false
56-
deletion = true
57-
non_fast_forward = true
58-
required_linear_history = true
59-
required_signatures = true
60-
update = false
61-
update_allows_fetch_and_merge = false
62+
creation = var.rules_creation
63+
deletion = var.rules_deletion
64+
non_fast_forward = var.rules_non_fast_forward
65+
required_linear_history = var.rules_required_linear_history
66+
required_signatures = var.rules_required_signatures
67+
update = var.rules_update
68+
update_allows_fetch_and_merge = var.rules_update_allows_fetch_and_merge
6269

6370
pull_request {
64-
dismiss_stale_reviews_on_push = true
65-
require_code_owner_review = false
66-
require_last_push_approval = false
67-
required_approving_review_count = 0
68-
required_review_thread_resolution = false
71+
dismiss_stale_reviews_on_push = var.pr_dismiss_stale_reviews_on_push
72+
require_code_owner_review = var.pr_require_code_owner_review
73+
require_last_push_approval = var.pr_require_last_push_approval
74+
required_approving_review_count = var.pr_required_approving_review_count
75+
required_review_thread_resolution = var.pr_required_review_thread_resolution
6976
}
7077
}
7178
}
79+
80+
resource "github_repository_autolink_reference" "autolink_reference" {
81+
count = var.autolink_reference_prefix != "" && var.autolink_reference_url_template != "" ? 1 : 0
82+
repository = github_repository.repository.name
83+
key_prefix = var.autolink_reference_prefix
84+
target_url_template = var.autolink_reference_url_template
85+
}

outputs.tf

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
output "repository_name" {
2-
description = "Map of repository names"
3-
value = { for key, repo in github_repository.repository : key => repo.name }
2+
description = "Repository name"
3+
value = github_repository.repository.name
44
}
55

6-
output "repository_urls" {
7-
description = "Map of repository URLs"
8-
value = { for key, repo in github_repository.repository : key => repo.html_url }
6+
output "repository_url" {
7+
description = "Repository URL"
8+
value = github_repository.repository.html_url
99
}
1010

11-
output "repository_ids" {
12-
description = "Map of repository IDs"
13-
value = { for key, repo in github_repository.repository : key => repo.id }
11+
output "repository_id" {
12+
description = "Repository ID"
13+
value = github_repository.repository.id
1414
}

0 commit comments

Comments
 (0)