How We Scaled Code Repository Management at DNSimple
Managing a handful of GitHub repositories is straightforward. Managing hundreds of them consistently is a challenge. Over the years at DNSimple, we've evolved from manual configuration to a fully automated Infrastructure as Code (IaC) approach. This is the story of that evolution, the lessons we learned, and how we built a system that now manages all our GitHub resources through pull requests and CI/CD pipelines.
At DNSimple, we've managed our internal infrastructure as code since day one, primarily using Chef for configuration management. Infrastructure as Code wasn't new to us, it was the foundation of how we operated. The challenge was applying these same principles to externally managed resources like GitHub repositories, which required a different approach than our traditional internal infrastructure management.
The problem
As our engineering organization grew, so did our repository count. What started as a few dozen repositories quickly became hundreds, spread across multiple teams and projects. With that growth came inconsistency:
- Repositories had different settings, even when they should have been identical
- Labels for issue triage varied from repository to repository, making cross-project tracking difficult
- Some repositories had issue templates, while others didn't
- Permission management was manual and error-prone
- Security features like vulnerability alerts weren't consistently enabled
- Pull request templates were copy-pasted (when they existed at all)
The root problem wasn't just inconsistency, it was that we had no scalable way to maintain standards across our entire repository portfolio. Every new repository meant manually configuring dozens of settings. Every policy change meant updating hundreds of repositories by hand.
First attempt: Repocop
In our first attempt to solve this problem, we created a tool called Repocop, named after the Ruby linter RuboCop. It was a collection of Ruby scripts built on Thor that would interact with GitHub's API to configure repositories.
The tool supported several operations through individual commands:
thor repo:settings dnsimple/repository-name
thor repo:labels dnsimple/repository-name
thor repo:features dnsimple/repository-name
thor repo:permissions dnsimple/repository-name
thor repo:templates dnsimple/repository-name
Or you could run them all at once with a shell script:
#!/bin/sh
thor repo:settings $1
thor repo:labels $1
thor repo:features $1
thor repo:permissions $1
thor repo:templates $1
The tool could configure repository settings like merge strategies and branch deletion policies:
REPO_SETTINGS = {
has_downloads: false,
has_issues: true,
has_projects: false,
has_wiki: false,
allow_squash_merge: true,
allow_merge_commit: false,
allow_rebase_merge: false,
delete_branch_on_merge: true,
}
It managed a standardized set of labels across all repositories:
REPO_LABELS = {
"bug" => { color: "e10c02", description: "Code defect or incorrect behavior" },
"enhancement" => { color: "1e5184", description: "New feature, enhancement or code changes" },
"task" => { color: "444444", description: "Task to be performed" },
"triage" => { color: "ededed", description: "Issues that are opened and need to be investigated" },
"dependencies" => { color: "0366d6", description: "Pull requests that update a dependency file" },
# ...
}
And it could create or update template files like issue templates:
def templates(repo)
puts "Updating templates..."
filename = "generic-issue.md"
content = File.read(File.join("templates", filename))
ClientGithub.template_create_or_update(ClientGithub.client, repo, "Generic Issue", content, ".github/ISSUE_TEMPLATE/#{filename}")
end
Why Repocop failed
On paper, Repocop solved the immediate problem: it allowed us to apply configurations programmatically rather than manually. In practice, it was quickly forgotten.
The fundamental issues were:
-
Not automated: Someone had to remember to run it from their local workstation. For new repositories, this rarely happened.
-
No visibility: Changes happened locally, with no review process or history of what changed when.
-
Convoluted to maintain: Adding new features or updating configurations required modifying Ruby code and ensuring local dependencies were up to date.
-
No ownership model: There was no clear answer to "who owns which repositories?" or "what's the current state of all our repos?"
-
Poor developer experience: Setting up the tool, managing credentials, and running commands was tedious. The barrier to entry meant most team members never used it.
Building it wasn't enjoyable, and getting people to use it was even harder.
Second attempt: Terraform
In 2022, we started an experiment. Inspired by our work on the DNSimple Terraform provider, we wanted to explore Terraform as a tool for managing external resources. We decided to build a proof-of-concept using the Terraform GitHub provider to manage repositories.
Initially, we didn't know we were going to replace Repocop. We were just exploring how to apply Infrastructure as Code principles to externally managed resources and looking for a practical project to learn from.
We approached the problem from a different angle: repository ownership. Instead of thinking about "how do we apply settings to repositories" we asked "how do we track which repositories exist, who owns them, and what their state should be?" This approach aligned with our broader commitment to automation across all infrastructure.
The first version was simple. It tracked:
- Repository creation, updates, and deletion
- Repository labels
- Team permissions
These were the fundamental building blocks we needed to answer questions like "what repositories belong to the platform team?" and "which repos are using the Go language?"
We structured our Terraform configuration around a map variable that made it easy to define repositories declaratively:
variable "repos" {
type = map(object({
description = optional(string)
visibility = string
has_issues = optional(bool, true)
squash_merge_commit_message = optional(string, "COMMIT_MESSAGES")
vulnerability_alerts = optional(bool, false)
topics = list(string)
permissions = optional(list(string), [])
archive_on_destroy = optional(bool, true)
}))
}
Adding a new repository became as simple as adding an entry to this map:
"dnsimple-redirector" = {
description = "Redirector written in Go."
visibility = "private"
vulnerability_alerts = true
topics = ["dnsimple-policy-eng", "dnsimple-policy-lang-golang"]
permissions = ["engineering_write", "platform_maintain"]
}
The topics field became our tagging system, allowing us to categorize repositories by language (dnsimple-policy-lang-golang), team ownership (dnsimple-policy-triage-platform), and engineering policies (dnsimple-policy-eng).
We rolled out the first version at the beginning of 2023. For about a year, it ran almost unchanged. The Terraform state was managed, but plans and applies were still executed from individual workstations using locally installed Terraform.
This was already better than Repocop (at least we had a single source of truth in Git), but it still wasn't great.
The turning point: CI/CD and Terraform Cloud
The project truly took off when we moved to fully automated CI/CD provisioning through GitHub Actions and Terraform Cloud at the beginning of 2024.
This was the transformation that changed everything.
We created two GitHub Actions workflows. The first runs on every pull request, executing a Terraform plan and posting the results as a PR comment:
on:
pull_request:
paths:
- 'workspace/**/*'
jobs:
terraform-plan:
runs-on: ubuntu-latest
steps:
- name: Create Plan Run
uses: hashicorp/tfc-workflows-github/actions/create-run@v1.3.2
id: plan-run
with:
workspace: $
configuration_version: $
plan_only: true
- name: Update PR
uses: actions/github-script@v8
script: |
const output = `#### Terraform Cloud Plan Output
Plan: $ to add,
$ to change,
$ to destroy.
[View in Terraform Cloud]($)`;
github.rest.issues.createComment({
issue_number: context.issue.number,
body: output
});
The second workflow automatically applies approved changes when they're merged to main:
on:
push:
branches:
- main
paths:
- 'workspace/**/*'
jobs:
terraform:
steps:
- name: Apply
uses: hashicorp/tfc-workflows-github/actions/apply-run@v1.3.2
with:
run: $
This changed the game completely.
The pull request workflow, which we've long valued for collaboration, became the foundation of our infrastructure changes.
Everyone could now see the plan immediately. You didn't need Terraform installed locally. You didn't need to understand Terraform's internals. You just opened a pull request, and GitHub Actions showed you exactly what would change.
Applying changes became instantaneous. Once approved, merging the PR automatically triggered the apply. No more "can someone with Terraform set up run this for me?"
Full visibility and history. Every change went through code review. We could see who requested what, who approved it, and when it was applied. We could roll back by reverting a commit.
Expanding beyond basic settings
Once team members saw how easy it was to propose changes through pull requests, contributions exploded.
People started proposing additions to track common template files. We added support for managing pull request templates across all repositories:
resource "github_repository_file" "infra-github__pull_request_template" {
repository = github_repository.infra-github.name
branch = "main"
file = ".github/pull_request_template.md"
content = file("${path.module}/templates/github/PULL_REQUEST_TEMPLATE/1-standard.md")
commit_message = "chore: Update pull request template"
commit_author = "Terraform"
commit_email = "terraform@dnsimple.com"
overwrite_on_create = true
}
We added issue templates:
resource "github_repository_file" "infra-github__generic_issue_template" {
repository = github_repository.infra-github.name
file = ".github/ISSUE_TEMPLATE/1-generic-issue.md"
content = file("${path.module}/templates/github/ISSUE_TEMPLATE/1-generic-issue.md")
commit_message = "chore: Update generic issue template"
commit_author = "Terraform"
commit_email = "terraform@dnsimple.com"
overwrite_on_create = true
}
We started managing CODEOWNERS files to ensure the right teams were automatically tagged for review:
resource "github_repository_file" "infra-github__codeowners" {
repository = github_repository.infra-github.name
file = ".github/CODEOWNERS"
content = file("${path.module}/templates/github/CODEOWNERS/platform.txt")
}
These tools improved how we communicate progress on GitHub across teams.
We eventually extended this to common configuration files we wanted to distribute across projects based on language or framework: linter configurations, CI templates, and other standards.
This is when we realized: we were officially replacing Repocop. And nobody even noticed the transition because the new system was so much better.
Repocop hadn't been used since early 2024. In June 2025, we officially deleted it from our codebase.
The Terraform ecosystem grows
The success of our GitHub infrastructure project inspired us to adopt Terraform for other infrastructure domains. In 2024 and 2025, we rolled out additional Terraform-based projects:
infra-dnsimple: Managing our domains and DNS zones using the DNSimple Terraform providerinfra-provider1: Managing our cloud Provider1 infrastructureinfra-provider2: Managing our bare metal Provider2 infrastructure
Each project follows the same pattern: declarative configuration, pull request workflow, automated CI/CD, and Terraform Cloud for state management.
The benefits we realized
Looking back at this evolution, the benefits are clear and significant:
Centralized management
Permission management is now centralized through the GitHub pull request workflow. We no longer need individual team members acting as administrators and manually applying changes based on requests. This eliminates the risk of misinterpretation and ensures consistency.
Code review for infrastructure
All changes are scripted and prepared as code changes in a pull request. Changes are reviewed, discussed, and approved before being merged. This brings the same rigor to infrastructure management that we apply to application code.
Automated rollout
When a commit lands in main, the rollout is automatic. No manual steps and no waiting for someone with the right permissions to run a command.
Full control and easy rollback
Since everything is code in Git, we have complete history and the ability to roll back simply by reverting a commit. We can answer questions like "when did we change this repository's settings?" by looking at the Git history.
Bulk changes at scale
Need to update the pull request template across 200 repositories? It's a single line change in the template file and a pull request. Terraform handles applying it everywhere.
Before Terraform, this would have meant either manually updating 200 repositories or writing a custom script to do it.
AI-assisted operations
Since our infrastructure is code, we can now leverage AI agents and tools to collaborate more effectively. Want to know which repositories use a specific label? Ask an AI to search the Terraform configuration. Need to make a complex bulk change? AI can help generate the Terraform code.
Team ownership and discoverability
We can now answer questions we couldn't before:
- "Which repositories does the platform team maintain?" → Search for
platform_maintainin permissions - "What Go projects do we have?" → Search for the
dnsimple-policy-lang-golangtopic - "Which repositories have vulnerability alerts enabled?" → Query the Terraform state
This information was impossible to gather systematically with our previous approaches.
Contributing workflow
Each repository has a CONTRIBUTING.md file that clearly documents how anyone on the team can propose changes:
- Create a branch and make your changes
- Open a pull request (GitHub Actions automatically runs
terraform plan) - Review the plan output in the PR comment
- Request review from the platform team
- Once approved, merge (GitHub Actions automatically runs
terraform apply) - Monitor the apply in Terraform Cloud
This workflow is accessible to everyone on the team, regardless of their Terraform expertise.
Lessons learned
This journey taught us several valuable lessons:
Automation isn't enough. Repocop was automated, but it still required human intervention to run. True automation means no humans in the loop for routine operations.
Developer experience matters more than we thought. The difference between "run this script locally" and "open a pull request" is enormous. The latter is familiar, visible, and collaborative.
Applying Infrastructure as Code to external resources pays off. While we were already proficient with IaC for internal infrastructure, adapting our practices to external resources like GitHub required learning new tools. The investment in Terraform paid dividends, giving us a scalable, maintainable system that will serve us for years.
Start with ownership and discoverability. By focusing first on "what repositories exist and who owns them," we built a foundation that made everything else easier.
Incremental adoption works. We didn't try to migrate everything at once. We started with a proof of concept, validated the approach, and gradually expanded the scope.
The right tools amplify good practices. Terraform's declarative model, combined with GitHub Actions and Terraform Cloud, created a workflow that encouraged contributions and collaboration.
What's next
We're continuing to evolve our code and practices. Some areas we're exploring:
- More sophisticated repository templates based on project type
- Automated compliance checking and reporting
- Cross-repository dependency tracking
- Integration with our internal documentation systems
The system we've built is flexible enough to accommodate these future needs without requiring fundamental changes to our approach.
Conclusion
The journey from a forgotten collection of Ruby scripts to a fully automated Terraform-based system took several years, but the transformation has been worth it. We now manage hundreds of repositories with confidence, consistency, and minimal manual effort.
If you're facing similar challenges with repository management at scale, I'd encourage you to apply Infrastructure as Code principles to your external resources. If you're already using IaC for internal infrastructure, extending those practices to external services like GitHub is a natural evolution. Start small, focus on the workflow and developer experience, and build incrementally. The investment will pay off.
The tools and patterns we've adopted aren't specific to DNSimple or GitHub. They're broadly applicable to any infrastructure management challenge. Whether you're managing repositories, cloud resources, or DNS zones (we manage ours with Terraform too), the principles remain the same:
- Treat infrastructure as code
- Automate the workflow
- Make it easy for everyone to contribute
Ready to bring the same level of automation to your DNS management? Give DNSimple a try free for 30 days, and see how we can help you manage your domains and DNS as code. And as always, if you have any questions about your DNS configuration, domain management, or the new features, get in touch — we'd love to help.
Happy automating!
Simone Carletti
Italian software developer, a PADI scuba instructor and a former professional sommelier. I make awesome code and troll Anthony for fun and profit.
We think domain management should be easy.
That's why we continue building DNSimple.