Why monorel?
The Go ecosystem doesn't have a release tool that handles the canonical Go monorepo layout cleanly. The layout in question:
- Main module at the repo root with bare
vX.Y.Ztags.go install <module>@v1.2.3requires this. - Sub-modules in subdirectories with
<path>/vX.Y.Ztags.go get <module>/transports/foo@v1.2.3requires this.
The off-the-shelf options each have a sharp edge for this layout.
release-please
Works, with friction. The friction lives in three sharp edges:
- Squash-merge strips Conventional Commits footers. GitHub's squash uses the PR title as the subject and the PR body as the message body. Per-commit
Release-As:orBREAKING CHANGE:footers don't survive. Workarounds (put it in the PR body) are easy to forget. - Full-history scans on new packages leak footers. A
Release-As:footer in any commit in repo history can apply to a newly-registered package because release-please has no "first release" boundary to stop the scan. We hit this onloglayer-go: an oldRelease-As: 1.1.0on an unrelated change set the initial version of a new sub-module tov1.1.0. exclude-pathsdoesn't catch path-attribution leaks for everything. A docs-only PR can still bump the main module if the path-attribution rules don't cover the directory. The list grows over time and is easy to forget.
These are all recoverable, but recovery means manual release-as cleanup, manual tag deletes, manual manifest fixes. The tool's mental model is "infer intent from commit history"; the failures are all variations of "the inference got confused."
Knope
Doesn't fit the bare-tag root convention. Knope per-package tag prefixes are mandatory; you can't get bare vX.Y.Z for the root module. That's a Go-specific requirement Knope wasn't built for.
changesets
Works conceptually but is JS-native. Adapting it to Go requires a synthetic package.json per Go module, a manual bridge between npm version and git tag, and a JS toolchain in the release path.
What monorel does
monorel takes the changesets idea (per-PR intent files, named affected packages, bump levels) and ships it as a Go-native CLI:
- Changeset files are the source of truth. Each release-affecting PR includes
.changeset/<name>.mdwith YAML frontmatter mapping package names to bump levels and a markdown body that becomes the changelog entry. No commit-message parsing, no path attribution, noRelease-As:footers. The class of "release tool got confused" failures becomes structurally impossible. - Tag format is per-package.
tag_prefix = ""for the main module yields barevX.Y.Z;tag_prefix = "transports/foo"for a sub-module yieldstransports/foo/vX.Y.Z. Both work in the same repo. - Always-open release PR. The bot orchestrator force-pushes a speculative-version branch and upserts a PR. Merging the PR runs
monorel releaseon the merge commit, pushes tags, and publishes per-tag releases. - Pre-release support.
monorel pre enter rcswitches the repo to release-candidate mode; subsequent releases append-rc.Nto the next stable version and increment a per-package counter.pre exitreturns to stable. - Provider-neutral. GitHub today; GitLab / Gitea / Bitbucket / Forgejo by adding a subpackage. The orchestrator never sees provider-specific types.
When monorel is overkill
If your repo is a single Go module with a single CHANGELOG and no plans to grow into a monorepo, git tag and a hand-written CHANGELOG are fine. monorel's value shows up when you have:
- two or more packages that version independently,
- a public API where downstream users
go getspecific sub-modules at specific tags, - a release cadence frequent enough that hand-crafting changelogs is friction.
Below that threshold, the tool's overhead (a monorel.toml, .changeset/*.md per PR, a release workflow) costs more than it saves.
Where monorel sits
monorel is a CLI plus a thin GitHub Action wrapper. There is no GitHub App, no hosted service, no per-repo install beyond a workflow file. The same binary runs in CI and on your laptop.
The next page walks through wiring it up: Getting Started.
