Manage GitHub Repositories using the Terraform GitHub Provider

GitHub has become the standard platform for hosting code. In organizations practicing GitOps, the repository is the single source of truth for application code, infrastructure, configuration, and deployment workflows. However, the repositories themselves are often created manually. Each new repository depends on whoever creates it to enable the right settings: branch protection, vulnerability alerts, secret scanning, and visibility controls. When that responsibility falls to a single person or a separate platform team, it introduces both delay and human error. A missed setting can go unnoticed until a security review surfaces it, or worse, until credentials are exposed in a public repository.

Terraform provides a declarative way to manage this. Most teams already use the AWS provider to provision consistent infrastructure across different AWS accounts. The GitHub provider applies the same principle to repository management. Instead of submitting a request and waiting for manual setup, a team can open a pull request to add its repository definition to a Terraform configuration. The platform team reviews the PR, confirms the settings match organizational standards, and merges it. The CI/CD pipeline runs terraform apply, and the repository is created with every required setting in place. No manual steps, no missed configurations.

In this note, I will show how to use the Terraform GitHub provider to manage GitHub repositories, import existing ones using the import {} block, and enforce branch protection across all public repositories. First, I will cover the repository management setup, then the import process, and finally branch protection.

Prerequisites

This use case utilizes Terraform and GitHub Actions to create and manage GitHub repositories with enforced standards: branch protection, vulnerability alerts, and consistent configuration. The Terraform state is stored in an Amazon S3 bucket.

This use case requires the following prerequisites:

PreReq-1: An Amazon S3 bucket for Terraform remote state.

PreReq-2: An AWS account with an IAM role configured for GitHub Actions via OIDC. This eliminates long-lived credentials. I covered this setup in this note. This IAM role needs permissions to list the S3 bucket and read/write objects to the state file path. Store the role ARN as a GitHub secret named IAM_ROLE.

PreReq-3: A GitHub Personal Access Token (classic) with repo and admin:org scopes. This token authenticates the Terraform GitHub provider and must have access to all repositories you intend to manage. Store this as a GitHub Actions secret named GH_TOKEN.

If you want to follow along, the code for this solution is available in my GitHub repository: kunduso-org/github-repo-terraform. The code includes Terraform configurations, GitHub Actions CI/CD, and security scanning with Checkov.

Implementation

This solution covers the following use cases:
1. Create and manage GitHub repositories
2. Import existing repositories into Terraform state using the import {} block
3. Enable vulnerability alerts
4. Enable branch protection on the default branch

Let me now elaborate on each of these.

Step 1: Create and manage GitHub repositories
I used the github_repository resource with a for_each loop over a locals map. Each repository is defined as an entry in the map with its properties: description, visibility, wiki settings, and homepage URL. This means adding a new repository requires only adding a new entry to the locals file — no new resource blocks.
github_repository resource with for_each
The locals map acts as the single source of truth for all repository configurations. Each entry specifies the exact settings the repository should have, and Terraform ensures the actual state on GitHub matches.
locals map with repository definitions
I configured two provider aliases — one for my personal account (kunduso) and one for the organization (kunduso-org) — so repositories across both accounts are managed from a single Terraform configuration.

Step 2: Import existing repositories into Terraform state
Since my repositories already existed on GitHub, I used the import {} block to bring them under Terraform management without recreating them. The import block tells Terraform to adopt the existing resource into its state file using the repository name as the identifier.
import blocks in imports.tf
Once imported, running terraform plan immediately surfaces any drift between what Terraform declares and what exists on GitHub. In my case, it flagged repositories that were missing homepage URLs and had vulnerability alerts disabled — drift I would not have noticed otherwise.

For a detailed walkthrough of how the import block works and how to automate it with GitHub Actions, see my earlier note on automating AWS resource import into Terraform state.

Step 3: Enable vulnerability alerts
GitHub deprecated the vulnerability_alerts field on the github_repository resource, so I used the dedicated github_repository_vulnerability_alerts resource instead. This resource takes the repository name and a boolean enabled flag.
vulnerability alerts resource
I applied this to all repositories using the same for_each pattern, ensuring every managed repository has vulnerability alerts enabled consistently.

Step 4: Enable branch protection on the default branch
I used the github_branch_protection resource to enforce branch protection rules on the main branch of all public repositories. The for_each expression filters the locals map to include only repositories with visibility = "public" since branch protection requires GitHub Pro for private repositories on the free plan.
branch protection resource
The rules I enforced are:
– Require pull request before merging (no direct push to main)
– No force pushes
– No branch deletion
– Enforce for admins (even repository owners cannot bypass)

I set required_approving_review_count = 0 since I am the only contributor. In a team environment, this would be set to 1 or more to require peer review before merging.

Deployment

The infrastructure is deployed via GitHub Actions using the workflow defined in .github/workflows/terraform.yml. The workflow runs terraform plan on pull requests and terraform apply only when changes are merged into the main branch, using OIDC authentication to obtain secure, temporary AWS credentials for state file access. The GitHub token (GH_TOKEN) is passed to Terraform via the -var flag during both plan and apply.

This ensures that all repository changes are reviewed via pull requests before being applied. The workflow also posts the Terraform plan output as a comment on the PR, providing visibility into exactly what will change before merging.

This repository also includes a code-scanning pipeline (.github/workflows/code-scan.yml) that uses Checkov to scan Terraform configurations for security best practices. For detailed implementation of Checkov with GitHub Actions, see: automate terraform configuration scan with checkov and github actions.

Verification

After merging the changes, the GitHub Actions workflow ran terraform apply and the output confirmed that all repositories were imported into state and the branch protection rules were applied.
Terraform apply output
I verified the branch protection by navigating to one of the public repositories on GitHub. The main branch now shows the protection rules: pull requests required, force pushes disabled, and admin enforcement active.
Branch protection rules on GitHub

Conclusion

In this article, I used the Terraform GitHub provider to manage all my repositories across two GitHub accounts from a single configuration. The setup demonstrates how repository management can follow the same IaC principles that teams already use for cloud infrastructure: declarative definitions, version-controlled changes, and automated enforcement.

Now all my repositories are managed via the GitHub provider. When I need to add a new repository, I add an entry to the locals file and create a pull request. When I need to enforce a new standard across all repositories, I update the resource definition once, and every repository receives the change on the next apply. If the resource already exists on GitHub, I use an import block to bring it under management without recreating it.

If you have any questions or suggestions, feel free to comment or get in touch.

Leave a Reply