rlyx 0.1.0

Cut a version, tag it, and publish a GitHub Release with raw git notes
Documentation
# 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.lock` records 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

```bash
cargo install --locked rlyx
```

**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.*`, or `Cargo.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)
- **Writes CHANGELOG** using `git log` from the correct baseline (`last_tag..HEAD` or per-package).
- **Pushes** commits and tags.
- **Monorepo:** can create **one GitHub Release per package** with scoped notes (when `gh` is available).

---

## Quick start

### Single repo

```toml
# rlyx.toml
default_bump = "patch"
bump_commit_message = "chore(release): bump to {{version}}"
bump_tag_message    = "release {{version}}"

# IMPORTANT for JS/Python: refresh locks after version bump
# choose what your repo needs (examples):
post_bump_command = "pnpm i"

[changelog]
enable = true
path   = "CHANGELOG.md"
```

```bash
rlyx patch     # or: rlyx minor | rlyx major
```

### Monorepo

```toml
# rlyx.toml
default_bump = "patch"

# tag/message shapes
pkg_tag_template = "{{name}}@v{{version}}"
include_v_prefix = true
bump_tag_message = "{{name}}@{{version}}"

# run after bumping versions (pick one that fits your workspace)
post_bump_command = "pnpm i"

# IMPORTANT: only manage these packages (exact manifest names)
packages = ["@acme/core", "@acme/adapters"]

# optional, used for multi-package commits
monorepo_bump_commit_message = "chore(release): bump {{count}} packages\n\n{{list}}"

[changelog]
enable = true
path   = "CHANGELOG.md"

# optional migration (discover old tag shapes)
[migration."@acme/core"]
legacy_tag_patterns = ["core-v*", "core@*"]
```

First time (or after migrations):

```bash
rlyx lock rebuild \
  --baseline @acme/core:v1.2.3 \
  --baseline @acme/adapters:v0.8.0   # only if a package lacks a discoverable baseline
```

Release:

```bash
rlyx --monorepo                # interactive per-package bumps
# subset:
rlyx monorepo --packages "@acme/core,@acme/adapters"
```

> Want tags to **match your package names exactly** (keep slashes and `@`)?  
> Use: `pkg_tag_template = "{{name}}@{{version}}"` and set `include_v_prefix` as you prefer.  
> rlyx will push tags safely even with `@` and `/`.

---

## Configuration reference (`rlyx.toml`)

All keys are optional unless noted. Defaults are shown.

```toml
# ===== General =====
# Default semver bump when CLI omits one
default_bump = "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.
bump_commit_message = "chore(release): bump {{count}} packages\n\n{{list}}"

# Multi-package (monorepo) commit message; falls back to bump_commit_message if unset
monorepo_bump_commit_message = "chore(release): bump {{count}} packages\n\n{{list}}"

# Tag annotation (tag *message/body*, not the tag name)
# Vars: {{name}} {{version}}
bump_tag_message = "release {{version}}"

# Header inserted at top of the changelog if missing
changelog_header = "# 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).
pkg_tag_template = "{{name}}@v{{version}}"

# Whether to include "v" in {{version}} when building tags: v1.2.3 vs 1.2.3
include_v_prefix = true

# Allow releasing with a dirty working tree (not recommended)
allow_dirty = false

# Limit rlyx to packages listed here (exact manifest "name" values)
packages = ["@acme/core", "@acme/adapters"]

# Command to run after bumping versions, before changelog/tag (optional)
post_bump_command = "pnpm i"

# ===== Changelog =====
[changelog]
enable = true
path   = "CHANGELOG.md"

# ===== Migration hints (optional) =====
# Helps rlyx discover historical baselines when tag shapes changed.
[migration."@acme/core"]
legacy_tag_patterns = ["core-v*", "core@*"]
```

### Environment variable overrides

All optional; values mirror TOML:

- `RLYX_DEFAULT_BUMP`
- `RLYX_BUMP_COMMIT_MESSAGE`
- `RLYX_MONOREPO_BUMP_COMMIT_MESSAGE`
- `RLYX_BUMP_TAG_MESSAGE`
- `RLYX_CHANGELOG_HEADER`
- `RLYX_CHANGELOG_ENABLE` (`true`/`false`)
- `RLYX_CHANGELOG_PATH`
- `RLYX_PKG_TAG_TEMPLATE`
- `RLYX_INCLUDE_V_PREFIX` (`true`/`false`)
- `RLYX_POST_BUMP_COMMAND`

---

## CLI reference

### Top-level forms

```text
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**

- `--id` default: `rc`. Examples: `v1.2.3-rc.1`, `v1.2.3-nightly.20250131[.N]`.
- `--bump` controls the base bump (defaults to `default_bump`).

**`finalize` (single repo)**

- Takes the last reachable prerelease tag, strips the `-pre` suffix, and creates `vX.Y.Z`.
- Refuses if the stable tag already exists.

**`lock rebuild`**

- Builds/updates `rlyx.lock` by discovering packages and anchoring **per-package** baselines.
- `--baseline NAME:REF` lets you pin a baseline: `REF` may be `v1.2.3`, `1.2.3`, or a raw commit SHA.
- `--force` overwrites an existing lock file.
- `--from-file` is reserved for future use.

**`tag synthesize`**

- Mints annotated tags at given refs without bumping files:
  ```bash
  rlyx tag synthesize --baseline @acme/core:v1.2.3 --baseline @acme/adapters:deadbeef
  ```

**`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 rebuild` in a preparatory step and use `rlyx --monorepo` only when a human is present, or script `tag synthesize` for 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 via `gh` to titles
  - **Authors**: from `git shortlog -sn` (singular/plural friendly)
  - **Compare**: link to the GitHub compare page

**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:

```bash
git push origin "refs/tags/@scope/pkg@0.2.0"
```

---

## Supported projects

- **Node:** `package.json` (and lockfile refresh if `pnpm`/`yarn`/`npm` exist)
- **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 add `migration.<name>.legacy_tag_patterns`.
- **“Push failed for tag with @/ /.”**  
  Use `pkg_tag_template = "{{name}}@{{version}}"`, and if necessary push `refs/tags/<tag>`.
- **“It asked me to bump 70 packages.”**  
  Ensure your `packages = [...]` allowlist names **exactly** match each manifest’s `"name"`. Use `rlyx 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:
  ```bash
  rlyx lock rebuild
  git add rlyx.lock && git commit -m "chore: refresh rlyx.lock" || true
  ```
- For automated tag-only flows (no file edits), consider:
  ```bash
  rlyx tag synthesize --baseline @acme/core:v1.2.3
  ```
- For human-in-the-loop monorepo releases, run `rlyx --monorepo` locally or in a TTY-enabled runner.

---

## Command cheatsheet

```bash
# Single repo
rlyx patch|minor|major
rlyx prerelease --bump minor --id rc
rlyx finalize
rlyx rollback [--local]
rlyx selfupdate

# Monorepo
rlyx lock rebuild [--force] [--baseline NAME:REF ...]
rlyx lock verify
rlyx --monorepo
rlyx monorepo --packages "@acme/core,@acme/adapters"

# Tags without bumping files
rlyx tag synthesize --baseline @acme/core:v1.2.3
```

---

### Notes on defaults you may want to change immediately

- If you want single-repo commits like `chore(release): bump to 1.2.3`, override `bump_commit_message` accordingly (the default is multi-package friendly).
- If you want tag names to **mirror packages** (keep `@scope/` and `/`):  
  `pkg_tag_template = "{{name}}@{{version}}"` and choose your `include_v_prefix`.