Plumb

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

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

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