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.

Two workflows, both first-class

Releaser supports two equally valid ways to drive releases:

  • Manual (default): you decide each bump. No extra config, free-form commits. See Manual releases.
  • Conventional Commits (opt-in): add commits: [enabled: true] and Releaser reads git log to decide bumps automatically. Includes pre-commit hook and GitHub Actions template. See Conventional Commits.

Pick whichever fits your team — cascade, pre-releases, publishing and changelogs work the same in both.

Features

FeatureReleaserVersioceGitHub Tag Bump
SemVer bump (patch/minor/major)YesYesYes
Pre-release tags (dev, beta, rc)YesPartialPartial
Same tag increments (dev.1 → dev.2)YesNoNo
Tag change keeps base (dev → beta)YesNoNo
Release (strip tag)YesNoNo
Cascade bumps to dependentsYesNoNo
Dependency graph (visual)YesNoNo
Topological Hex publishingYesNoNo
Release status (local vs Hex)YesNoNo
Changelog from git commitsYesYesNo
Git hooks (commit + tag)YesYesNo
Multi-file version syncYesYesNo
Build metadataYesYesNo
Explicit version setYesYesYes
Monorepo / poncho supportYesNoNo

Installation

Add to your root mix.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

SituationCommandResult
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

TagUsage
devActive development, may break things
alphaFirst internal test version
betaFeature-complete, may have bugs
rcRelease 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[1][1][0], cfdi_transform[1][1][0], cfdi_complementos
   sat_auth v1.0.1
    depends on: cfdi_csd[1][1][0]
       
 Level 3 
   cfdi_cancelacion v0.0.1
    depends on: sat_auth[2][1][1]
 end 

Reading the dep annotations

Each project-internal dep is rendered as <name>[level][count][deep]:

BracketMeaning
[level]Topological level of that dep (0 = leaf, no project deps). Colored per level — cycle: 0→cyan, 1→green, 2→yellow, 3→magenta, 4→red, 5→blue, then rem(level, 6) repeats.
[count]Number of direct project-internal deps that the dep itself has.
[deep]Of those [count] deps, how many themselves have at least one project-internal dep. Shallow (one level of look-ahead), not recursive.

Clean mode: when all three values are zero (a true leaf with no project deps), the dep prints as a bare name with no brackets — see cfdi_complementos in the example above.

Why this matters: at a glance you know if editing a dep cascades. cfdi_csd[1][1][0] means level 1, has 1 project dep, and that dep is a leaf — safe to edit in isolation. sat_auth[2][1][1] means level 2, has 1 project dep, and that dep itself has more project deps — editing reaches deeper into the graph.

Only the levels view is annotated. The dependents-tree form (mix releaser.graph <app>) is unchanged.

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

HookTypeWhat it does
Releaser.Hooks.GitTagpostgit add + git commit + git tag
Releaser.Hooks.ChangelogHookpostGenerate/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
# 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