Anthony Wentzel

One monorepo template, four products

In 2021 we started four products from one monorepo template: path-filtered GitHub Actions, PR title linting, Renovate bot and Codecov from day one.

From 2021 · 2 min read · Published September 7, 2026

2021 was a busy year. Between February and July we started four separate products: two consumer mobile apps, a live game for Twitch audiences, and a data-gathering platform. Different customers, different domains, one small team, and one monorepo template.

They all started from the same skeleton. Open any of the four repos and you'll find the same shape:

.github/workflows/
  build-android.yml
  build-ios.yml
  build-web.yml
  deploy-api-dev.yml
  deploy-api-prod.yml
  pr-labeler.yml
  pr-lint.yml
  release-management.yml
projects/
  api/        # NestJS on AWS Lambda via Serverless
  ui/         # React Native app, also built for web
renovate.json
codecov.yml

The first ones were Rush monorepos; later ones dropped Rush and kept the layout. What mattered was the layout, not the tool.

Every workflow is path-filtered

The API deploys only when API code changes. The mobile builds only run when UI code changes. This was the single biggest cost and time saver:

.github/workflows/deploy-api-prod.yml
name: Deploy API (prod)
defaults:
  run:
    working-directory: ./projects/api
on:
  push:
    branches:
      - main
    paths:
      - '**/api/**'

With one repo per product but two very different deploy targets inside it, path filters let a copy change in the app skip a Lambda deploy entirely, and an API fix skip a twenty-minute iOS build.

PR titles are part of the build

The least glamorous file turned out to be the most useful:

.github/workflows/pr-lint.yml
- uses: seferov/pr-lint-action@master
  with:
    title-regex: '^(feat|fix|chore|hotfix|revert)\([a-z-\d]*\):\s'

Every PR title has to look like fix(ui): twitch connect or feat(api): crons. It feels pedantic for about a week. After that you get three things for free:

  1. Scannable history. You can see at a glance which half of the monorepo a change touched.
  2. Automatic labels. A labeler action turns feat and fix into GitHub labels.
  3. Release notes that write themselves. Release Drafter groups merged PRs by label into a draft release on every push to main.

If you look at my commit history from that year, nearly every message follows this format. That's the linter's influence, and I've kept the habit.

Renovate bot and Codecov from day one

Both were in the template, so both were on from the first commit. Dependency updates arrived as small, reviewable PRs instead of a painful quarterly upgrade, and coverage was visible before anyone had a chance to decide it didn't matter.

What the template didn't solve

Secrets. Each repo still needed its own AWS profile, database URI, and signing setup, wired by hand into CI. The template gave us the shape of a deploy pipeline, not the credentials, and that setup step was where new projects lost the most time. Today I'd reach for a platform with first-class environment management rather than hand-assembling .env files in a workflow.


Related: React Native CI/CD that only builds the platform that changed · Yeoman generators: code conventions you can run