Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions infra/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,8 @@ backend.hcl
*.auto.tfvars
terraform.tfvars
!terraform.tfvars.example

# Local backend overrides (developer-specific)
backend_override.tf
*_override.tf
!*_override.tf.example
24 changes: 24 additions & 0 deletions infra/github/.terraform.lock.hcl

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

47 changes: 47 additions & 0 deletions infra/github/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# GitHub repo configuration (OpenTofu)

Manages branch protection (via repository rulesets) and deployment
environments for `JMR-dev/dev_blog`.

This module is also the source-of-truth copy that is mirrored to the
[`gh-repo-bootstrap`](https://github.com/JMR-dev/gh-repo-bootstrap) repo,
which packages it as a reusable module + `gh` CLI extension
(`gh repo-bootstrap <owner>/<repo>`).

## Prerequisites

- [OpenTofu](https://opentofu.org/) >= 1.8
- A GitHub token with `repo` + `admin:repo_hook` scopes. Easiest:
```sh
export GITHUB_TOKEN=$(gh auth token)
```
- (For remote state) Cloudflare R2 credentials — copy
`backend.hcl.example` to `backend.hcl` and fill it in.

## Usage

```sh
# First time only:
tofu init -backend-config=backend.hcl

# Apply:
tofu apply
```

To run with **local state** (no R2 needed) for experimentation, comment
out the `backend "s3"` block in `versions.tf`, then `tofu init` again.

## What it manages

- A repository ruleset on the default branch enforcing:
- No deletion
- No force-push
- Required PR with N approving reviews (configurable)
- Resolved review threads
- Optional signed commits
- The set of GitHub deployment environments listed in `environments`.

It does **not** create the repository itself, and does not manage
repo-level settings (merge button options, default branch, topic, etc.).
Add those via `gh api` or import the `github_repository` resource if you
need them under OpenTofu control.
8 changes: 8 additions & 0 deletions infra/github/backend.hcl.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Example Cloudflare R2 backend configuration.
# Copy to `backend.hcl` (gitignored) and fill in your values, then run:
# tofu init -backend-config=backend.hcl

bucket = "dev-blog-tfstate"
endpoints = { s3 = "https://<account-id>.r2.cloudflarestorage.com" }
access_key = "<R2_ACCESS_KEY_ID>"
secret_key = "<R2_SECRET_ACCESS_KEY>"
46 changes: 46 additions & 0 deletions infra/github/main.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
data "github_repository" "this" {
name = var.repo_name
}

resource "github_repository_ruleset" "default_branch" {
repository = data.github_repository.this.name
name = var.ruleset_name
target = "branch"
enforcement = "active"

conditions {
ref_name {
include = ["refs/heads/${var.default_branch}"]
exclude = []
}
}

dynamic "bypass_actors" {
for_each = var.bypass_actors
content {
actor_id = bypass_actors.value.actor_id
actor_type = bypass_actors.value.actor_type
bypass_mode = bypass_actors.value.bypass_mode
}
}

rules {
deletion = true
non_fast_forward = true
required_signatures = var.require_signed_commits

pull_request {
required_approving_review_count = var.required_reviews
dismiss_stale_reviews_on_push = true
require_code_owner_review = false
require_last_push_approval = false
required_review_thread_resolution = true
}
}
}

resource "github_repository_environment" "envs" {
for_each = toset(var.environments)
repository = data.github_repository.this.name
environment = each.value
}
14 changes: 14 additions & 0 deletions infra/github/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
output "repository_full_name" {
value = data.github_repository.this.full_name
description = "Full name (owner/repo) of the repository being managed."
}

output "ruleset_id" {
value = github_repository_ruleset.default_branch.id
description = "ID of the branch protection ruleset."
}

output "environments" {
value = sort([for e in github_repository_environment.envs : e.environment])
description = "Environments managed by this configuration."
}
11 changes: 11 additions & 0 deletions infra/github/terraform.tfvars.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
repo_owner = "JMR-dev"
repo_name = "dev_blog"
default_branch = "main"
required_reviews = 1
require_signed_commits = false
environments = [
"production",
"staging",
"preview",
"development",
]
56 changes: 56 additions & 0 deletions infra/github/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
variable "repo_owner" {
description = "GitHub user or organization that owns the repository."
type = string
}

variable "repo_name" {
description = "Repository name (without owner prefix)."
type = string
}

variable "default_branch" {
description = "Branch protected by the ruleset."
type = string
default = "main"
}

variable "required_reviews" {
description = "Number of required approving reviews on PRs targeting the default branch."
type = number
default = 1
}

variable "require_signed_commits" {
description = "Require signed commits on the protected branch."
type = bool
default = false
}

variable "environments" {
description = "List of GitHub deployment environments to ensure exist."
type = list(string)
default = []
}

variable "ruleset_name" {
description = "Name to give the branch protection ruleset."
type = string
default = "default-branch-protection"
}

variable "bypass_actors" {
description = <<-EOT
Actors permitted to bypass the ruleset. Each entry needs:
- actor_id: numeric ID (for built-in repo roles: 1=read, 2=triage,
3=write, 4=maintain, 5=admin)
- actor_type: one of RepositoryRole, Team, Integration,
OrganizationAdmin, DeployKey
- bypass_mode: "always" or "pull_request"
EOT
type = list(object({
actor_id = number
actor_type = string
bypass_mode = string
}))
default = []
}
33 changes: 33 additions & 0 deletions infra/github/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
terraform {
required_version = ">= 1.8.0"

required_providers {
github = {
source = "integrations/github"
version = "~> 6.2"
}
}

# Cloudflare R2 (S3-compatible) backend, mirroring infra/versions.tf.
# Account-specific values are supplied at init time:
# tofu init -backend-config=backend.hcl
# For local-only experimentation, comment this entire block out and
# OpenTofu will fall back to the local backend.
backend "s3" {
key = "dev_blog/github.tfstate"
region = "auto"

skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
skip_requesting_account_id = true
skip_s3_checksum = true
use_path_style = true
}
}

provider "github" {
owner = var.repo_owner
# Token is read from the GITHUB_TOKEN environment variable.
# Easiest source: `export GITHUB_TOKEN=$(gh auth token)`.
}
Loading