GH Mass-administration: Terraform

This is a part of my guide to how I manage multiple Github repositories. This post focuses on configuring repository settings on Github using Terraform. Back to main guide.

For my use case, I need to control default settings as well as maintaining secrets that will be used in actions. To simplify my setup, I'm sharing the same NuGet and Docker Hub publishing keys among all my repositories. These keys may expire over time, but using Terraform, it's easy to keep them up to date.


I'm fond of Terraform, when I need to declaratively describe resources or other static elements in f.ex. AWS. Terraform has many plugins, one of which is for Github.

I've created a file which contains the following - be sure to replace the variables as needed:

locals {
  github_owner = "LordMike"
  github_token = "REPLACE_ME"

  # Repositories
  repositories = jsondecode(file("repos.json")).repositories
  repos        = keys(local.repos)
  repos_public = [for r in local.repos : r if lookup(local.repositories[r], "public", true)]

  # Nuget reposistories
  nuget_key   = "REPLACE_ME"
  nuget_repos = [for r in local.repos : r if lookup(local.repositories[r], "nuget", true)]

  # Docker repositories
  docker_username = "lordmike"
  docker_key      = "REPLACE_ME"
  docker_repos    = [for r in local.repos : r if lookup(local.repositories[r], "docker", false)]

provider "github" {
  owner = local.github_owner
  token = local.github_token

resource "github_repository" "repository" {
  for_each = toset(local.repos)

  name        = split("/", each.key)[1]
  description = lookup(local.repositories[each.key], "description", "")
  topics      = lookup(local.repositories[each.key], "topics", null)

  has_wiki               = lookup(local.repositories[each.key], "has_wiki", false)
  has_projects           = lookup(local.repositories[each.key], "has_projects", false)
  has_issues             = lookup(local.repositories[each.key], "has_issues", true)
  has_downloads          = lookup(local.repositories[each.key], "has_downloads", false)
  delete_branch_on_merge = lookup(local.repositories[each.key], "delete_branch_on_merge", true)

resource "github_branch_default" "default_branch"{
  for_each = toset(local.repos)

  repository = split("/", each.key)[1]
  branch     = "master"

resource "github_branch_protection" "protect_master" {
  for_each = toset(local.repos_public)

  repository_id = split("/", each.key)[1]

  pattern             = "master"
  enforce_admins      = false
  allows_deletions    = false
  allows_force_pushes = false

resource "github_actions_secret" "nuget_key" {
  for_each = toset(local.nuget_repos)

  repository      = split("/", each.key)[1]
  secret_name     = "NUGET_KEY"
  plaintext_value = local.nuget_key

resource "github_actions_secret" "docker_username" {
  for_each = toset(local.docker_repos)

  repository      = split("/", each.key)[1]
  secret_name     = "DOCKER_USERNAME"
  plaintext_value = local.docker_username

resource "github_actions_secret" "docker_key" {
  for_each = toset(local.docker_repos)

  repository      = split("/", each.key)[1]
  secret_name     = "DOCKER_KEY"
  plaintext_value = local.docker_key

Tricks used in this file:

  • for_each: repeat a resource for each key in a set. I create filtered sets, like nuget_repos to simplify the resources.
  • Derived locals: to simplify most of the file, I create helper variables like docker_repos that use a terraform lambda expression to filter the whole set of repositories.
  • lookup: to create defaults. I "lookup" a property, like has_wiki, where I can default to false. If I wanted the default to be true, I can simply change the lookup expression.

Terraform imports, a short note

Like me, you're probably using existing repositories. Terraform can be made aware of these resources by importing them. I found that to import them, I need to escape their resource name.

# Import a repository, note the escaped name in the indexer
terraform import 'github_repository.repository[\"LordMike/MBW.Utilities.ReflectedCast\"]' LordMike/MBW.Utilities.ReflectedCast

# Import an existing default branch, or other resources in the same fashion. `terraform apply` will inform you when it cannot create a resource that exists.
terraform import 'github_branch_default.default_branch[\"LordMike/MBW.Utilities.ReflectedCast\"]' LordMike/MBW.Utilities.ReflectedCast

Terraform apply

Apply the state after setting up your repos.json and resources. Note that Githubs API can be slow at times, so once your initial statefile has been built by the first terraform apply / refresh, you can skip refreshing it again.

# Refresh terraform, fetch all states from Github
terraform refresh

# Plan and apply changes
terraform apply

# Plan and apply, but do not refresh first
terraform apply -refresh=false