Skip to content

Commit

Permalink
Enhance GitHub repository management by adding environment support, r…
Browse files Browse the repository at this point in the history
…efining secret handling, and improving collaborator permissions mapping
  • Loading branch information
Dave Arnold committed Feb 19, 2025
1 parent 30851e4 commit 88899ff
Show file tree
Hide file tree
Showing 11 changed files with 717 additions and 163 deletions.
142 changes: 119 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Terraform GitHub Repository Module

A comprehensive Terraform module for managing GitHub repositories with advanced features like branch protection, file management, and team access control.
A comprehensive Terraform module for managing GitHub repositories with advanced features like branch protection, file management, and team access control. You can use this module to create new repositories or manage existing ones.

## Features

- Create new repositories or manage existing ones
- Complete GitHub repository management
- Branch protection rules
- File content management
Expand All @@ -15,37 +15,35 @@ A comprehensive Terraform module for managing GitHub repositories with advanced

## Usage

### Creating a New Repository
```hcl
module "repository" {
module "new_repository" {
source = "HappyPathway/repo/github"
name = "my-repository"
repo_org = "MyOrganization"
create_repo = true # Default, can be omitted
force_name = true
github_repo_description = "Repository description"
github_repo_topics = ["terraform", "automation"]
github_is_private = false
github_has_issues = true
github_has_projects = true
github_has_wiki = true
vulnerability_alerts = true
gitignore_template = "Node"
# Managed file content
managed_extra_files = {
"README.md" = {
content = file("${path.module}/templates/readme.md")
overwrite = true
}
"docs/getting-started.md" = {
content = file("${path.module}/templates/getting-started.md")
overwrite = false
}
}
}
```

## Examples
### Managing an Existing Repository
```hcl
module "existing_repository" {
source = "HappyPathway/repo/github"
name = "existing-repository"
repo_org = "MyOrganization"
create_repo = false # Tell Terraform to manage existing repository
# All other settings will be applied to the existing repository
github_repo_topics = ["managed", "terraform"]
github_has_issues = true
}
```

### Basic Repository

Expand Down Expand Up @@ -99,10 +97,108 @@ module "managed_repo" {
}
```

## Inputs

| Name | Description | Type | Required | Default |
|------|-------------|------|----------|---------|
| name | Repository name | string | Yes | - |
| repo_org | GitHub organization name | string | No | null |
| create_repo | Whether to create a new repository or manage existing | bool | No | true |
| force_name | Keep exact repository name (no date suffix) | bool | No | false |
| github_repo_description | Repository description | string | No | null |
| github_repo_topics | Repository topics | list(string) | No | [] |
| github_is_private | Make repository private | bool | No | true |
| // ...other inputs... |

## Outputs

| Name | Description |
|------|-------------|
| github_repo | All repository attributes (see details below) |
| ssh_clone_url | SSH clone URL |
| node_id | Repository node ID for GraphQL |
| full_name | Full repository name (org/repo) |
| repo_id | Repository ID |
| html_url | Repository web URL |
| http_clone_url | HTTPS clone URL |
| git_clone_url | Git protocol clone URL |
| visibility | Repository visibility (public/private) |
| default_branch | Default branch name |
| topics | Repository topics |
| template | Template repository info |

### Complete Repository Attributes

The `github_repo` output includes:

Basic Info:
- `name` - Repository name
- `full_name` - Full repository name (org/repo)
- `description` - Repository description
- `html_url` - GitHub web URL
- `ssh_clone_url` - SSH clone URL
- `http_clone_url` - HTTPS clone URL
- `git_clone_url` - Git protocol URL
- `visibility` - Public or private status

Settings:
- `topics` - Repository topics
- `has_issues` - Issue tracking enabled
- `has_projects` - Project boards enabled
- `has_wiki` - Wiki enabled
- `is_template` - Template repository status
- `allow_merge_commit` - Merge commit allowed
- `allow_squash_merge` - Squash merge allowed
- `allow_rebase_merge` - Rebase merge allowed
- `allow_auto_merge` - Auto-merge enabled
- `delete_branch_on_merge` - Branch deletion on merge

Additional Info:
- `default_branch` - Default branch name
- `archived` - Archive status
- `homepage_url` - Homepage URL if set
- `vulnerability_alerts` - Vulnerability alerts status
- `template` - Template repository details if used
- `gitignore_template` - .gitignore template if used
- `license_template` - License template if used

## Limitations and Important Notes

### Managing Existing Repositories
When managing existing repositories (`create_repo = false`):
- The repository must already exist in the specified organization
- You must have admin access to the repository
- Some settings may be read-only if they were set during repository creation
- Initial repository settings (like `auto_init`) are ignored
- Branch protection rules can only be added, not removed

### Error Cases
The module will fail if:
- When `create_repo = false` and the repository doesn't exist
- When `create_repo = false` and `repo_org` is not specified
- When trying to manage a repository you don't have admin access to
- When applying branch protection rules to a private repository without a GitHub Enterprise plan

### Best Practices
1. When managing existing repositories:
- Start with `create_repo = false` and minimal settings
- Gradually add configuration to avoid conflicts
- Use `terraform plan` to verify changes
- Consider using `lifecycle` blocks to ignore specific attributes

2. For new repositories:
- Use `create_repo = true` (default)
- Set `force_name = true` to maintain consistent naming
- Configure all settings during initial creation

## Testing

This module includes automated tests using Terraform's built-in testing framework:
This module includes automated tests that verify:
- Repository creation
- Data source lookups for existing repositories
- All output attributes

Run the tests using:
```bash
terraform test
```
Expand Down
10 changes: 8 additions & 2 deletions action_secrets.tf
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
data "github_actions_public_key" "repo_key" {
repository = local.github_repo.name
}

resource "github_actions_secret" "secret" {
for_each = tomap({ for secret in var.secrets : secret.name => secret.value })
secret_name = each.key
plaintext_value = each.value
repository = local.github_repo.name
secret_name = each.key
encrypted_value = base64encode(each.value)

depends_on = [data.github_actions_public_key.repo_key]
}

resource "github_actions_variable" "variable" {
Expand Down
22 changes: 21 additions & 1 deletion collaborators.tf
Original file line number Diff line number Diff line change
@@ -1,7 +1,27 @@
locals {
# Permission mapping for collaborator roles
permission_map = {
"pull" = "read"
"triage" = "triage"
"push" = "write"
"maintain" = "maintain"
"admin" = "admin"
}
}

data "github_user" "collaborators" {
for_each = var.collaborators
username = each.key
}

# Add a collaborator to a repository
resource "github_repository_collaborator" "collaborators" {
for_each = tomap(var.collaborators)
repository = local.github_repo.name
username = each.key
permission = each.value
permission = local.permission_map[each.value]

depends_on = [
data.github_user.collaborators
]
}
56 changes: 56 additions & 0 deletions environment.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
resource "github_repository_environment" "environments" {
for_each = { for env in var.environments : env.name => env }

environment = each.value.name
repository = github_repository.repo[0].name
reviewers {
teams = try(each.value.reviewers.teams, [])
users = try(each.value.reviewers.users, [])
}
deployment_branch_policy {
protected_branches = try(each.value.deployment_branch_policy.protected_branches, true)
custom_branch_policies = try(each.value.deployment_branch_policy.custom_branch_policies, false)
}
}

resource "github_actions_environment_secret" "environment_secrets" {
for_each = {
for pair in flatten([
for env in var.environments : [
for secret in coalesce(env.secrets, []) : {
env_name = env.name
name = secret.name
value = secret.value
}
]
]) : "${pair.env_name}.${pair.name}" => pair
}

repository = github_repository.repo[0].name
environment = each.value.env_name
secret_name = each.value.name
plaintext_value = each.value.value

depends_on = [github_repository_environment.environments]
}

resource "github_actions_environment_variable" "environment_variables" {
for_each = {
for pair in flatten([
for env in var.environments : [
for _var in coalesce(env.vars, []) : {
env_name = env.name
name = _var.name
value = _var.value
}
]
]) : "${pair.env_name}.${pair.name}" => pair
}

repository = github_repository.repo[0].name
environment = each.value.env_name
variable_name = each.value.name
value = each.value.value

depends_on = [github_repository_environment.environments]
}
44 changes: 24 additions & 20 deletions github_branch.tf
Original file line number Diff line number Diff line change
Expand Up @@ -33,35 +33,39 @@ locals {

# https://registry.terraform.io/providers/integrations/github/latest/docs/resources/branch_protection
resource "github_branch_protection" "main" {
count = var.enforce_prs && !var.github_is_private ? 1 : 0
enforce_admins = var.github_enforce_admins_branch_protection
pattern = var.github_default_branch
# push_restrictions = var.github_push_restrictions
count = (var.enforce_prs && !var.github_is_private) || var.github_is_private ? 1 : 0

repository_id = local.github_repo.node_id
pattern = var.github_default_branch

# Basic protection settings
enforce_admins = var.github_enforce_admins_branch_protection
allows_deletions = false
allows_force_pushes = false
require_signed_commits = true
required_linear_history = true
require_conversation_resolution = true

required_status_checks {
strict = try(var.required_status_checks.strict, false)
contexts = try(var.required_status_checks.contexts, [])
}

required_pull_request_reviews {
dismiss_stale_reviews = var.github_dismiss_stale_reviews
restrict_dismissals = true
pull_request_bypassers = var.pull_request_bypassers
require_code_owner_reviews = var.github_require_code_owner_reviews
required_approving_review_count = var.github_required_approving_review_count
pull_request_bypassers = local.pull_request_bypassers
}

restrict_pushes {
push_allowances = var.github_push_restrictions
}

lifecycle {
ignore_changes = [
required_status_checks[0].contexts
]
}

dynamic "required_status_checks" {
for_each = var.required_status_checks == null ? [] : ["*"]
content {
contexts = required_status_checks.value.contexts
strict = required_status_checks.value.strict
}
}

depends_on = [
# first let the automation create the codeowners and backend file then only create branch protection rule
# if branch protection rule is created first, codeowners will fail
github_repository_file.codeowners,
github_repository_file.extra_files
]
}
9 changes: 9 additions & 0 deletions github_files.tf
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ resource "github_repository_file" "codeowners" {
branch = var.github_default_branch
file = "CODEOWNERS"
content = templatefile("${path.module}/templates/CODEOWNERS", { codeowners = local.codeowners })
commit_message = "Update CODEOWNERS file"
commit_author = "Terraform"
commit_email = "terraform@example.com"
overwrite_on_create = true
lifecycle {
ignore_changes = [
Expand Down Expand Up @@ -45,6 +48,9 @@ resource "github_repository_file" "extra_files" {
branch = var.github_default_branch
file = each.value.path
content = each.value.content
commit_message = "Update ${each.value.path}"
commit_author = "Terraform"
commit_email = "terraform@example.com"
overwrite_on_create = true
lifecycle {
ignore_changes = [
Expand All @@ -60,6 +66,9 @@ resource "github_repository_file" "managed_extra_files" {
branch = var.github_default_branch
file = each.value.path
content = each.value.content
commit_message = "Update ${each.value.path}"
commit_author = "Terraform"
commit_email = "terraform@example.com"
overwrite_on_create = true
lifecycle {
ignore_changes = [
Expand Down
Loading

0 comments on commit 88899ff

Please sign in to comment.