agentleFS
reddygopalkrishnareddygopalkrishna/developer-agent-skills/skills/monorepo-management/SKILL.md

monorepo-management

Skill8 starsChanged 10 days agoMITView on GitHub

Sets up and maintains monorepos - workspace tooling choice, package boundaries, affected-only builds and tests, remote caching, shared config, versioning and code ownership. Use when creating or restructuring a monorepo, adding a package or app to one, CI is slow because everything rebuilds, imports cross package boundaries, or choosing between Nx, Turborepo, Bazel, pnpm, Gradle, Go or Cargo workspaces. Not for splitting a monolith into services.

7 other projects use a version of this.

Adopted by

Most recently taken up by

The file

We split this skill into the rules it gives an agent

10 rules. 0 are common, 8 are distinctive, and 2 are very specific to this project.

  • 0

    Common

    Most of the 7 edited versions we read keep these.

  • 8

    Distinctive

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Not for: breaking a monolith into deployable services (see the large-scale-migrations skill).

    line 19
  2. Distinctive:

    If most answers are on the right, keep separate repos and share code through versioned packages.

    line 31
  3. Distinctive:

    Start with the lightest tool that gives you affected builds and caching for your stack.

    line 35
  4. Distinctive:

    One lockfile at the root: Commit it. Never commit per-package lockfiles in a JS workspace.

    line 60
  5. Distinctive:

    Prefer "no versions" for internal packages: it is the main benefit of a monorepo.

    line 98
  6. Very specific:

    Add a CODEOWNERS file (GitHub: root, docs/ or .github/.

    line 102
  7. Distinctive:

    Every package has an owner and a short README: purpose, public API, how to test.

    line 103
  8. Distinctive:

    Keep root-level files (CI, lockfile, shared config) owned by the platform/tooling owners.

    line 104
  9. Distinctive:

    Deep imports into other packages: - every internal refactor becomes a cross-team breaking change.

    line 109
  10. Very specific:

    Every package has an owner in CODEOWNERS and a README.

    line 122

“Common” is measured: kept by at least 6 in 10 of the 7 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Monorepo Management

A monorepo only works if two things stay true: package boundaries are enforced by tooling, and CI does work proportional to the change, not to the repo. Without both, a monorepo becomes a slow, tangled monolith with extra config.

When to use

  • Creating a monorepo, or moving several repos into one.
  • Adding a new app or package to an existing workspace.
  • CI time grows with repo size; every PR rebuilds and retests everything.
  • Packages import each other's internals; changes in one break others unexpectedly.
  • Choosing or replacing workspace tooling.

Not for: breaking a monolith into deployable services (see the large-scale-migrations skill). A monorepo is about where code lives, not how it is deployed.

Does a monorepo fit?

SignalMonorepo helpsMonorepo hurts
Shared codeMany packages share libraries, types, configProjects share little
Change shapeChanges often span several packages (API + client)Changes stay inside one project
TeamsTeams accept shared tooling and conventionsTeams need different toolchains, release cadences, access controls
Tooling budgetSomeone owns build/CI toolingNobody has time to maintain the build system
Access controlCode can be visible to all contributorsParts must be hidden from some contributors

If most answers are on the right, keep separate repos and share code through versioned packages.

Choosing tooling

Start with the lightest tool that gives you affected builds and caching for your stack. Add a heavier one only when you hit its limits.

ToolBest forAffected / cachingCost
npm / pnpm / yarn workspacesJS/TS, linking local packagesNone by themselves (pnpm --filter can select changed packages)Lowest
TurborepoJS/TS task orchestration on top of workspacesContent-hash task cache, local and remote; filter by git rangeLow
NxJS/TS (plus plugins for other stacks), larger orgsProject graph, nx affected, local and remote cache, boundary lint rulesMedium
BazelPolyglot, very large repos, hermetic buildsFine-grained, remote cache and remote executionHigh: needs dedicated owners
Gradle multi-projectJVMIncremental builds, build cache (local/remote)Medium
Go workspaces (go.work)Several Go modules developed togetherGo build cache; no affected detectionLow
Cargo workspacesRust cratesShared target/ and Cargo.lock; incremental compilationLow
.NET solution (*.sln) + Directory.Build.props.NETIncremental builds; shared props and central package managementLow

Defaults: JS/TS with fewer than ~50 packages: pnpm workspaces + Turborepo or Nx. JVM: Gradle. Polyglot at large scale with a platform team: Bazel.

Workflow: structure the repo

  1. Pick a layout and stick to it.
    apps/        # deployable things: web, api, workers
    packages/    # libraries consumed by apps (or libs/)
    tooling/     # shared config: eslint, tsconfig, build scripts
  2. Declare the workspace in the tool's root file: pnpm-workspace.yaml (packages: globs), "workspaces" in root package.json (npm/yarn), settings.gradle(.kts) include(...), go.work (go work init, go work use ./svc), [workspace] members in root Cargo.toml.
  3. Use workspace references for internal deps, not published versions: "@acme/ui": "workspace:*" (pnpm, Yarn 2+; npm and Yarn 1 use a plain version range), implementation(project(":lib")) (Gradle), path = "../lib" or workspace = true (Cargo).
  4. One lockfile at the root. Commit it. Never commit per-package lockfiles in a JS workspace.
  5. Centralize versions of third-party deps where the tool supports it: Cargo [workspace.dependencies], Gradle version catalogs (gradle/libs.versions.toml), .NET Central Package Management (Directory.Packages.props), pnpm catalogs (check that the installed pnpm version supports them). One version of each dependency across the repo prevents "works in package A, breaks in B".
  6. Share config through packages: tooling/tsconfig with tsconfig.base.json, an ESLint config package, a shared Gradle convention plugin. Packages extend; they do not copy.

Boundaries

Rules every repo needs, enforced by a tool that fails CI:

  • Import packages only through their public entry point. No @acme/ui/src/internal/button. Set "exports" in each package.json so deep imports fail at resolution.
  • Dependency direction is one way. Apps depend on packages; packages never depend on apps; low-level packages never depend on feature packages.
  • No cycles between packages.
StackEnforcement
JS/TSpackage.json exports; Nx @nx/enforce-module-boundaries with project tags; eslint-plugin-boundaries; dependency-cruiser; ESLint no-restricted-imports
JVMGradle project dependencies (no dependency means no access); ArchUnit tests for package rules
Gointernal/ directories are enforced by the compiler
Bazelvisibility attributes on targets
.NETProject references; internal + InternalsVisibleTo only for tests

CI: work proportional to the change

  1. Compute affected projects from the diff against the merge base: nx affected -t lint test build, turbo run test --filter=...[origin/main], pnpm --filter "...[origin/main]" test. Newer Turborepo versions also offer --affected; check turbo run --help.
  2. Fetch enough history for the diff: with GitHub Actions actions/checkout, set fetch-depth: 0 (or fetch the base branch) or affected detection silently falls back to "everything" or "nothing".
  3. Include dependents, not just changed packages. A change to packages/ui must test every app that uses it. Turborepo/pnpm filter syntax ...pkg selects dependents; nx affected does this by default.
  4. Cache task outputs keyed by inputs (sources, lockfile, env vars, tool versions). Declare outputs (dist/**, build/) and inputs precisely; a missing input gives wrong cache hits, which is worse than no cache.
  5. Turn on remote caching so CI and developers share results (Nx and Turborepo offer hosted and self-hosted options; Gradle and Bazel support remote cache servers). Treat cache write access as sensitive: anyone who can write can poison builds. Let only CI on trusted branches write.
  6. Run a full build on main periodically (nightly or on merge) to catch anything affected detection missed.
  7. Shard large test suites across CI machines once affected-only is not enough.

Versioning and releasing

ModelWhenHow
Single version (everything releases together)Apps deployed together; internal-only packagesOne version in root; tag the repo
Independent versionsPackages published to a registry for outside consumersChangesets (npx changeset, then changeset version and changeset publish), Nx release, or similar
No versions (always head)Internal packages consumed only inside the repoworkspace:*; apps deploy from a commit

Prefer "no versions" for internal packages: it is the main benefit of a monorepo. Publishing to a registry is outward-facing: confirm with the user before running any publish command.

Ownership

  • Add a CODEOWNERS file (GitHub: root, docs/ or .github/; GitLab: root, docs/ or .gitlab/) mapping paths to teams, and require code-owner review on protected branches. On Azure DevOps, use branch policies with path-filtered required reviewers.
  • Every package has an owner and a short README: purpose, public API, how to test.
  • Keep root-level files (CI, lockfile, shared config) owned by the platform/tooling owners.

Anti-patterns

  • Monorepo without affected builds - CI time grows forever and developers start skipping it.
  • Deep imports into other packages - every internal refactor becomes a cross-team breaking change.
  • Copy-pasted config - twenty slightly different tsconfig.json files. Extend a shared base.
  • Multiple versions of the same dependency - subtle runtime bugs (two React copies, two protobuf runtimes). Centralize versions.
  • Undeclared task inputs - caches return stale outputs; builds "pass" with old code.
  • Shallow clones in CI - affected detection compares against nothing.
  • Bazel without owners - adopting a heavyweight build system nobody can maintain.

Done when

  • The workspace builds and tests from a clean clone with the documented root commands.
  • Boundary rules run in CI and fail on a deep import or wrong-direction dependency.
  • A PR touching one leaf package runs only that package and its dependents; a change to shared config runs everything.
  • Task caching is on, with inputs and outputs declared; cache writes are limited to trusted CI.
  • Every package has an owner in CODEOWNERS and a README.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.