Choosing between polyrepo, monorepo, and hybrid layouts; CODEOWNERS-driven ownership; repository templates; rulesets; naming conventions; and the .github directory — the decisions that shape how your whole team works
This decision shapes your CI architecture, dependency management, code-sharing patterns, team autonomy, and on-call blast radius for years. There's no universally correct answer — but there is a decision framework that cuts through the religion around it.
The core trade-off
Dimension
Polyrepo
Monorepo
Deployment independence
✓ Each service has its own pipeline
Requires path-filtered triggers in CI to achieve the same
Cross-service refactors
Painful — multiple PRs, coordination overhead
✓ One atomic PR, one review, one merge
Dependency management
Each repo pins its own versions; drift is common
✓ One version of every dependency; no version skew
Team autonomy
✓ Teams own their repo end-to-end
Shared repo requires governance to avoid stepping on each other
CI cost
✓ Only the changed repo's CI runs
Requires affected-service detection to avoid running all CI on every commit
Tooling complexity
Simple git, simple GitHub Actions
Needs Nx/Turborepo/Bazel/Pants or hand-rolled path filters for scale
Onboarding
New hire needs access to N repos
✓ One clone, one PR flow, one CI system
Secret management
✓ Secrets scoped per repo by default
Requires environment-scoped secrets + CODEOWNERS to limit access
Decision framework
Answer these four questions:
Do services share code that changes together? If yes, and that shared code is a non-trivial library that consumers need to update in lockstep, monorepo wins. If the shared code is stable and versioned independently (like an npm package), polyrepo is fine.
How many teams, and how independent are they? Two teams sharing a monorepo with clear CODEOWNERS is fine. Twenty teams with conflicting release cycles and different tech stacks is a recipe for monorepo pain.
What's your CI build time budget? Monorepo only works at scale if you can detect and run only the affected projects. If you're not willing to invest in that tooling, polyrepo keeps CI fast by default.
How often do cross-service changes happen? If two services always change together, they should be in one repo. If they almost never change together, separate repos reduce noise.
The hybrid (most pragmatic for growing teams)
Group services that genuinely evolve together into a monorepo per domain, keep truly independent services in their own repos. Example:
github.com/acme/platform/← monorepo: auth, billing, notifications (always deployed together)data-pipeline/← monorepo: ingestion, transforms, warehouse (data team owns)mobile-ios/← standalone: iOS app (separate release cadence)mobile-android/← standalone: Android appdocs/← standalone: public documentation site
LEAD INSIGHT
The monorepo vs polyrepo debate is often a proxy argument for a deeper question: who owns the deployment pipeline? Solve that governance question first, then the repo structure usually becomes obvious.
2.2
GitHub Monorepo at Scale: CODEOWNERS & Path Filters
📚 Free Weekly Tutorials
Java, Spring Boot, AWS, DevOps & AI — straight to your inbox.
Two GitHub features make monorepos tractable: CODEOWNERS for ownership and required reviews, and path filters in Actions for targeted CI.
CODEOWNERS
The CODEOWNERS file lives in the repo root, .github/, or docs/. It maps file paths to GitHub users or teams using gitignore-style patterns. When a PR changes a file, the matching owners are automatically added as required reviewers (if branch protection requires CODEOWNER approval).
# .github/CODEOWNERS# Default owner for everything not matched below
* @acme/platform-leads# Service-level ownership
/services/payments/ @acme/payments-team
/services/auth/ @alice @acme/security-team
/services/notifications/ @acme/notifications-team# Shared libraries — anyone can read, leads must approve changes
/shared/ @acme/platform-leads# Infrastructure as code — ops team required
/infra/ @acme/ops-team
*.tf @acme/ops-team
*.tfvars @acme/ops-team# CI config — platform team must review any workflow changes
.github/workflows/ @acme/platform-leads# Docs — anyone on the team is fine
/docs/ @acme/all-engineers
Pattern precedence: the last matching pattern wins. Put general patterns first, specific ones last. A file matched by * and then /services/auth/ is owned by @acme/security-team, not the default owner.
GOTCHA
CODEOWNERS only triggers required reviews if the branch protection rule "Require review from Code Owners" is enabled. Having a CODEOWNERS file without this setting means owners are suggested, not required. Verify your branch protection settings match your ownership intentions.
Path-filtered Actions for monorepo CI
Without path filters, every push to any file triggers all workflows — CI costs explode and developer feedback slows. Use paths and paths-ignore to scope each workflow:
A required status check with paths filtering will be skipped (not failed) when no matching files change. GitHub treats a skipped required check as passing — which is usually what you want. But if you're relying on a required check to block merges, verify this behaviour in your specific branch protection setup.
Affected-service detection with dynamic matrices
For complex monorepos, compute the affected services at runtime rather than maintaining one workflow per service:
A repository marked as a template lets anyone in your org create a new repo pre-populated with its files. This is the right way to standardise new service scaffolding — not manually copying from a reference repo.
What a template copies — and what it doesn't
What IS copied
What is NOT copied
All files and directories
Git history (new repo gets a fresh initial commit)
Branches (excluding the default branch name)
Issues, PRs, discussions, wiki pages
.github/ workflows, CODEOWNERS, PR templates
Secrets, variables, environments
Topics, description
GitHub Pages settings
Repository settings (if using the API)
Branch protection rules
The missing pieces — secrets, branch protection rules, team access — need to be wired up separately. Use a GitHub App + Actions workflow triggered on repository: created to automate this:
# .github/workflows/new-repo-setup.yml (lives in your .github org repo)on:repository_dispatch:types: [repo-created]
# Alternatively, trigger via GitHub App webhook on repository.created eventjobs:configure:runs-on: ubuntu-latest
steps:
- name: Apply branch protection
run: |
gh api repos/$REPO/branches/main/protection \
--method PUT \
--field required_status_checks='{"strict":true,"contexts":["ci/test"]}' \
--field enforce_admins=true \
--field required_pull_request_reviews='{"required_approving_review_count":1}'
env:GH_TOKEN:${{ secrets.SETUP_APP_TOKEN }}REPO:${{ github.event.client_payload.repo }}
What to put in an org template
.github/workflows/ci.yml← standard lint + test pipelinerelease.yml← release-please or semantic-releasecodeql.yml← default CodeQL scanningdependabot-automerge.yml← auto-merge patch updatesCODEOWNERS← placeholder, team fills inPULL_REQUEST_TEMPLATE.mdISSUE_TEMPLATE/bug_report.ymlfeature_request.ymlconfig.yml.gitignore← language-appropriate defaults.editorconfig← consistent formatting across editorsREADME.md← structure with TOC placeholderCONTRIBUTING.mdSECURITY.mddependabot.yml← enabled for the package ecosystem
2.4
Submodules vs Subtrees vs Dependency Managers
This comes up whenever a team wants to share code between repos. All three options work; all three have costs. The honest comparison:
Approach
How it works
Strengths
Real-world pain
Git submodules
A pointer (commit SHA) to another repo, stored in .gitmodules. The submodule content is not in the parent repo's history.
Independent versioning per consumer; each consumer can pin to a specific commit
Clones need --recurse-submodules
Updating requires two commits (submodule + parent)
Detached HEAD state inside submodule is a constant gotcha
CI setup is more complex
Git subtree
Merges another repo's history directly into a subdirectory of the parent repo. The content becomes a first-class part of the parent's history.
No special clone command needed; contributors unaware of subtrees work normally; can contribute back upstream with git subtree push
History can become confusing with squashed subtree merges
Pulling updates is a manual step
Not a good fit if multiple repos consume the same shared code
Dependency manager (npm, Maven, pip, Go modules)
Shared code is published as a versioned package to a registry (GitHub Packages, npm, Maven Central). Consumers declare a version dependency in their manifest.
Standard practice; tooling (Dependabot, Renovate) handles updates; clear version contracts; works across languages
Cross-cutting changes require: PR in shared lib → publish new version → PR in consumer → wait for CI
Slow feedback loop during co-development
RECOMMENDATION
Default to dependency managers for stable shared code. Use submodules only when you need exact-commit pinning of an external repo you don't control (e.g., vendored firmware). Avoid subtrees for anything more than a one-time import. If co-development friction with dependency managers is too high, that's a signal the two things should be in a monorepo together.
Submodule quick reference
# Add a submodule$ git submodule add https://github.com/org/shared-lib.git libs/shared
# Clone a repo that has submodules$ git clone --recurse-submodules https://github.com/org/consumer.git
# Update an existing clone's submodules$ git submodule update --init --recursive
# Update a submodule to the latest commit on its tracking branch$ git submodule update --remote libs/shared
# Remove a submodule cleanly$ git submodule deinit libs/shared
$ git rm libs/shared
$ rm -rf .git/modules/libs/shared
2.5
Repository Rulesets vs Branch Protection Rules
GitHub shipped Repository Rulesets in 2023 as the next-generation replacement for Branch Protection Rules. Both still exist; understanding which to use and how to migrate matters for tech leads managing org-wide policy.
Key differences
Capability
Branch Protection Rules (legacy)
Repository Rulesets (new)
Scope
One rule set per branch pattern, per repo
Org-wide or repo-level; one ruleset can cover multiple repos
Target
Branches only
Branches and tags
Enforcement modes
Active only
Active, Evaluate (audit log, no blocking), or Disabled
Bypass actors
Admins can bypass (coarse)
Explicitly configurable: specific users, teams, apps, or deployment environments
Layering
One protection per pattern — last write wins
Multiple rulesets stack; the most restrictive rule for any property wins
Required workflows
Not available
Can require a specific reusable workflow to pass (Enterprise)
API / Kotlin DSL
Full API support
Full API + GitHub App webhook events for rule evaluation
Creating a ruleset via the UI
Settings → Rules → Rulesets → New ruleset → New branch ruleset
Name: protect-main
Enforcement: Active
Bypass list: add your release automation GitHub App (so it can push tags without a PR)
Target branches: include refs/heads/main and refs/heads/release/**
Rules: restrict deletions, block force pushes, require PR with 1 approving review, require status checks to pass
Don't delete existing branch protection rules while migrating to rulesets. Create the ruleset in Evaluate mode first — it runs without blocking anything but logs every violation in the audit log. After a week of evaluating, flip to Active and then delete the old branch protection rule. This gives you a safe rollout with no disruption to developers.
2.6
Naming Conventions, Visibility & Archiving at Org Scale
At 50+ repos, inconsistent naming makes discoverability, automation, and access management painful. Establish conventions early; they compound in value.
Repository naming
Pattern
Example
Use for
service-name
payment-processor
Backend services / microservices
app-platform-name
app-ios, app-android
Client applications
lib-name
lib-auth-utils
Internal shared libraries
infra-scope
infra-aws-prod
Infrastructure/Terraform repos
data-pipeline-name
data-clickstream
Data engineering pipelines
docs-scope
docs-api, docs-runbooks
Documentation repos
.github
(exactly this name)
Org-level defaults (see 2.8)
Visibility policy
Internal (GitHub Enterprise): visible to all org members, not the public. Default for most product code — balances discoverability with security.
Private: explicitly scoped access. Use for repos containing credentials, sensitive customer data processing, or unreleased product features.
Public: requires a deliberate decision + security review. Enables Dependabot on public repos for free, but exposes your code, commit history, and any accidentally committed secrets permanently.
IMPORTANT
Making a private repo public is irreversible in practice — GitHub's API can flip the flag back to private, but any secrets, tokens, or sensitive data committed while public may already be scraped by credential scanners. Treat "make public" as a one-way door and scan with git-secrets or trufflehog before flipping.
Archiving strategy
Archived repos are read-only and hidden from the default org repo list. Archive when:
A service is fully decommissioned but the code is valuable as reference
An experiment or spike was completed and won't be resumed
A library has been superseded and all consumers have migrated
# Archive a repo via CLI$ gh repo archive org/old-service --yes
# Bulk archive all repos matching a pattern (careful)$ gh repo list org --json name,updatedAt \
--jq '.[] | select(.updatedAt < "2024-01-01") | .name' \
| xargs -I{} gh repo archive org/{} --yes
Before archiving: ensure runbook links in incident management tools, internal wikis, and Slack bookmarks are updated to point to the archive or the successor.
2.7
README, CONTRIBUTING, SECURITY.md & Friends
GitHub surfaces these files at specific URL paths and in the UI. Each one has an automation hook beyond its human-readable purpose.
File
GitHub behaviour
Automation hook
README.md
Rendered on repo homepage and on the org profile if in .github/profile/
Use shields.io badges for build status, coverage, version. Can embed workflow status: 
CONTRIBUTING.md
Linked automatically when a user opens a new issue or PR
Include required local setup steps so contributors don't open "how do I run this?" issues
SECURITY.md
Shown in the Security tab; enables "Report a vulnerability" button for private disclosure
Enables GitHub's private vulnerability reporting. Without it, researchers open public issues to report security bugs.
CODE_OF_CONDUCT.md
Linked in community health score; required for GitHub's Open Source program
Sets expectations for issue/PR tone; GitHub moderators reference it for content removal requests
SUPPORT.md
Linked when users open issues; redirects support questions away from GitHub Issues
Reduces noise in the issue tracker by directing users to Slack, Discourse, or a support form instead
FUNDING.yml
Adds a "Sponsor" button to the repo
Supports GitHub Sponsors, Patreon, Open Collective, custom URLs
Minimal SECURITY.md template
# SECURITY.md
## Reporting a Vulnerability
**Do not open a public GitHub issue for security vulnerabilities.**
Use GitHub's private vulnerability reporting:
Settings → Security → Report a vulnerability
Or email: security@acme.com (PGP key: https://acme.com/pgp)
We aim to respond within 48 hours and release a patch within 14 days
for critical issues. We follow coordinated disclosure — we'll work
with you on timing before any public announcement.
TIP
Enable GitHub's private vulnerability reporting (Settings → Security → Private vulnerability reporting → Enable) so researchers can submit reports directly into a private draft security advisory without your email being involved at all.
2.8
Default Branch Naming, .github/ Layout & the Org .github Repo
Default branch naming
GitHub now defaults new repos to main. Set the org-level default under Organization Settings → Member privileges → Repository default branch. This affects new repos; existing repos with master need a manual rename:
# Rename default branch on an existing repo (GitHub CLI)$ gh repo rename-default-branch main --repo org/service-name
# Or via REST API (also updates open PRs targeting the old branch)$ gh api repos/org/service-name/branches/master/rename \
--method POST --field new_name=main
.github/ directory layout
The .github/ directory is the convention for everything GitHub-specific. A complete layout:
.github/workflows/ci.yml← CI pipelinerelease.yml← release automationcodeql.yml← security scanningdependabot-automerge.yml← auto-merge patch updatesISSUE_TEMPLATE/bug_report.yml← YAML form (preferred over .md)feature_request.ymlconfig.yml← disables blank issues, adds linksactions/← composite actions local to this reposetup-node/action.ymlCODEOWNERSPULL_REQUEST_TEMPLATE.mddependabot.ymllabeler.yml← used by actions/labelerrelease-please-config.json← release-please config
The org-level .github repository
Create a repository named exactly .github inside your GitHub organization. Files placed here become org-wide defaults — they apply to any repo in the org that doesn't have its own version of the file. This is the single most leveraged thing you can do for org-level standardisation.
github.com/acme/.github/← the special org default repoprofile/README.md← renders on github.com/acme org homepageworkflow-templates/ci.yml← suggested workflow shown in Actions tab of new reposci.properties.json← metadata for the suggestion (name, description, icon)CONTRIBUTING.md← default for all repos without their ownSECURITY.md← default for all repos without their ownCODE_OF_CONDUCT.md← default for all repos without their ownSUPPORT.md← default for all repos without their own
SCOPE NOTE
The .github org repo provides defaults for community health files (CONTRIBUTING, SECURITY, etc.) and workflow templates. It does not automatically apply CODEOWNERS or workflows to other repos — those must exist in each repo. For org-wide workflow enforcement, use Required Workflows under org-level rulesets (GitHub Enterprise).
Workflow templates (workflow-templates/)
Files in .github/workflow-templates/ appear as suggestions in the Actions → New workflow tab of every repo in your org. Pair each .yml with a .properties.json:
// workflow-templates/ci.properties.json
{
"name": "Acme Standard CI",
"description": "Lint, test and build — Acme standard pipeline",
"iconName": "octicon workflow",
"categories": ["CI"],
"filePatterns": ["package.json$"]
}
When a developer in your org opens the Actions tab and clicks "New workflow," your template appears prominently alongside GitHub's built-in ones. This is the lowest-friction way to nudge teams toward org standards without forcing it.
Up Next — Phase 3: Branching Strategies & Code Review Workflows
GitFlow vs trunk-based development, merge queues, CODEOWNERS-enforced required reviews, review SLAs, draft PRs, and auto-merge — the policies that control how code reaches production.