Relex

Relaxed release management for Elixir projects. Automates semantic version bumping, changelog updates, git commits/tags.

Release + Elixir = Relex — because shipping versions should feel like a mix away from relaxing.

Note: This project is developed extensively with AI-assisted engineering. While every effort is made to ensure quality, use it at your own risk and always review the changes it makes to your project.

Installation

Install relex as a mix archive (globally available in any project):

mix archive.install hex relex

Then in your project, run the init task to scaffold CHANGELOG.md and a .relex.exs config template:

mix relex.init
mix relex.init --ai-cmd gemini

Configuration

Relex reads configuration from .relex.exs in your project root (like .formatter.exs). Create one manually or via mix relex.init:

# .relex.exs
[
  ai_cmd: {"gemini", ["-p"]},
  branch_pre: %{
    "staging" => "rc",
    ~r/^preview\// => "beta"
  }
]

Both keys are optional. Defaults:

Key Default Description
ai_cmd{"claude", ["-p"]} AI CLI for changelog generation
branch_pre%{} Custom branch-to-suffix mappings (merged with built-in defaults)

Semantic Versioning

This tool follows Semantic Versioning (MAJOR.MINOR.PATCH):

Pre-release versions

Pre-release identifiers (e.g. 1.2.0-rc.1) are supported and automatically inferred from the current git branch:

Branch Suffix Example
main / master (none) 1.2.0
develop / dev-dev.N1.2.0-dev.1
release/* / rc/*-rc.N1.2.0-rc.1
beta/*-beta.N1.2.0-beta.1
alpha/*-alpha.N1.2.0-alpha.1

The full lifecycle looks like:

main:    1.1.0 (stable)
              ↓ branch develop
develop: 1.2.0-dev.1 → 1.2.0-dev.2 → 1.2.0-dev.3
                                       ↓ branch release/1.2.0
release: 1.2.0-rc.1  → 1.2.0-rc.2
                         ↓ merge to main
main:    1.2.0 (stable)

Rules:

Custom branch mappings can be configured in .relex.exs:

[
  branch_pre: %{
    "staging" => "rc",
    ~r/^preview\// => "beta"
  }
]

Build metadata (e.g. 1.0.0+build.42) is part of the semver spec but is not currently supported by this tool.

Usage

mix relex              # list available commands
mix help relex.patch   # help for a specific command

Commands

Release

mix relex.patch              # bug fix:         1.0.8 -> 1.0.9
mix relex.minor              # new feature:     1.0.8 -> 1.1.0
mix relex.major              # breaking change: 1.0.8 -> 2.0.0
mix relex.minor --pre rc     # pre-release:     1.1.0 -> 1.2.0-rc.1
mix relex.minor --dry-run    # preview changes
  1. Resolves the pre-release suffix from the current branch (or --pre flag)
  2. Validates the git working directory is clean
  3. If [Unreleased] in CHANGELOG.md is empty, offers to generate an entry using Claude Code (requires claude CLI)
  4. Bumps the version in mix.exs
  5. Updates CHANGELOG.md with the release date and comparison links
  6. Commits changes with message release: vX.Y.Z
  7. Creates an annotated git tag vX.Y.Z (stable releases only — pre-releases skip tagging by default)

AI-generated changelog

When releasing with an empty [Unreleased] section, the tool gathers the git log and diff since the last tag and asks an AI CLI to suggest a changelog entry using Keep a Changelog headings (### Added, ### Changed, ### Fixed, ### Removed). You are prompted to confirm before anything is written.

By default it uses Claude Code. To use a different AI CLI, configure it in .relex.exs:

[ai_cmd: {"gemini", ["-p"]}]

The prompt is appended as the last argument.

Merge (post-merge promotion)

mix relex.merge              # 1.5.1-dev.3 -> 1.5.1
mix relex.merge minor        # 1.5.1-dev.3 -> 1.6.0
mix relex.merge major        # 1.5.1-dev.3 -> 2.0.0
mix relex.merge --dry-run    # preview changes
mix relex.merge --no-tag     # promote without tagging

Promotes a pre-release version to stable after merging a development branch into main/master. Without arguments, strips the pre-release suffix. Pass a segment to bump higher.

If you run mix relex.patch (or minor/major) on a stable branch and the current version has a pre-release suffix, the task will detect this and suggest using merge instead.

Tagging behavior

By default, stable releases create an annotated git tag (vX.Y.Z) and suggest pushing only that specific tag. Pre-release versions skip tagging entirely to avoid polluting the tags list with -dev.N, -rc.N, etc.

Use --no-tag to skip tagging on any release, including stable ones.

Rollback

mix relex.rollback           # undo last release (soft reset)
mix relex.rollback --hard    # undo and discard changes
mix relex.rollback --dry-run # preview rollback

Undoes the last release by deleting its tag and resetting the release commit. Requires the latest commit to be a release commit (release: vX.Y.Z).

Amend

mix relex.amend              # fold changes into release commit
mix relex.amend --dry-run    # preview amend

Amends the last release commit with any current changes and re-tags. Useful for fixing a typo or adding a missing file right after releasing.

Options

Option Applies to Description
--dry-run all commands Preview changes without applying them
--pre LABEL patch, minor, major Create a pre-release version (e.g., --pre rc, --pre beta)
--no-tag patch, minor, major, merge Skip creating a git tag for the release
--hard rollback Discard release changes entirely

Programmatic API

Relex.read_version()
#=> "1.2.0"

Relex.bump("1.2.3", :minor)
#=> "1.3.0"

Relex.bump("1.1.0", :minor, "rc")
#=> "1.2.0-rc.1"

Relex.bump("1.2.0-rc.1", :patch, "rc")
#=> "1.2.0-rc.2"

Relex.bump("1.2.0-rc.2", :patch)
#=> "1.2.0"

Relex.promote("1.5.1-dev.3", :minor)
#=> "1.6.0"

Relex.has_pre?("1.2.0-rc.1")
#=> true

Relex.write_version("1.3.0")