← all articles

Why we built a manifest instead of moving 30 repositories

Topics: git · architecture · tooling

When someone hears that we have roughly 30 repositories, the first question is usually: why not move them into a monorepo?

It is a fair question, but it begins with the assumption that multiple repositories are the problem. In our case, ownership, permissions, and release boundaries mattered, and each repository had its own history and build path. We did not need to erase those boundaries. We needed a way to see the whole system and rebuild the same workspace when required.

The answer was a central manifest inspired by Android repo. It does not combine the code into one repository. It describes the repositories that form the workspace, where each one belongs, and which revision is expected.

The missing piece was shared memory

Before the map existed, each engineer knew a different part of the system. One person remembered which service a frontend needed. Another kept a local setup script. Someone else had the only directory that had worked during the previous release.

That knowledge was not in Git. When a new engineer joined or someone rebuilt their machine, the same questions returned: which repositories are required, which branches work together, is this the current shared package, and why does the build work on one laptop but not another?

A monorepo does not solve every version of that problem automatically. It puts the files together, but it also changes permissions, release paths, and how teams work. We chose to solve the smaller problem first: make the workspace shape explicit.

What the manifest contains

A simplified manifest lists the repositories that belong in a workspace and the paths where they are checked out:

<manifest>
  <remote name="origin" fetch="..." />
  <default remote="origin" revision="main" />
 
  <project name="service-a" path="services/service-a" />
  <project name="console-a" path="apps/console-a" />
  <project name="shared-ui" path="packages/shared-ui" />
</manifest>

The manifest answers workspace questions: which repositories exist, where they are checked out, where they come from, and which revision represents a known state when a build needs to be repeated.

A manifest change is reviewed like code. Adding, moving, or removing a repository becomes a visible system decision in a pull request instead of an instruction buried in chat. The Git history also shows how the workspace changed over time.

One command instead of a setup checklist

A setup script reads the manifest, creates the directory structure, and syncs the required repositories. Running it twice must be safe. I do not want a command that works on an empty folder and damages the workspace when run again.

The script should not hide Git. If a repository is dirty, on another revision, or inaccessible to the current user, it should show that repository by name and path. “Sync failed” without the failing target saves nobody any time.

A useful status command shows at least:

  • current branch or revision;
  • whether local changes exist;
  • expected revision when one is pinned;
  • access or fetch failures.

That visibility removes a frustrating category of debugging: the system does not work, but every repository looks correct when inspected alone. The manifest makes the difference between current and expected state visible.

An engineer also does not need all 30 repositories for every task. The file can define groups around a product or workflow. The group still comes from the central map; it is not a second list that can drift on its own.

The repositories remain independent

We did not turn the manifest into a layer that controls everything. Each repository keeps its permissions, CI, releases, and code ownership. Those boundaries are why the polyrepo exists.

If every ordinary commit required a manifest change, we would have created a new bottleneck. The file changes when the workspace shape changes or when a known set of revisions must be reproducible. It does not change merely because one repository releases within its normal boundary.

That separation also keeps review clear. The frontend owner reviews frontend code. The service owner reviews its contract. Someone who understands the cross-repository effect reviews a change to the map.

Cross-repository changes are not atomic

This is a real cost, and I do not hide it. There is no single commit that changes an interface, service, shared package, and deployment repository at the same instant.

Contracts therefore need to support a gradual move. Add the new field before removing the old one. Let a service accept both versions during a transition. Publish a shared package before requiring every consumer to use it. If a change cannot run in stages, it needs a deliberate rollout plan.

The manifest can pin a set of revisions known to work together. It cannot replace compatible contracts. It helps us inspect and reproduce the state; it does not turn a polyrepo into a monorepo behind the scenes.

CI reads the same map

If the map is trusted locally, CI should not maintain another repository list. A verification job can create a clean workspace from the manifest, then run the smallest meaningful contract or end-to-end test for the selected group.

“Clean” matters. Reusing an old directory can hide a missing repository, wrong revision, or undocumented setup step. The real test of the manifest is whether it can build the expected shape from nothing.

Other scripts can read the same information: show dirty repositories, compare current state with expected state, or run a common command over a group. Each tool does not need repository names hard-coded into it.

Coding agents use the map too

Tools such as Codex and Claude can inspect one repository well. A task becomes harder when it crosses an interface, service, and shared package and the agent does not know where the boundary ends.

The manifest gives the agent a versioned map instead of making it guess from directory names. A custom skill or command can select the required group, sync it, read repository instructions, and run the right checks.

That does not mean granting unrestricted access to everything. The map helps narrow the scope. The agent sees the repositories involved in the task and stays outside the rest. When the work ends, status shows exactly which repositories changed.

For custom agent skills, MCP tools, and E2E workflows, one source describing the system has been more useful than writing a longer prompt for every task. Structured context is easier to verify.

What this approach does not solve

It does not unify tool versions. It does not make a cross-repository change atomic. It does not remove the need to maintain the setup scripts. An outdated manifest becomes a source of false confidence, which is worse than having no map.

We treat it as part of the architecture: it has an owner, changes are reviewed, and setup is tested in a clean environment. I keep the scripts around it as small as possible. When something fails, I want to see the Git command and repository, not search through a large layer of magic.

The approach will not fit every team. If all projects release together and share permissions and tooling, a monorepo may be simpler. The choice is not an engineering identity; it depends on the boundaries the organization needs to keep.

For us, the repositories remained independent and gained one place describing how they fit together. That is the manifest's job: turn the workspace from knowledge scattered between people into a definition that can be read, run, and checked.