rlyx
rlyx is a Rust-first release tool for single repos and monorepos (JS, Python, Rust). It bumps versions, runs a post-bump command (lock refresh / installs), creates annotated Git tags (package-scoped in monorepos), writes a clean CHANGELOG, and (optionally) makes GitHub Releases — powered by Git history, not rituals.
- Per-package baselines. In monorepos, each package uses its own last tag as the baseline window.
- Package-scoped tags. Default tag shape is
{{name}}@v{{version}}(e.g.@scope/core@v1.4.2). Fully configurable (keep slashes, keep@). - Lock file safety.
rlyx.lockrecords baselines (including SHAs) so you can rebuild even if tags vanish or history shifts. - Zero registry publishing. rlyx doesn’t publish to npm/PyPI/Crates. It prepares Git artifacts; your CI owns the rest.
Installation
Requires: git.
Optional: gh (GitHub CLI) to resolve PR titles and create GitHub Releases.
Optional (JS projects): pnpm / yarn / npm if you want lockfiles refreshed when versions change.
What it does
- Detects project type (JS / Python / Rust).
- Bumps versions in
package.json,pyproject.toml/setup.*, orCargo.toml. - Runs a post-bump command before staging/commit/tag (e.g.
pnpm i,bun install,yarn install --mode update-lockfile,pip-compile, etc.). - Creates annotated tags:
- single repo:
vX.Y.Z - monorepo:
{{name}}@vX.Y.Z(configurable)
- single repo:
- Writes CHANGELOG using
git logfrom the correct baseline (last_tag..HEADor per-package). - Pushes commits and tags.
- Monorepo: can create one GitHub Release per package with scoped notes (when
ghis available).
Quick start
Single repo
# rlyx.toml
= "patch"
= "chore(release): bump to {{version}}"
= "release {{version}}"
# IMPORTANT for JS/Python: refresh locks after version bump
# choose what your repo needs (examples):
= "pnpm i"
[]
= true
= "CHANGELOG.md"
Monorepo
# rlyx.toml
= "patch"
# tag/message shapes
= "{{name}}@v{{version}}"
= true
= "{{name}}@{{version}}"
# run after bumping versions (pick one that fits your workspace)
= "pnpm i"
# IMPORTANT: only manage these packages (exact manifest names)
= ["@acme/core", "@acme/adapters"]
# optional, used for multi-package commits
= "chore(release): bump {{count}} packages\n\n{{list}}"
[]
= true
= "CHANGELOG.md"
# optional migration (discover old tag shapes)
[]
= ["core-v*", "core@*"]
First time (or after migrations):
Release:
# subset:
Want tags to match your package names exactly (keep slashes and
@)?
Use:pkg_tag_template = "{{name}}@{{version}}"and setinclude_v_prefixas you prefer.
rlyx will push tags safely even with@and/.
Configuration reference (rlyx.toml)
All keys are optional unless noted. Defaults are shown.
# ===== General =====
# Default semver bump when CLI omits one
= "patch" # "patch" | "minor" | "major"
# Commit message when bumping (used in single or multi, see below)
# Vars: {{name}} {{version}} {{count}} {{list}}
# Default is multi-friendly; override if you prefer a single-repo style.
= "chore(release): bump {{count}} packages\n\n{{list}}"
# Multi-package (monorepo) commit message; falls back to bump_commit_message if unset
= "chore(release): bump {{count}} packages\n\n{{list}}"
# Tag annotation (tag *message/body*, not the tag name)
# Vars: {{name}} {{version}}
= "release {{version}}"
# Header inserted at top of the changelog if missing
= "# Changelog"
# Tag *name* template for monorepo tags
# Use {{name}} to keep exact pkg name (including @scope and /).
# Use {{id}} to use a sanitized id (slashes -> _; leading @ removed).
= "{{name}}@v{{version}}"
# Whether to include "v" in {{version}} when building tags: v1.2.3 vs 1.2.3
= true
# Allow releasing with a dirty working tree (not recommended)
= false
# Limit rlyx to packages listed here (exact manifest "name" values)
= ["@acme/core", "@acme/adapters"]
# Command to run after bumping versions, before changelog/tag (optional)
= "pnpm i"
# ===== Changelog =====
[]
= true
= "CHANGELOG.md"
# ===== Migration hints (optional) =====
# Helps rlyx discover historical baselines when tag shapes changed.
[]
= ["core-v*", "core@*"]
Environment variable overrides
All optional; values mirror TOML:
RLYX_DEFAULT_BUMPRLYX_BUMP_COMMIT_MESSAGERLYX_MONOREPO_BUMP_COMMIT_MESSAGERLYX_BUMP_TAG_MESSAGERLYX_CHANGELOG_HEADERRLYX_CHANGELOG_ENABLE(true/false)RLYX_CHANGELOG_PATHRLYX_PKG_TAG_TEMPLATERLYX_INCLUDE_V_PREFIX(true/false)RLYX_POST_BUMP_COMMAND
CLI reference
Top-level forms
rlyx [KIND] [FLAGS] # single repo release (KIND: patch | minor | major)
rlyx --monorepo [FLAGS] # monorepo (interactive, filters to config packages)
rlyx monorepo [--packages CSV] # monorepo (interactive), subset override
rlyx prerelease [--id <str>] [--bump KIND] # single repo prerelease
rlyx finalize # single repo: convert last prerelease to stable
rlyx rollback [--local] # remove tags at HEAD; rewind release commits; default also updates remote
rlyx selfupdate # install latest from crates.io / prefer GH prerelease if on prerelease
rlyx lock rebuild [--force] [--baseline NAME:REF ...] [--from-file PATH]
rlyx lock verify
rlyx tag synthesize --baseline NAME:REF [...]
Common flags
--dry: perform everything except pushing--no-changelog: skip changelog write for this run--repo owner/repo: override detected GitHub repo (for URLs and GH release creation)--monorepo: force monorepo mode if auto-detect is ambiguous--packages "<a,b>": monorepo subcommand only; filter a subset--ignore_package: single repo only; bump/tag without rewriting manifest (tag-only)
prerelease notes
--iddefault:rc. Examples:v1.2.3-rc.1,v1.2.3-nightly.20250131[.N].--bumpcontrols the base bump (defaults todefault_bump).
finalize (single repo)
- Takes the last reachable prerelease tag, strips the
-presuffix, and createsvX.Y.Z. - Refuses if the stable tag already exists.
lock rebuild
- Builds/updates
rlyx.lockby discovering packages and anchoring per-package baselines. --baseline NAME:REFlets you pin a baseline:REFmay bev1.2.3,1.2.3, or a raw commit SHA.--forceoverwrites an existing lock file.--from-fileis reserved for future use.
tag synthesize
- Mints annotated tags at given refs without bumping files:
rollback
- Without
--local, removes remote tags at HEAD and force-pushes branch after rewinding obvious release commits. - With
--local, operates locally only.
Monorepo is currently interactive. Non-interactive per-package bump flags are on the roadmap. For CI flows today, run
rlyx lock rebuildin a preparatory step and userlyx --monorepoonly when a human is present, or scripttag synthesizefor tag-only flows.
CHANGELOG behavior
- New sections are prepended.
- Header:
## <tag> <YYYY-MM-DD> - Sections:
- Changes since
<base>:git log --no-merges --pretty=- %h %s - Pull requests: numbers like
(#123)optionally resolved viaghto titles - Authors: from
git shortlog -sn(singular/plural friendly) - Compare: link to the GitHub compare page
- Changes since
Monorepo: a single batch section (e.g. ## batch-20250131235959) with one sub-section per package tagged, each with its scoped commit window.
Tagging details & special characters
- Single repo tag name:
v<semver>. - Monorepo tag name: built from
pkg_tag_template. To keep slashes and@in tag names, use{{name}}(not{{id}}). - Tags are annotated and include durable trailers:
rlyx:id=<sanitized-tag-id> rlyx:pkg=<package-name> rlyx:ver=<version-core> rlyx:sha=<target-commit-sha>
If your remote/refspec complains about special chars, push the explicit ref:
Supported projects
- Node:
package.json(and lockfile refresh ifpnpm/yarn/npmexist) - Python:
pyproject.toml(PEP 621 / Poetry),setup.cfg,setup.py - Rust:
Cargo.toml(single crate or workspace; workspace members are updated)
Troubleshooting
- “Working tree is not clean.” Commit/stash changes or
allow_dirty = true(use sparingly). - “No baseline for package X.”
rlyx lock rebuild --baseline <name>:<v1.2.3|sha>or addmigration.<name>.legacy_tag_patterns. - “Push failed for tag with @/ /.”
Usepkg_tag_template = "{{name}}@{{version}}", and if necessary pushrefs/tags/<tag>. - “It asked me to bump 70 packages.”
Ensure yourpackages = [...]allowlist names exactly match each manifest’s"name". Userlyx monorepo --packages "a,b,c"for ad-hoc subsets. - “Monorepo ‘kind’ flag didn’t apply.”
Current monorepo flow is interactive; bump kind flags are reserved for future non-interactive mode.
Safety rails
- Clean tree check (unless
allow_dirty). - Baseline reachability checks on
lock verify/rebuild. - Semver-aware tag ordering & collision detection across legacy patterns.
- Rollback removes tags at HEAD and rewinds release commits (optionally pushes the branch).
CI hints
- Generate/refresh lock in a dedicated job:
&& || - For automated tag-only flows (no file edits), consider:
- For human-in-the-loop monorepo releases, run
rlyx --monorepolocally or in a TTY-enabled runner.
Command cheatsheet
# Single repo
||
# Monorepo
# Tags without bumping files
Notes on defaults you may want to change immediately
- If you want single-repo commits like
chore(release): bump to 1.2.3, overridebump_commit_messageaccordingly (the default is multi-package friendly). - If you want tag names to mirror packages (keep
@scope/and/):
pkg_tag_template = "{{name}}@{{version}}"and choose yourinclude_v_prefix.