Monorepo Setup Guide¶
This guide explains how to configure and use Releez in a monorepo with multiple independently-versioned projects.
Overview¶
Releez supports monorepos through project-based configuration. Each project in your monorepo can have:
- Independent versioning (e.g.,
core-1.2.3,ui-4.5.6) - Separate changelogs
- Isolated release branches and PRs
- Custom hooks and settings
- Selective builds in CI/CD
Quick Start¶
1. Basic Monorepo Configuration¶
Add a [[tool.releez.projects]] section for each independently-versioned
project:
# Root pyproject.toml or releez.toml
[tool.releez]
base-branch = "main"
git-remote = "origin"
create-pr = true
# Project 1: Core Library
[[tool.releez.projects]]
name = "core"
path = "packages/core"
changelog-path = "CHANGELOG.md" # Relative to packages/core/
tag-prefix = "core-"
# Project 2: UI Components
[[tool.releez.projects]]
name = "ui"
path = "packages/ui"
changelog-path = "CHANGELOG.md"
tag-prefix = "ui-"
2. Start a Release¶
Releez automatically detects which projects have unreleased changes:
# Auto-detect changed projects and release them
releez release start
# Release specific projects
releez release start --project core --project ui
# Release all projects regardless of changes
releez release start --all
3. What Happens¶
For each changed project, Releez will:
- Compute the next version (e.g.,
core-1.2.3) - Generate release notes from commits affecting that project
- Create a release branch:
release/core-1.2.3 - Update the project's changelog
- Create a PR with label
release:core
Configuration Reference¶
Project Configuration¶
Each [[tool.releez.projects]] entry supports:
| Field | Required | Description |
|---|---|---|
name |
Yes | Unique project identifier (used in CLI, labels) |
path |
Yes | Directory path relative to repo root |
changelog-path |
Yes | Changelog file path relative to project path |
tag-prefix |
Yes | Prefix for git tags (e.g., "core-" → core-1.2.3) |
alias-versions |
No | Override global alias-versions setting |
include-paths |
No | Additional paths to monitor for changes |
Example: Full Configuration¶
[tool.releez]
base-branch = "main"
git-remote = "origin"
create-pr = true
alias-versions = "none" # Global default
# Global hooks (apply to all projects)
[tool.releez.hooks]
post-changelog = [
["prettier", "--write", "{changelog}"],
]
# Core library project
[[tool.releez.projects]]
name = "core"
path = "packages/core"
changelog-path = "CHANGELOG.md"
tag-prefix = "core-"
alias-versions = "major" # Override: create v1, v1.2 aliases
# Monitor root dependencies
include-paths = [
"pyproject.toml",
"uv.lock",
]
# Core-specific hooks (must follow the [[tool.releez.projects]] entry it belongs to)
[tool.releez.projects.hooks]
post-changelog = [
["uv", "version", "{version}"],
]
# UI components project
[[tool.releez.projects]]
name = "ui"
path = "packages/ui"
changelog-path = "CHANGELOG.md"
tag-prefix = "ui-"
# No include-paths: only releases when UI code changes
Change Detection¶
How It Works¶
Releez detects changed projects using path-based filtering:
- For each project, find the latest git tag matching its
tag-prefix - Get all commits since that tag that touched the project's paths
- If commits exist, mark the project as changed
Monitored Paths¶
Each project monitors:
- Primary path: The
pathdirectory (e.g.,packages/core) - Additional paths: Anything in
include-paths(optional)
Example: Root File Changes¶
Projects can explicitly opt-in to monitoring root-level files:
[[tool.releez.projects]]
name = "core"
path = "packages/core"
include-paths = [
"pyproject.toml", # Root dependencies
"uv.lock", # Lock file
".github/", # CI changes
]
When to use include-paths:
- ✅ Shared dependencies affect this project
- ✅ Root config changes require releasing
- ❌ Independent microservices (don't need root files)
Tag Naming¶
Tag Patterns¶
Each project's tags follow a consistent pattern:
Examples:
- Core library:
core-1.2.3,core-v1,core-v1.2 - UI components:
ui-4.5.6,ui-v4,ui-v4.5 - API service:
api-2.0.0,api-v2,api-v2.0
Alias Versions¶
For full releases (not prereleases), Releez can create alias tags:
[[tool.releez.projects]]
name = "core"
tag-prefix = "core-"
alias-versions = "major" # Creates core-v1
Options:
"none"- No aliases (only exact version)"major"- Create major alias (e.g.,core-v1)"minor"- Create major and minor aliases (e.g.,core-v1,core-v1.2)
Note: Aliases are only created for full releases, not prereleases.
Release Workflows¶
Auto-Detect and Release Changed Projects¶
Output:
Detected 2 changed projects: core, ui
✓ core: Starting release for version core-1.2.3
Created branch: release/core-1.2.3
Created PR: https://github.com/org/repo/pull/123
✓ ui: Starting release for version ui-4.5.6
Created branch: release/ui-4.5.6
Created PR: https://github.com/org/repo/pull/124
Release Specific Projects¶
# Release only the core project
releez release start --project core
# Release multiple specific projects
releez release start --project core --project ui
Release All Projects¶
Release from a Support Branch (Maintenance Releases)¶
Support branches in monorepo mode use a project-scoped naming convention:
support/{tag-prefix}{major}.x (e.g. support/ui-1.x for a project with
tag-prefix = "ui-").
Create the branch with Releez, then release from it normally:
# Create support/ui-1.x from the latest ui-1.x.x tag
releez release support-branch 1 --project ui
# From the support branch, cut a release for the ui project
releez release start --project ui
Releez detects the branch, scopes git-cliff to tags in the ui-1.x line (e.g.
ui-1.0.0, ui-1.1.0), and uses the support branch as the PR base. Versions
that would bump to a different major are rejected.
Other projects in the same monorepo are unaffected; their releases run normally from the default base branch.
See the Support Branches guide for full details.
Check Which Projects Changed¶
# See which projects have unreleased changes
releez projects changed
# Output format for CI
releez projects changed --format json
JSON output:
uv Workspace Integration¶
If your monorepo is a
uv workspace, each package has
its own pyproject.toml with a [project] version field, and a shared
uv.lock at the repo root tracks the resolved dependency graph including
workspace member versions.
Recommended Hook Pattern¶
Use uv version (without --frozen) to bump the package version and regenerate
the lock file in a single step. Then explicitly stage uv.lock so it is
included in the release commit:
[tool.uv.workspace]
members = ["packages/core", "packages/ui"]
[tool.releez]
base-branch = "main"
[[tool.releez.projects]]
name = "core"
path = "packages/core"
tag-prefix = "core-"
changelog-path = "CHANGELOG.md"
include-paths = ["pyproject.toml", "uv.lock"]
[tool.releez.projects.hooks]
post-changelog = [
["uv", "version", "--directory", "packages/core", "{version}"],
["git", "add", "uv.lock"],
]
[[tool.releez.projects]]
name = "ui"
path = "packages/ui"
tag-prefix = "ui-"
changelog-path = "CHANGELOG.md"
include-paths = ["pyproject.toml", "uv.lock"]
[tool.releez.projects.hooks]
post-changelog = [
["uv", "version", "--directory", "packages/ui", "{version}"],
["git", "add", "uv.lock"],
]
Why this works:
uv version --directory packages/ui 0.2.3updatespackages/ui/pyproject.tomland re-resolvesuv.lockin one step, preserving all existing pins (it is equivalent touv lockwithout--upgrade).git add uv.lockexplicitly stages the updated lock file. Becauseuv.locklives outside the project directory, releez's selective staging would otherwise leave it out of the release commit.- The
{version}template variable is always the bare semver (e.g.0.2.3), with the tag prefix stripped — exactly whatuv versionexpects.
Why include-paths = ["pyproject.toml", "uv.lock"]:
Both files are declared as include-paths so that a root-level dependency
update (bump in pyproject.toml or uv.lock) registers as an unreleased change
and triggers a new release for all affected projects.
Changelog Staging¶
The project changelog (packages/ui/CHANGELOG.md) is inside the project
directory and is staged automatically by releez — no explicit git add needed
for it. Only files outside the project directory (like the root uv.lock)
require an explicit git add hook.
Dependency Management¶
Inter-Project Dependencies¶
When one project depends on another, you can use hooks to update versions:
[[tool.releez.projects]]
name = "app"
path = "packages/app"
tag-prefix = "app-"
# Update core dependency version after release
[tool.releez.projects.hooks]
post-changelog = [
["uv", "add", "--directory", "packages/app", "core@{version}"],
]
Strategies¶
Manual (recommended for Phase 1):
- Use
post-changeloghooks to update dependencies - Commit dependency updates in the same release PR
Semi-automatic (future):
- Detect projects with dependencies on released projects
- Suggest bumping dependent projects
Fully automatic (future):
- Auto-bump all dependent projects
- Create cascading releases
GitHub Actions Integration¶
Detect Changed Projects¶
Matrix Strategy (Homogeneous Stacks)¶
When all projects use the same tech stack, use the include matrix output to
fan out jobs automatically:
name: Build Changed Projects
on: pull_request
jobs:
detect:
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.detect.outputs.matrix }}
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v5
- run: uv tool install releez
- id: detect
run: |
releez projects changed --format json > changed.json
echo "matrix=$(jq -c '.include' changed.json)" >> $GITHUB_OUTPUT
build:
needs: detect
if: needs.detect.outputs.matrix != '[]'
strategy:
matrix: ${{ fromJson(needs.detect.outputs.matrix) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Build ${{ matrix.project }}
run: echo "Building ${{ matrix.project }}"
Conditional Jobs per Project (Polyglot Stacks)¶
When projects use different tech stacks, emit the projects JSON array and use
contains(fromJSON(...)) to gate each job:
jobs:
detect:
runs-on: ubuntu-latest
outputs:
projects: ${{ steps.detect.outputs.projects }}
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v5
- run: uv tool install releez
- id: detect
run: |
CHANGED=$(releez projects changed --format json)
echo "projects=$(echo "$CHANGED" | jq -c '.projects')" >> $GITHUB_OUTPUT
check-core:
needs: detect
if: contains(fromJSON(needs.detect.outputs.projects), 'core')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: pytest packages/core
check-ui:
needs: detect
if: contains(fromJSON(needs.detect.outputs.projects), 'ui')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- run: npm test --prefix packages/ui
Pitfall: Use
contains(fromJSON(outputs.projects), 'core'), notcontains(outputs.projects, 'core'). The latter does substring matching on the raw JSON string —'core'would incorrectly match"core-ui"too.
Version Artifacts for Changed Projects¶
- uses: hotdog-werx/releez@v0
id: version
with:
mode: version-artifact
project: ${{ matrix.project }}
- name: Build with version
run: |
echo "Version: ${{ steps.version.outputs.version }}"
echo "Changed: ${{ steps.version.outputs.changed }}"
Finalize Releases¶
name: Finalize Release
on:
pull_request:
types: [closed]
branches: [main]
jobs:
finalize:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: astral-sh/setup-uv@v5
- run: uv tool install releez
- uses: hotdog-werx/releez@v0
with:
mode: finalize
github-token: ${{ secrets.GITHUB_TOKEN }}
Migration from Single Repo¶
Step 1: Backup Current State¶
Step 2: Add Projects Configuration¶
# Before (single repo)
[tool.releez]
base-branch = "main"
# After (monorepo)
[tool.releez]
base-branch = "main"
[[tool.releez.projects]]
name = "main"
path = "."
changelog-path = "CHANGELOG.md"
tag-prefix = "" # Keep existing tag format
Step 3: Test Configuration¶
Step 4: Gradual Migration¶
If you want to transition to prefixed tags:
[[tool.releez.projects]]
name = "main"
path = "."
changelog-path = "CHANGELOG.md"
tag-prefix = "main-" # New prefix
Then:
- Release once with new prefix:
main-2.0.0 - Future releases use new format
- Old tags remain valid for history
CLI Reference¶
Projects Commands¶
# List all configured projects
releez projects list
# Show project details
releez projects info core
# Detect changed projects
releez projects changed
releez projects changed --format json
Release Commands with Project Support¶
# Start release for changed projects
releez release start
releez release start --project core
releez release start --all
# Preview version/tags for a project
releez release preview --project core
# Generate release notes for a project
releez release notes --project core
# Tag releases
releez release tag --project core
For release tag, release preview, and release notes, monorepo mode
requires explicit project selection: use --project <name> (repeatable) or
--all.
Detect Release from Branch¶
# Detect which project and version from branch name
releez release detect-from-branch --branch release/core-1.2.3
# Use current branch
releez release detect-from-branch
Output:
Troubleshooting¶
Project Not Detected as Changed¶
Issue: Made changes but releez projects changed doesn't show the project.
Solutions:
- Check if changes are committed:
git status - Verify paths in config match actual directory structure
- Check if latest tag exists:
git tag -l 'core-*' - Manually specify project:
releez release start --project core
Tag Already Exists¶
Issue: Error when creating tag: "tag already exists"
Solutions:
- Check existing tags:
git tag -l 'core-*' - Ensure
tag-prefixis unique per project - Delete local tag if incorrect:
git tag -d core-1.2.3
Root File Changes Not Triggering Release¶
Issue: Updated root pyproject.toml but project didn't release.
Solution: Add root files to include-paths:
[[tool.releez.projects]]
name = "core"
path = "packages/core"
include-paths = ["pyproject.toml", "uv.lock"]
Multiple Projects in One PR¶
Issue: Want to release multiple projects in a single PR.
Current limitation: Releez creates one PR per project for cleaner reviews.
Workaround: Manually release each project, then combine branches:
Best Practices¶
1. Use Descriptive Tag Prefixes¶
# Good: Clear, short prefixes
tag-prefix = "api-"
tag-prefix = "core-"
tag-prefix = "ui-"
# Avoid: Too verbose
tag-prefix = "my-awesome-project-"
2. Strategic Use of include-paths¶
Only add root files that truly affect the project:
# Library that uses root dependencies
[[tool.releez.projects]]
name = "core"
include-paths = ["pyproject.toml", "uv.lock"]
# Independent service
[[tool.releez.projects]]
name = "worker"
# No include-paths: only releases when worker code changes
3. Consistent Alias Versions¶
Set a global default, override only when needed:
[tool.releez]
alias-versions = "none" # Default: no aliases
[[tool.releez.projects]]
name = "sdk"
alias-versions = "major" # Exception: SDK needs v1 for convenience
4. Hooks for Automation¶
Use hooks to maintain consistency:
# Global hook: format changelogs for every project
[tool.releez.hooks]
post-changelog = [
["prettier", "--write", "{changelog}"],
]
# Per-project hook (must follow the [[tool.releez.projects]] entry it belongs to)
[[tool.releez.projects]]
name = "python-pkg"
path = "packages/python-pkg"
tag-prefix = "python-pkg-"
changelog-path = "CHANGELOG.md"
[tool.releez.projects.hooks]
post-changelog = [
["uv", "version", "--directory", "packages/python-pkg", "{version}"],
["git", "add", "uv.lock"],
]
5. CI/CD Optimization¶
Only build what changed:
jobs:
detect:
outputs:
matrix: ${{ steps.detect.outputs.matrix }}
has-changes: ${{ steps.detect.outputs.has-changes }}
steps:
- id: detect
run: |
CHANGED=$(releez projects changed --format json)
echo "matrix=$(echo "$CHANGED" | jq -c '.include')" >> $GITHUB_OUTPUT
echo "has-changes=$(echo "$CHANGED" | jq -e '.projects | length > 0')" >> $GITHUB_OUTPUT
build:
needs: detect
if: needs.detect.outputs.has-changes == 'true'
strategy:
matrix: ${{ fromJson(needs.detect.outputs.matrix) }}
Examples¶
See examples/monorepo-config.toml for a complete example configuration with:
- Multiple projects (core, ui, api)
- Different alias-versions strategies
- Custom hooks per project
- Strategic use of include-paths