GitHub repositories are infrastructure too
My team looks after multiple products, each with its own code repository and a separate repository for its Terraform infrastructure. Add shared .NET libraries, Terraform modules, and some repositories for external documentation, and we are responsible for roughly 25 GitHub repositories.
Most fall into four or five recognizable categories. Repositories in the same category often need similar settings, secrets, and GitHub Actions workflows. But similar doesn’t mean identical, and a workflow that makes sense for an application may have no business being in a documentation repository.
I was reminded of that when I needed to distribute a workflow that monitored comments and triggered an action when a comment matched a particular string. The workflow also needed a secret. Neither belonged in every repository.
It should have been a fairly ordinary maintenance task. Instead, it exposed a problem with how I was managing our GitHub configuration.
The script that kept growing
I already had an imperative script for configuring repositories. It made GitHub API calls and applied settings to the repositories we controlled.
It worked, but it was broad: the script treated repositories much the same way regardless of what they were for. Every new requirement pushed more logic into the same place. API endpoints, pagination, error handling, and resilience had already made it more complex than I wanted. Repository pattern matching would have been another addition to that growing pile.
I could have kept adding conditions: apply this workflow to these repositories, set this secret in those, skip documentation repositories, and so on. But the code would then be responsible for both deciding what each repository should look like and figuring out how to make GitHub look that way.
That was the more useful problem to solve. The issue wasn’t that a script couldn’t automate GitHub. It was that the intent was buried in the automation.
Describe, discover, reconcile
That was the starting point for Octosmith: a declarative tool for orchestrating GitHub resources.
The approach is straightforward. Describe the configuration you want in YAML. Use selectors to decide which repositories a template applies to. Discover the repositories, inspect their current configuration, and produce a plan showing what needs to change. Apply the plan when you’re ready.
There are two layers to the configuration. The root octosmith.yml selects the organization and the set of repositories Octosmith should consider, and establishes how to treat repositories that don’t match a template. For example:
# octosmith.yml
version: 1
organization: acme
repositories:
scope:
include:
teams:
- product-team
settings:
collection_management: explicit
unmatched_repositories: ignore
Here, Octosmith considers only repositories associated with the product-team team. Repositories outside that scope aren’t considered, while repositories in scope that don’t match a template remain unmanaged instead of failing the run. In explicit collection mode, undeclared members of supported collections are preserved. The root file defines the scope and broad reconciliation policy; it doesn’t specify the settings for every repository.
Those belong in templates. For example, a repository template can select service repositories and manage a setting alongside a workflow file:
version: 1
kind: repository
match:
include:
names:
- "service-*"
repository:
settings:
has_issues: true
actions:
secrets:
- SHARED_WORKFLOW_TOKEN
files:
.github/workflows/shared.yml:
ensure: exact
source: files/shared.yml
This is a deliberately small example, not the complete configuration for my team’s original workflow. The important parts are the match, which decides where the template applies, and the repository section, which declares what Octosmith is allowed to manage. The workflow source is stored alongside the Octosmith configuration. The Actions secret is sourced from the execution environment rather than stored in YAML.
The main commands should look familiar:
octosmith plan --path ./github-config
octosmith apply --path ./github-config
The plan/apply separation was there from the beginning: I wanted to inspect proposed changes before executing them. Persisted executable plans came later, taking another cue from Terraform.
I briefly considered an Aspire-like DSL in C#. But YAML is understood (and sometimes hated) nearly everywhere. The format is familiar to anyone who has worked with Kubernetes manifests, for better or worse. More importantly, I wanted the configuration to remain a description rather than become another program.
There are reusable fragments and includes for composing shared configuration. But I am deliberately reluctant to add dynamic templating or elaborate inheritance rules. If understanding a repository’s effective configuration requires tracing a miniature programming language, I would be recreating part of the problem I set out to solve.
Consistency without uniformity
Consider five .NET library repositories. They should share some settings, perhaps team permissions and a common workflow. But one of them may have additional configuration that the others don’t need.
I don’t want a common template to erase those differences just because they aren’t mentioned in it.
Octosmith’s default approach is to enforce the configuration it declares while leaving unrelated configuration alone. A setting named in the template is authoritative. A setting absent from it generally isn’t Octosmith’s business.
There are nuances. Collections can be managed in explicit mode, preserving undeclared members, or strict mode, where supported collections can remove undeclared members. Managed files have another safeguard: deletion must be requested explicitly, rather than inferred from a file’s absence from the template.
And a legitimate exception to a managed setting still needs to be modeled. Today, that means using a separate template, optionally composing shared pieces through includes. Octosmith isn’t trying to make exceptions disappear; it tries to keep the shared baseline clear without treating every extra setting as drift.
Why not Terraform?
We already use Terraform extensively. So why write another tool instead of using the GitHub provider?
Terraform can manage GitHub repositories. It can import existing resources, use modules to share configuration, and express differences through inputs and lifecycle rules. I don’t want to suggest otherwise.
The difference is less about what Terraform can do and more about the operating model I wanted.
Manage the declared parts, not an entire repository lifecycle
In Terraform, you normally declare resource instances, track them through Terraform state, and reconcile their configuration. The resource model supports selective behavior and exceptions, but you need to model those choices.
With Octosmith, I start from repositories that already exist and have their own lifecycles. I declare the particular settings, secrets, workflows, and other supported configuration that I want to keep aligned.
This isn’t absolute ownership of a repository. It is authority over the parts that a particular configuration describes.
Discover which repositories need a template
An explicit list of 25 names is manageable today. It is also another list to remember to update tomorrow.
Octosmith can select repositories by name or glob, team, visibility, and custom properties. Literal name selection remains available, but patterns make the configuration useful as the organization changes.
If a new service repository matches the relevant template, it can be picked up on the next run without adding another resource declaration for that particular repository.
Terraform also has discovery mechanisms. The distinction here is that discovery and classification are central to Octosmith’s normal workflow, rather than something to assemble around a collection of managed resource instances.
Read GitHub instead of keeping an ownership state file
Octosmith reads GitHub’s current configuration when it plans changes. It doesn’t maintain a persistent Terraform-style state file mapping each managed repository to an owned resource instance.
That’s useful when multiple teams have separate responsibilities, especially if their configuration lives in different repositories. Persisted plans still exist, but they are execution artifacts with preconditions, not a permanent registry of resource ownership.
No persistent state file doesn’t mean there is no state or no risk of concurrent changes. It means Octosmith makes a different trade-off about how it finds the state it needs.
Allow independent configurations
This distinction is especially important in a larger organization.
Imagine a platform team managing rulesets and organization-wide repository conventions, while a product team manages its workflows and application-specific settings. The platform team’s configuration repository may be internal and invisible to the product team. Their GitHub permissions may differ too.
I don’t want Octosmith to require a single central configuration, or one actor with exclusive responsibility for each repository. Independent configurations can manage different parts of the same repository, provided each configuration is internally coherent.
That doesn’t resolve conflicts across configurations. If two actors declare contradictory values for the same setting, they can keep undoing each other’s work. For now, that requires organizational coordination. A tool that cannot see another team’s configuration can’t reliably arbitrate its intent.
Treat repository files as part of the same problem
The original task wasn’t just about API settings. It involved placing a workflow file in repositories and providing the secret it consumed.
Octosmith can manage selected files as part of the desired configuration. By default, file changes are delivered through pull requests, so they go through the repository’s familiar review process. Secrets and settings use GitHub APIs instead.
Terraform can also work with repository files, so this isn’t a claim of exclusive capability. I wanted the workflow file and the surrounding repository configuration expressed through one reconciliation model, with a reviewable delivery path for file changes.
These differences aren’t a reason for everyone to stop using Terraform for GitHub. They are the reasons I chose a different shape for this particular problem.
The trade-offs are real
Octosmith can limit what it intends to change. That doesn’t mean GitHub lets me grant it permissions at exactly the same granularity.
For the first organizational rollout, I created a GitHub App and a workflow that obtains an installation token through actions/create-github-app-token. The permissions required to inspect and modify repository configuration are substantial. In some cases, including aspects of repository team permissions, even reading the relevant state needs elevated permissions.
That’s currently giving me pause. The PR to use Octosmith with our organization’s repositories is prepared, but I haven’t merged it yet. So I can describe the tool I’ve built, not claim that the original 25-repository rollout is already a production success.
Reconciliation also runs into the boundaries of GitHub’s API. Secret values cannot be read back, so when a configuration declares a secret and its source value is available, Octosmith submits the available declared value without comparing it to the remote value. The --skip-missing-values option helps with local runs where I don’t have all the organization’s secret values: missing values can be excluded from reconciliation instead of being replaced by placeholders.
Other capabilities are only partially exposed. For example, repository Copilot configuration has read APIs without corresponding write operations for all the changes I might want to make. The desired-state model can’t overcome API operations GitHub doesn’t offer.
And keeping templates understandable takes restraint. Includes support reuse, but I’m wary of nested template hierarchies and dynamic branches. Some explicit repetition may be cheaper than complicated precedence rules that nobody can confidently explain.
Where the journey stands
Octosmith started with a workflow, a secret, and a script I no longer wanted to keep expanding. It has grown into an open-source resource orchestration tool with repository discovery, templates, planning, reconciliation, and more.
Repositories are the first supported resource type. I’m also thinking about project and organization configuration, but those raise different modeling questions. Organization settings, for example, might end up using a singleton template that behaves more like a description than a reusable template.
The immediate next step is still the one that started this journey: getting comfortable with the permission model and deploying Octosmith against the repositories we manage.
If you maintain a collection of GitHub repositories, I’d be interested to hear where you draw the line between shared conventions and repository-specific choices. And if this approach sounds useful, Octosmith is on GitHub. Feedback and experiments are welcome.
Try Octosmith
If you’re dealing with similar repository management problems and want to give Octosmith a try, the easiest way to start is by scaffolding a configuration repository.
If you don’t have Deno installed, follow the official installation instructions. Octosmith uses Deno to run its CLI and scaffold configuration repositories.
Once Deno is available, run:
deno create jsr:@octosmith/octosmith@0 github-config -- --organization acme
This creates a starting configuration with an octosmith.yml file, a repository template, and GitHub Actions workflows for validation and reconciliation.
From there, you can adjust the repository scope, define the settings you want to manage, and run octosmith plan --path ./github-config to see what would change before applying anything.
I’d recommend starting with a test repository or a small, well-defined set of repositories rather than immediately granting access to an entire organization.
For more details, check the getting-started guide. The published packages are also available on JSR.
Recap
The lesson wasn’t that imperative scripts are bad or that every configuration problem needs YAML. My script had accumulated two responsibilities: deciding what applied to each repository and making the GitHub API calls to enforce it.
Octosmith separates those concerns. The aim is to make common configuration predictable without making every repository identical. I’m still working through what that means in practice, particularly around permissions and independent ownership, and that’s part of the journey I wanted to share.
This article was produced using an AI-assisted editorial process and was reviewed with AI assistance. Read about my editorial process.
Support this blog
If you liked this article, consider supporting this blog by buying me a pizza!