A mix publish task that releases a package to Hex.

Its one idea: every check runs before anything is written. A run that fails verification leaves the repository exactly as it was — no bumped mix.exs, no commit to amend, no local tag to delete.

mix publish patch          # 0.1.1 -> 0.1.2
mix publish minor          # 0.1.1 -> 0.2.0
mix publish major          # 0.1.1 -> 1.0.0
mix publish 0.4.0-rc.1     # an explicit version, which must sort above the current one

Installation

def deps do
  [
    {:plumb, "~> 0.2", only: :dev, runtime: false}
  ]
end

What it does, in order

Nothing is written until every one of these has passed:

  1. The git working tree is clean.
  2. The target tag does not exist locally.
  3. The target tag does not exist on the remote — checked with git ls-remote up front, rather than discovered at push time with a commit and a tag already made.
  4. The README install snippet fits the version being released (opt-in, below).
  5. mix format --check-formatted.
  6. mix test.
  7. mix docs builds.

Then, and only then:

  1. mix.exs is rewritten with the new version.
  2. It is committed as Release vX.Y.Z.
  3. An annotated tag vX.Y.Z is created.
  4. HEAD and the tag are pushed.
  5. mix hex.publish uploads the package and its documentation.

The tag is pushed before the upload, so the source_ref in the published documentation resolves the moment the docs go live rather than 404ing until you remember to push it.

Why the docs build is a release gate

mix docs failing — or emitting warnings you have learned to scroll past — means the published page has broken links: a @doc pointing at a function that was renamed, or at a module marked @moduledoc false. No test catches that, because nothing is wrong at runtime. It is only wrong for the person reading your documentation, and by then it is published.

Running it as a gate turns that into something you fix before the release rather than after it.

The README check

A README telling people to depend on ~> 0.1.1 when you have just published 0.2.0 is wrong in a way nothing else catches: the code compiles, the tests pass, the docs build, and everyone following the install instructions silently gets the old release.

It is off unless asked for, since README shapes vary:

def project do
  [
    plumb: [readme: :exact]
  ]
end
  • :satisfies — the version being released must satisfy the requirement the README names. Imposes no style; catches a release falling outside the range the README allows at all.
  • :exact — the README must name exactly ~> MAJOR.MINOR of the version being released. For a 0.x project, where a minor bump is a breaking change, this is the honest one: ~> 0.3 tells a reader that 0.4.0 is a safe upgrade, and it is not.
  • :readme_path — defaults to "README.md".

The check reads the version being released, not the one in mix.exs. Releasing 0.4.0 from a README that says ~> 0.3 fails before the bump, so you fix the snippet and re-run rather than discovering it after publishing.

Where the version comes from

A @version "..." module attribute is preferred, and is what you want anyway for source_ref in your docs config:

@version "0.3.0"

def project do
  [version: @version, docs: [source_ref: "v#{@version}"]]
end

A literal version: "0.3.0" in the project config works too. Only the first occurrence is rewritten, and only when the version is a literal string — a version: @version that points at an attribute is left alone, so the attribute is what gets bumped.

A mix.exs that computes its version some other way is refused rather than guessed at.

Options

  • --dry-run — run every check and build the tarball, report what would follow, write nothing
  • --remote NAME — the git remote to push to, default origin
  • --allow-untracked — let untracked files through the clean-tree check. Tracked changes still block it, and untracked files are still not committed
  • --skip CHECKS — comma-separated checks to leave out: format, test, docs. An escape hatch for a project with no ex_doc, not a habit
mix publish minor --dry-run
mix publish patch --remote upstream
mix publish patch --skip docs

What it deliberately does not do

Changelog entries. Generating one means either parsing commit messages or templating a stub, and both produce a changelog that reads like a git log. If you want that, expublish does it well and covers much of the same ground.

Publishing from CI. The task prompts through mix hex.publish, which is the confirmation step that stops an accidental release. Automating that away is the opposite of the point.

License

MIT