Kai leads a five-person team maintaining a TypeScript monorepo with three published npm packages and two internal apps. Their current setup is a mess: ESLint takes 45 seconds to lint, Prettier conflicts with ESLint rules, nobody remembers to bump versions, and dependencies are months out of date because manual updates are tedious. Kai wants a pipeline where code quality is enforced automatically, versions are managed through PR descriptions, and dependencies stay current without human effort.
Step 1 — Replace ESLint + Prettier with Biome
ESLint and Prettier together require 15+ packages (eslint, prettier, eslint-config-prettier, eslint-plugin-import, @typescript-eslint/parser, @typescript-eslint/eslint-plugin...) and two config files that frequently conflict. Biome replaces all of it with a single binary and a single config file.
// biome.json — Root configuration for the entire monorepo.
// Biome handles formatting AND linting in one pass.
// The "recommended" preset enables 200+ rules with zero false positives.
{
"$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
"organizeImports": {
"enabled": true
},
"formatter": {
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 100
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"correctness": {
"noUnusedImports": "error",
"noUnusedVariables": "warn",
"useExhaustiveDependencies": "warn"
},
"performance": {
"noAccumulatingSpread": "error"
},
"suspicious": {
"noExplicitAny": "warn"
}
}
},
"files": {
"ignore": [
"node_modules",
"dist",
".next",
"coverage",
"*.generated.ts"
]
},
"overrides": [
{
"include": ["**/*.test.ts", "**/*.spec.ts"],
"linter": {
"rules": {
"suspicious": {
"noExplicitAny": "off"
}
}
}
}
]
}
The migration from ESLint takes one command: npx @biomejs/biome migrate eslint --write. Biome maps ESLint rules to its own equivalents and generates the config. The team deletes .eslintrc, .prettierrc, and 12 dev dependencies.
The speed difference is immediate. ESLint took 45 seconds on the monorepo. Biome checks the same codebase in 1.2 seconds — fast enough to run on every keystroke in the editor without lag.
# .github/workflows/ci.yml — CI check with Biome.
# "biome ci" runs formatting check + linting in a single command.
# Exits with error code if anything fails — no --fix in CI.
name: Code Quality
on:
pull_request:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: biomejs/setup-biome@v2
with:
version: latest
- name: Lint and format check
run: biome ci .
Step 2 — Manage Versions with Changesets
Before Changesets, the team bumped versions manually in package.json, forgot to update changelogs, and occasionally published packages with mismatched dependency versions. Changesets turns versioning into a natural part of the PR workflow.
// .changeset/config.json — Changesets configuration.
// "linked" ensures @repo/core and @repo/cli always share the same version.
// GitHub changelog plugin adds PR links and author attribution.
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": [
"@changesets/changelog-github",
{ "repo": "kai-team/monorepo" }
],
"commit": false,
"fixed": [],
"linked": [["@repo/core", "@repo/cli"]],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["@repo/web", "@repo/docs"]
}
When a developer finishes a feature, they run npx changeset before opening the PR. The CLI asks three questions: which packages changed, what semver bump (patch/minor/major), and a one-line description. It creates a Markdown file in .changeset/ that gets committed with the code.
<!-- .changeset/blue-foxes-dance.md -->
<!-- This file was generated by `npx changeset` -->
<!-- It describes what changed and how to bump the version -->
---
"@repo/core": minor
"@repo/ui": patch
---
Added streaming response support to the API client. Updated UI components to display streaming progress.
The GitHub Action creates a "Version Packages" PR that accumulates all pending changesets, bumps versions, and generates changelogs. When the team is ready to release, they merge this PR and a second workflow publishes to npm.
# .github/workflows/release.yml — Automated versioning and publishing.
# On push to main: if changesets exist, create a "Version Packages" PR.
# When that PR merges: publish changed packages to npm.
name: Release
on:
push:
branches: [main]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
release:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
id-token: write
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
registry-url: "https://registry.npmjs.org"
- run: pnpm install --frozen-lockfile
- name: Build packages
run: pnpm --filter "./packages/*" build
- name: Create Release PR or Publish
uses: changesets/action@v1
with:
publish: pnpm changeset publish
version: pnpm changeset version
title: "chore: version packages"
commit: "chore: version packages"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Step 3 — Automate Dependency Updates with Renovate
Renovate scans every dependency file in the repo — package.json, Dockerfile, GitHub Actions workflow versions — and creates PRs to update them. The key is configuring it so the team gets a manageable number of PRs instead of a flood.
// renovate.json5 — Renovate configuration.
// Groups related packages to reduce PR count.
// Automerges low-risk updates (types, patches) after CI passes.
// Schedules updates for Monday mornings so the team reviews them weekly.
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
"group:monorepos",
"group:recommended",
":automergeMinor",
"schedule:weekly"
],
"schedule": ["after 9am and before 12pm on Monday"],
"timezone": "America/New_York",
"labels": ["dependencies"],
"packageRules": [
{
// Type definitions are risk-free — automerge without review
"matchPackagePatterns": ["^@types/"],
"automerge": true,
"automergeType": "branch",
"groupName": "Type definitions"
},
{
// Dev tools: linters, test runners, build tools
"matchDepTypes": ["devDependencies"],
"matchUpdateTypes": ["minor", "patch"],
"automerge": true,
"groupName": "Dev dependencies (non-major)"
},
{
// React ecosystem: group all React-related updates
"matchPackagePatterns": ["^react", "^@types/react"],
"groupName": "React"
},
{
// Next.js: group framework + plugins
"matchPackagePatterns": ["^next", "^@next/"],
"groupName": "Next.js"
},
{
// Major updates: never automerge, add a label for visibility
"matchUpdateTypes": ["major"],
"automerge": false,
"labels": ["dependencies", "breaking"]
},
{
// Docker: update base images monthly
"matchDatasources": ["docker"],
"schedule": ["on the first day of the month"]
},
{
// GitHub Actions: group all action version updates
"matchManagers": ["github-actions"],
"groupName": "GitHub Actions",
"automerge": true
}
],
// Limit concurrent PRs to avoid overwhelming CI
"prConcurrentLimit": 5,
// Include changelogs and release notes in PRs
"fetchChangeLogs": "pr"
}
Step 4 — Wire It All Together
The final piece is a pre-commit hook that runs Biome locally and a CI check that ensures every PR with package changes includes a changeset.
# .github/workflows/changeset-check.yml — Enforce changesets on PRs.
# If the PR modifies files in packages/ but has no .changeset/ file,
# the check fails with a helpful message.
name: Changeset Check
on:
pull_request:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: Check for changeset
run: |
# Check if any package source files changed
CHANGED=$(git diff --name-only origin/main...HEAD -- 'packages/*/src/**')
if [ -n "$CHANGED" ]; then
# Source files changed — changeset required
CHANGESETS=$(git diff --name-only origin/main...HEAD -- '.changeset/*.md' | grep -v README)
if [ -z "$CHANGESETS" ]; then
echo "❌ This PR modifies package source files but has no changeset."
echo "Run 'npx changeset' to add one."
echo ""
echo "Changed files:"
echo "$CHANGED"
exit 1
fi
fi
echo "✅ Changeset check passed"
// package.json (root) — Scripts and pre-commit hook.
// "lefthook" is a fast Git hook manager (written in Go, no Node overhead).
{
"scripts": {
"check": "biome check .",
"fix": "biome check --fix .",
"changeset": "changeset",
"version": "changeset version",
"release": "pnpm --filter './packages/*' build && changeset publish"
},
"devDependencies": {
"@biomejs/biome": "^2.0.0",
"@changesets/changelog-github": "^0.5.0",
"@changesets/cli": "^2.27.0",
"lefthook": "^1.6.0"
}
}
# lefthook.yml — Git hooks configuration.
# Runs Biome on staged files before every commit.
# Fast enough (<2s) that developers don't skip it.
pre-commit:
commands:
biome:
glob: "*.{js,ts,jsx,tsx,json,css}"
run: npx biome check --fix --no-errors-on-unmatched --files-ignore-unknown=true {staged_files}
stage_fixed: true
Results
After two weeks with the new pipeline, Kai's team sees measurable improvements across every metric they cared about:
- Lint time: 45s → 1.2s — Biome replaces ESLint + Prettier. Developers now run checks on every save instead of waiting until CI.
- Config files: 4 → 1 —
.eslintrc,.eslintignore,.prettierrc,.prettierignorereplaced by a singlebiome.json. Twelve dev dependencies removed. - Version mistakes: 3/month → 0 — Changesets enforces that every PR with code changes includes a version bump description. The "Version Packages" PR shows exactly what will change before publishing.
- Dependency freshness: 73 outdated → 12 — Renovate created 28 PRs in the first week. 19 automerged (types, patches, dev tools). 9 major updates reviewed and merged by the team over two days.
- Time spent on dependency updates: 4 hours/month → 30 minutes/month — Renovate does the tedious work (checking for updates, reading changelogs, opening PRs). The team only reviews major version bumps.
- CHANGELOG quality — generated from PR descriptions, includes links to PRs and contributor attribution. Users can see exactly what changed in each release.