defmodule Mix.Tasks.Release.Publish do
use Mix.Task
@shortdoc "Tag and publish current version to Hex"
@moduledoc """
Automates a library release for Hex.
Steps:
1. Ensure git worktree is clean.
2. Run `mix precommit` (unless `--skip-precommit`).
3. Read version from `mix.exs` and derive tag `v<version>`.
4. Ensure tag does not already exist.
5. Create annotated tag.
6. Optionally push commit + tag.
7. Optionally publish package/docs to Hex.
## Options
* `--remote <name>` - git remote to push to (default: `origin`)
* `--message <msg>` - tag annotation message (default: `Release v<version>`)
* `--no-tag` - skip tag creation
* `--no-push` - skip git push
* `--no-publish` - skip `mix hex.publish`
* `--skip-precommit` - skip `mix precommit`
* `--yes` - pass `--yes` to `mix hex.publish`
## Examples
mix release.publish --yes
mix release.publish --no-publish
mix release.publish --no-tag --no-push --yes
"""
@switches [
remote: :string,
message: :string,
no_tag: :boolean,
no_push: :boolean,
no_publish: :boolean,
skip_precommit: :boolean,
yes: :boolean
]
@impl Mix.Task
def run(args) do
Mix.Task.run("app.start")
{opts, _argv, invalid} = OptionParser.parse(args, strict: @switches)
if invalid != [] do
Mix.raise("invalid options: #{inspect(invalid)}")
end
remote = Keyword.get(opts, :remote, "origin")
version = Mix.Project.config()[:version] || Mix.raise("missing project version")
tag = "v#{version}"
message = Keyword.get(opts, :message, "Release #{tag}")
ensure_clean_git!()
unless opts[:skip_precommit] do
run_mix!(~w(precommit))
end
unless opts[:no_tag] do
ensure_tag_missing!(tag)
run_git!(~w(tag -a) ++ [tag, "-m", message])
end
unless opts[:no_push] do
run_git!(~w(push) ++ [remote, "HEAD"])
unless opts[:no_tag] do
run_git!(~w(push) ++ [remote, tag])
end
end
unless opts[:no_publish] do
publish_args = if opts[:yes], do: ~w(hex.publish --yes), else: ~w(hex.publish)
run_mix!(publish_args)
end
Mix.shell().info("release.publish complete for #{tag}")
end
defp ensure_clean_git! do
{output, 0} = capture_cmd!("git", ["status", "--porcelain"])
if String.trim(output) != "" do
Mix.raise("git worktree not clean; commit or stash changes first")
end
end
defp ensure_tag_missing!(tag) do
{output, _code} = capture_cmd!("git", ["tag", "-l", tag])
if String.trim(output) != "" do
Mix.raise("tag already exists: #{tag}")
end
end
defp run_git!(args), do: run_or_raise!("git", args)
defp run_mix!(args), do: run_or_raise!("mix", args)
defp run_or_raise!(cmd, args) do
{_output, code} = stream_cmd!(cmd, args)
if code != 0 do
Mix.raise("command failed: #{Enum.join([cmd | args], " ")}")
end
end
defp stream_cmd!(cmd, args) do
Mix.shell().info("$ #{Enum.join([cmd | args], " ")}")
System.cmd(cmd, args, into: IO.stream(:stdio, :line), stderr_to_stdout: true)
end
defp capture_cmd!(cmd, args) do
Mix.shell().info("$ #{Enum.join([cmd | args], " ")}")
System.cmd(cmd, args, stderr_to_stdout: true)
end
end