This guide explains how Releaser publishes monorepo packages to Hex in the correct order, handling internal dependencies automatically.
The problem
In a monorepo, packages depend on each other via path: references:
# apps/api/mix.exs
defp deps do
[{:my_core, path: "../core"}]
endBut Hex doesn't understand path: — it needs version constraints:
{:my_core, "~> 1.1"}So to publish api, you need to:
- First publish
coreto Hex - Change
api's dep frompath:to the published version - Publish
api - Change everything back to
path:for local development
With 34 packages and nested dependencies, doing this manually is painful. Releaser automates the entire process.
How it works
Step 1: See the plan
$ mix releaser.publish --dry-run
=== Releaser Publish ===
Level 0:
my_core v1.1.0
Level 1:
my_api v1.2.4 (deps: my_core)
my_worker v0.5.1 (deps: my_core)
--dry-run: nothing will be published
my_api mix.exs changes:
{:my_core, path: "..."} → {:my_core, "~> 1.1"}
my_worker mix.exs changes:
{:my_core, path: "..."} → {:my_core, "~> 1.1"}
This shows:
- Level 0: Packages with no internal deps are published first
- Level 1: Packages that depend on level 0
- Path replacement: What
path:deps become
Step 2: Publish
$ mix releaser.publish
For each package (in dependency order), Releaser:
- Backs up the original
mix.exs - Replaces
{:my_core, path: "../core"}→{:my_core, "~> 1.1"} - Injects
package/0with license and links if missing - Runs
mix hex.publish --yes - Restores the original
mix.exs
If any publish fails, all mix.exs files are restored immediately.
Step 3: Verify
After publishing, your mix.exs files are back to using path: for
local development. Nothing changes in your working tree.
Publish options
Bump before publishing
# Bump all packages by patch before publishing
$ mix releaser.publish --bump patch
Publish specific packages
# Only publish my_api (automatically includes my_core because it depends on it)
$ mix releaser.publish --only my_api
This resolves transitive dependencies: if api depends on core, and
core depends on openssl, all three are published in the right order.
Publish to a Hex organization
$ mix releaser.publish --org my_company
Real-world example
Here's a 34-package monorepo with 4 levels of dependencies:
$ mix releaser.publish --dry-run
=== Releaser Publish ===
Level 0:
cfdi_catalogos v4.0.16
cfdi_complementos v4.0.17
cfdi_transform v4.0.14
clir_openssl v0.0.17
saxon_he v12.5.2
... (23 more)
Level 1:
cfdi_csd v4.0.16 (deps: clir_openssl)
cfdi_designs v1.0.0 (deps: cfdi_xml2json, cfdi_utils, cfdi_types, cfdi_complementos)
Level 2:
cfdi_xml v4.0.18 (deps: cfdi_csd, cfdi_transform, cfdi_complementos, cfdi_catalogos, cfdi_xsd, saxon_he)
sat_auth v1.0.1 (deps: cfdi_csd)
Level 3:
cfdi_cancelacion v0.0.1 (deps: sat_auth)
cfdi_descarga v0.0.1 (deps: sat_auth)
cfdi_csd mix.exs changes:
{:clir_openssl, path: "..."} → {:clir_openssl, "~> 0.0"}
cfdi_xml mix.exs changes:
{:cfdi_csd, path: "..."} → {:cfdi_csd, "~> 4.0"}
{:cfdi_transform, path: "..."} → {:cfdi_transform, "~> 4.0"}
{:cfdi_complementos, path: "..."} → {:cfdi_complementos, "~> 4.0"}
{:cfdi_catalogos, path: "..."} → {:cfdi_catalogos, "~> 4.0"}
{:cfdi_xsd, path: "..."} → {:cfdi_xsd, "~> 4.0"}
{:saxon_he, path: "..."} → {:saxon_he, "~> 12.5"}
The package/0 injection
Many monorepo packages don't have package/0 defined because they're not
published individually. Releaser automatically injects it during publish:
defp package do
[
licenses: ["MIT"],
links: %{"GitHub" => "https://github.com/me/project"},
files: ~w(lib mix.exs README.md LICENSE)
]
endYou can customize the defaults in your config:
releaser: [
publisher: [
package_defaults: [
licenses: ["Apache-2.0"],
links: %{"GitHub" => "https://github.com/me/project"},
files: ~w(lib priv mix.exs README.md LICENSE)
]
]
]Prerequisites
- Each app must have a
descriptionin itsmix.exs - The app must compile without errors
- You must be authenticated to Hex (see below)
Hex authentication
Releaser supports both auth modes that mix hex.publish accepts:
Local interactive auth (good for personal machines):
mix hex.user auth
# Prompts for username and password, then persists a token.
After that, mix releaser.publish just works.
Environment variable (good for CI / one-off pushes):
HEX_API_KEY=<your_key> mix releaser.publish
Get a key at https://hex.pm/dashboard/keys. The value is an auth scoped
key with write permission.
If neither method is available, mix releaser.publish aborts with a clear
message instead of hanging on the interactive password prompt.
Live output
mix releaser.publish streams the output of mix hex.publish to your
terminal in real time — you will see the upload progress, checksum, and
URLs as Hex prints them, rather than a single buffered dump at the end.
Before publishing: check status
Use mix releaser.status to see what needs publishing:
$ mix releaser.status
=== Release 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.
Run mix releaser.publish --dry-run to see the plan.
Recommended workflow
# 1. Develop with pre-release tags
mix releaser.bump cfdi_xml patch --tag dev
# ... make changes ...
mix releaser.bump cfdi_xml patch --tag dev
# 2. When ready, release
mix releaser.bump cfdi_xml release
# 3. Check what's pending
mix releaser.status
# 4. Preview the publish plan
mix releaser.publish --dry-run
# 5. Publish
mix releaser.publish