Releaser

Monorepo versioning, changelog, and Hex publishing for Elixir poncho/umbrella projects.

The only Hex package that handles versioning + publishing for Elixir monorepos with internal dependencies. Think Rush (Node.js) but for Elixir.

Features

Feature Releaser Versioce GitHub Tag Bump
SemVer bump (patch/minor/major) Yes Yes Yes
Pre-release tags (dev, beta, rc) Yes Partial Partial
Same tag increments (dev.1 → dev.2) Yes No No
Tag change keeps base (dev → beta) Yes No No
Release (strip tag) Yes No No
Cascade bumps to dependents Yes No No
Dependency graph (visual) Yes No No
Topological Hex publishing Yes No No
Release status (local vs Hex) Yes No No
Changelog from git commits Yes Yes No
Git hooks (commit + tag) Yes Yes No
Multi-file version sync Yes Yes No
Build metadata Yes Yes No
Explicit version set Yes Yes Yes
Monorepo / poncho support Yes No No

Installation

Add to your rootmix.exs:

defp deps do
  [
    {:releaser, "~> 0.1", only: :dev, runtime: false}
  ]
end

Quick start

# List all apps and versions
mix releaser.bump --list

# See dependency graph
mix releaser.graph

# Bump a package
mix releaser.bump my_app patch

# Check what needs publishing
mix releaser.status

# Publish everything to Hex
mix releaser.publish --dry-run

Versioning

Basic bump

mix releaser.bump my_app patch        # 4.0.17 → 4.0.18
mix releaser.bump my_app minor        # 4.0.17 → 4.1.0
mix releaser.bump my_app major        # 4.0.17 → 5.0.0

Explicit version

mix releaser.bump my_app 2.0.0        # set to exact version

Build metadata

mix releaser.bump my_app patch --build 20260420    # 4.0.18+20260420

Bump all apps

mix releaser.bump --all patch          # bump every app

Pre-release tags

Full lifecycle support for pre-release versions following SemVer 2.0.

Lifecycle

  ┌─────────────────────────────────────────────────────────────────┐
  │                   Version lifecycle                             │
  ├─────────────────────────────────────────────────────────────────┤
  │                                                                 │
  │  4.0.17 (current stable)                                        │
  │    │                                                            │
  │    ├── releaser.bump my_app patch --tag dev                     │
  │    │     → 4.0.18-dev.1          bump base + add tag            │
  │    │                                                            │
  │    ├── releaser.bump my_app patch --tag dev                     │
  │    │     → 4.0.18-dev.2          same tag = increment only      │
  │    │                                                            │
  │    ├── releaser.bump my_app patch --tag dev                     │
  │    │     → 4.0.18-dev.3          another dev fix                │
  │    │                                                            │
  │    ├── releaser.bump my_app patch --tag beta                    │
  │    │     → 4.0.18-beta.1         tag change = keeps base        │
  │    │                                                            │
  │    ├── releaser.bump my_app patch --tag beta                    │
  │    │     → 4.0.18-beta.2         beta fix                       │
  │    │                                                            │
  │    ├── releaser.bump my_app patch --tag rc                      │
  │    │     → 4.0.18-rc.1           release candidate              │
  │    │                                                            │
  │    ├── releaser.bump my_app release                             │
  │    │     → 4.0.18                strip tag = stable release     │
  │    │                                                            │
  │  4.0.18 (new stable)                                            │
  │                                                                 │
  └─────────────────────────────────────────────────────────────────┘

Tag rules

Situation Command Result
Clean 4.0.17patch --tag dev4.0.18-dev.1 (bump + tag)
Same tag -dev.2patch --tag dev4.0.18-dev.3 (increment)
Different tag -dev.3patch --tag beta4.0.18-beta.1 (keep base)
Any tag -beta.2release4.0.18 (strip tag)

Available tags

Tag Usage
dev Active development, may break things
alpha First internal test version
beta Feature-complete, may have bugs
rc Release candidate, production-ready except bugs

SemVer ordering: alpha < beta < dev < rc < stable

Cascade bumps

When you bump a package, all packages that depend on it automatically receive a patch bump:

mix releaser.bump clir_openssl minor --dry-run
Version changes:
  clir_openssl              0.0.17 → 0.1.0   (direct)
  cfdi_csd                  4.0.16 → 4.0.17   (cascade)
  sat_auth                  1.0.1  → 1.0.2    (cascade)
  cfdi_xml                  4.0.18 → 4.0.19   (cascade)
  cfdi_cancelacion          0.0.1  → 0.0.2    (cascade)
  cfdi_descarga             0.0.1  → 0.0.2    (cascade)

Disable with --no-cascade.

Dependency graph

mix releaser.graph
╔══════════════════════════════════════════════════╗
║           Dependency Graph                       ║
╚══════════════════════════════════════════════════╝

┌── Level 0  (no internal deps) ──┐
│   cfdi_catalogos v4.0.16
│   clir_openssl v0.0.17
│   saxon_he v12.5.2
│   ... (28 apps)
│       ▼
┌── Level 1 ──┐
│   cfdi_csd v4.0.16
│   └─ depends on: clir_openssl
│       ▼
┌── Level 2 ──┐
│   cfdi_xml v4.0.18
│   └─ depends on: cfdi_csd, cfdi_transform, ...
│   sat_auth v1.0.1
│   └─ depends on: cfdi_csd
│       ▼
┌── Level 3 ──┐
│   cfdi_cancelacion v0.0.1
│   └─ depends on: sat_auth
└── end ──┘

Show dependents of a specific app:

mix releaser.graph cfdi_csd
Dependents of cfdi_csd:
  └─ sat_auth
    └─ cfdi_cancelacion
    └─ cfdi_descarga
  └─ cfdi_xml

Publishing to Hex

Publishes all packages in topological order (dependencies first). Automatically replaces path: deps with Hex versions and restores after publishing.

# See the publish plan
mix releaser.publish --dry-run

# Publish everything
mix releaser.publish

# Bump + publish
mix releaser.publish --bump patch

# Only specific apps (+ their deps automatically)
mix releaser.publish --only cfdi_xml

# Publish to a Hex organization
mix releaser.publish --org myorg

What happens internally

For each package (in dependency order):

  1. Backup mix.exs
  2. Bump version (if --bump)
  3. Replace {:dep, path: "..."}{:dep, "~> X.Y"}
  4. Inject package/0 if missing
  5. mix hex.publish --yes
  6. Restore original mix.exs (always, even on failure)

Release status

Compare local versions against what's published on Hex:

mix releaser.status
Package              Local       Hex         Status
cfdi_xml             4.0.19      4.0.18      ahead
cfdi_csd             4.0.16      4.0.16      published
cfdi_complementos    4.0.18-dev.1  4.0.17    pre-release
my_new_app           0.1.0       —           unpublished

2 package(s) need publishing.

Changelog

Generate changelogs from git commits using conventional commit prefixes:

# Generate for all apps
mix releaser.changelog

# Generate for one app
mix releaser.changelog cfdi_xml

# Preview without writing
mix releaser.changelog --dry-run

# From a specific ref
mix releaser.changelog --from v4.0.17

Commits should follow conventional commits:

feat: add CartaPorte 3.1 support
fix: correct XML encoding for special characters
refactor: extract version parsing to struct
breaking: remove deprecated cer/key modules

Output follows Keep a Changelog format.

Hooks

Pre and post-bump hooks for custom automation.

Built-in hooks

Hook Type What it does
Releaser.Hooks.GitTag post git add + git commit + git tag
Releaser.Hooks.ChangelogHook post Generate/update CHANGELOG.md

Custom hooks

defmodule MyProject.NotifySlack do
  @behaviour Releaser.Hooks.PostHook

  @impl true
  def run(%{app: app, new_version: version, changes: changes}) do
    # Send Slack notification...
    :ok
  end
end

Disable hooks

mix releaser.bump my_app patch --no-hooks

Configuration

All config lives in your root mix.exs under the :releaser key:

def project do
  [
    app: :my_project,
    version: "0.1.0",
    deps: deps(),
    releaser: [
      # Root directory containing apps (default: "apps")
      apps_root: "apps",

      # Additional files to sync version in
      version_files: [
        {"README.md", ~r/@version (\S+)/},
        {"Dockerfile", ~r/ARG VERSION=(\S+)/}
      ],

      # Changelog configuration
      changelog: [
        path: "CHANGELOG.md",
        anchors: %{
          "feat" => "Added",
          "fix" => "Fixed",
          "refactor" => "Changed",
          "docs" => "Documentation",
          "perf" => "Performance",
          "breaking" => "Breaking Changes"
        }
      ],

      # Pre/post hooks
      hooks: [
        pre: [],
        post: [Releaser.Hooks.GitTag, Releaser.Hooks.ChangelogHook]
      ],

      # Hex publishing defaults
      publisher: [
        org: nil,
        package_defaults: [
          licenses: ["MIT"],
          links: %{"GitHub" => "https://github.com/me/project"},
          files: ~w(lib mix.exs README.md LICENSE)
        ]
      ]
    ]
  ]
end

All commands

# Versioning
mix releaser.bump <app> <major|minor|patch>     # bump with cascade
mix releaser.bump <app> <major|minor|patch> --tag dev
mix releaser.bump <app> release                  # strip pre-release
mix releaser.bump <app> 2.0.0                    # explicit version
mix releaser.bump --list                         # list versions
mix releaser.bump --all patch                    # bump all apps

# Graph
mix releaser.graph                               # full dependency graph
mix releaser.graph <app>                         # dependents of app

# Publishing
mix releaser.publish                             # publish all to Hex
mix releaser.publish --dry-run                   # show plan
mix releaser.publish --only app1,app2            # only these + deps
mix releaser.publish --bump patch                # bump before publish
mix releaser.publish --org myorg                 # Hex organization

# Status
mix releaser.status                              # local vs Hex comparison

# Changelog
mix releaser.changelog                           # generate for all
mix releaser.changelog <app>                     # generate for one
mix releaser.changelog --from v1.0.0             # from specific ref

# Global options (all commands)
--dry-run                                        # preview without changes
--no-hooks                                       # skip pre/post hooks

Recommended workflow

# 1. Start development
mix releaser.bump my_app patch --tag dev

# 2. Iterate
mix releaser.bump my_app patch --tag dev          # dev.1 → dev.2

# 3. Promote to beta
mix releaser.bump my_app patch --tag beta          # dev.3 → beta.1

# 4. Release candidate
mix releaser.bump my_app patch --tag rc

# 5. Final release
mix releaser.bump my_app release                   # rc.1 → stable

# 6. Check what needs publishing
mix releaser.status

# 7. Publish
mix releaser.publish

License

MIT