150+ repositories, one small team: what it takes to make the count stop mattering

We run more than 150 repositories with one small team. The number was never the hard part. The cost of one more repository was, and this is the machinery that brought it down.

André LorethAndré Loreth··7 min read
Share
A dense grid of about 150 small grey blocks in even rows, with a single orange block among them.

Every few weeks one of us opens a repository that nobody has touched in a year. The install command is the one we typed somewhere else this morning. Build, check and test have the same names. The pipeline runs, and the deployment goes out the way every other one does. Nobody has to go looking for the person who set it up.

We have 150+ repositories and one small team. The count sounds like the problem and never was. What matters is what one more repository costs: to create, to keep current, and to leave alone once the project is over.

Standards? Standards! was about the decisions we refuse to make twice. This post is about the machinery that applies them to a whole fleet, and about the first version of it, which had the right ideas and too many moving parts.

Where 150 comes from

It does not come from slicing things thin. Most of these repositories are monorepos. The number comes from the kind of work we do, which is a lot of quick-paced projects, each with its own customer and its own lifetime. A project starts, ships, gets maintained for a while, and at some point it is finished. The repository stays.

The right ideas, too much machinery

The first version dates from 2023, and most of what this post describes was already in it. A repository declared what it needed in a small file. A bot collected those files into an inventory. A program turned the inventory into registries, storage and access, and wrote the results back to each repository for the pipeline to use. It was keyless from the first day.

We would pick the same design again. The trouble was how much we built to get there. Every step was software of ours, and between a repository asking for something and receiving it sat four stages, one of which was a person pressing merge.

That made the stack fragile in ways that had nothing to do with the ideas:

  • One repository could block the fleet. A single repository the program could not read failed the run for all of them.
  • It had a ceiling. Every repository got its own service identity in the cloud, and a cloud project holds a hundred of those. The design stopped at a hundred repositories, however well the rest worked.
  • Only one of us could repair it. Anybody could declare what their repository needed, but every change to the machinery came from the same person.

The repository declares, the platform reacts

The replacement keeps the idea and removes most of what stood around it.

A repository still states what it needs, but now on the repository itself, as a small piece of metadata next to its name and description. There is no file to write and nothing to commit. An automated inventory reads every repository on a schedule and merges itself unless something looks wrong. When it changes, the platform creates what is missing, grants the access that goes with it, and hands the results back as plain values the pipeline picks up by name.

Four cards in a row connected by arrows: Repository, declares what it needs. Inventory, notices the change. Platform, reconciles. Resources, registry, storage, access. A return arrow runs underneath from the last card back to the first, labelled values the pipeline reads. Only the Repository card is orange.
The repository is the only place anything is declared. Everything to the right of it is derived.

What used to be a program is now a short declarative description that any of us can read in one sitting. It is also new: the two platforms still run side by side while repositories move across.

Nothing holds a key

No repository of ours stores a credential. There are no cloud keys in the pipeline, no personal access tokens, no deploy tokens and no registry passwords. Everything authenticates with OIDC: a pipeline run carries a short-lived signed token that says which repository it is running for, and whatever it needs to reach exchanges that token for access scoped to that repository.

The first platform worked this way too, through a service identity per repository. That identity is what capped us at a hundred. The new platform grants access to the repository itself, so there is nothing to create per repository and no ceiling.

The rest follows the same pattern. GitHub access goes through a token broker, deployments authenticate to ArgoCD the same way, packages are published without a token, and our own tools sign in with one OIDC client.

gh-token-broker

🔑 GitHub Token Broker, a OAuth 2.0 STS that mints least-privilege GitHub App tokens for Actions workflows, gated by custom CEL policies.

Go30 forks

At our size this is as much about bookkeeping as about security. A secret per repository would be 150 things to rotate. Without them, a new repository needs nothing handed to it, and an archived one holds nothing worth taking.

Provisioning is automatic

The platform acts on its own. A repository that starts asking for a registry has one by the next run, with the access granted and the address handed back, and nobody was asked. The same holds at the other end of a project: the repository is archived and drops out of everything that reacts. There is no ticket at the start of a project’s life and none at the end.

One pipeline, referenced everywhere

The other half of the machinery is older. No repository of ours contains build steps. The workflow file is a reference to a shared pipeline, pinned to a version, and the values the platform hands back are the inputs that pipeline expects.

actions

💥 Reusable GitHub Actions and workflows for Node.js/polyglot monorepos, GitOps deployments, releases, and tooling setup (GCP, K8s, Node).

TypeScript00 forksApache-2.0

When we moved the first ten repositories to the new platform, the change in each one was the same two lines, and all ten went over on the same day.

A row of identical repository cards labelled repo 01, repo 02 and so on up to repo 150. Each shows the same two-line diff: a grey removed line, pipeline at v1, and an orange added line, pipeline at v2.
What a fleet-wide change should look like: the same small diff, repeated.

We use that as a test now. If a fleet-wide change cannot be written as the same small diff in every repository, something that should be shared is still living in the repositories.

What this does for agents

A person who moves between our projects carries the conventions in their head. A coding agent starts every session knowing nothing about the last repository it worked in. A uniform fleet is worth more to an agent than to us: the task names, the layout and the pipeline are the ones it has already seen, so nothing has to be rediscovered, and rediscovery is where agents burn time and make things up.

We treat what we tell agents as one more standard. The conventions for each stack are written once as shared skills, and the instructions an agent reads at the start of a session come from one source instead of being written by hand per repository.

The platform helps as well. Setting up a project contains no step that waits for a colleague, whether a person or an agent is doing it. And because access belongs to the repository, an agent’s pipeline run can reach what that repository can reach and nothing else.

There is a less comfortable side. An agent copies the patterns it finds, so in a repository that has drifted it reproduces the drift, faster than a person would and with more confidence.

What it buys

The repository from a year ago opens and behaves like the one from this morning. Starting a project does not involve a request to anybody, and a finished one is archived and stops costing attention.

The number will keep growing, because projects keep starting and the finished ones stay. We stopped trying to keep the count down once the next repository cost about as little as the last one.

This article was written with the support of artificial intelligence. Given what we do here, it would have been strange not to.

Share