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:
- The git working tree is clean.
- The target tag does not exist locally.
- The target tag does not exist on the remote — checked with
git ls-remoteup front, rather than discovered at push time with a commit and a tag already made. - The README install snippet fits the version being released (opt-in, below).
mix format --check-formatted.mix test.mix docsbuilds.
Then, and only then:
mix.exsis rewritten with the new version.- It is committed as
Release vX.Y.Z. - An annotated tag
vX.Y.Zis created. HEADand the tag are pushed.mix hex.publishuploads 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.MINORof the version being released. For a 0.x project, where a minor bump is a breaking change, this is the honest one:~> 0.3tells a reader that0.4.0is 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, defaultorigin--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 noex_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