Current section

Files

Jump to
ecto_query_explorer lib mix tasks bump.ex
Raw

lib/mix/tasks/bump.ex

defmodule Mix.Tasks.Bump do
@moduledoc """
Prepares a new release by bumping version, updating changelog, and creating a git tag.
## Usage
mix bump # 0.1.4 -> 0.1.5 (patch by default)
mix bump patch # 0.1.4 -> 0.1.5
mix bump minor # 0.1.4 -> 0.2.0
mix bump major # 0.1.4 -> 1.0.0
mix bump --dry-run # preview changelog without making changes
This task will:
1. Bump the version in VERSION file
2. Add a new section to CHANGELOG.md
3. Commit the changes
4. Create a git tag
After running this task, push with tags to trigger the release workflow:
git push origin main --tags
"""
use Mix.Task
@shortdoc "Bump version, update changelog, commit and tag"
@impl Mix.Task
def run(args) do
{opts, args} = parse_args(args)
bump_type = parse_bump_type(args)
current_version = read_version()
new_version = bump_version(current_version, bump_type)
changelog_section = build_changelog_section(new_version)
if opts[:dry_run] do
Mix.shell().info("Dry run - changelog preview for #{current_version} -> #{new_version}:\n")
Mix.shell().info(changelog_section)
else
Mix.shell().info("Bumping version: #{current_version} -> #{new_version}")
write_version(new_version)
update_changelog(changelog_section)
Mix.shell().info("Committing changes...")
System.cmd("git", ["add", "VERSION", "CHANGELOG.md"])
System.cmd("git", ["commit", "-m", "Release v#{new_version}"])
Mix.shell().info("Creating tag v#{new_version}...")
System.cmd("git", ["tag", "v#{new_version}"])
Mix.shell().info("""
Release v#{new_version} prepared!
Next steps:
git push origin main --tags
""")
end
end
defp parse_args(args) do
{opts, rest, _} = OptionParser.parse(args, switches: [dry_run: :boolean])
{opts, rest}
end
defp parse_bump_type([]), do: :patch
defp parse_bump_type(["patch"]), do: :patch
defp parse_bump_type(["minor"]), do: :minor
defp parse_bump_type(["major"]), do: :major
defp parse_bump_type(_) do
Mix.raise("Usage: mix bump [patch|minor|major] [--dry-run]")
end
defp read_version do
"VERSION"
|> File.read!()
|> String.trim()
end
defp write_version(version) do
File.write!("VERSION", version <> "\n")
end
defp bump_version(version, bump_type) do
[major, minor, patch] =
version
|> String.split(".")
|> Enum.map(&String.to_integer/1)
case bump_type do
:patch -> "#{major}.#{minor}.#{patch + 1}"
:minor -> "#{major}.#{minor + 1}.0"
:major -> "#{major + 1}.0.0"
end
end
defp build_changelog_section(new_version) do
today = Date.utc_today() |> Date.to_iso8601()
{prs, direct_commits} = get_changes_since_last_tag()
entries =
case {prs, direct_commits} do
{[], []} ->
"- No changes\n"
_ ->
pr_entries = Enum.map(prs, fn {number, title} -> "- #{title} (##{number})" end)
commit_entries =
Enum.map(direct_commits, fn {sha, message} -> "- #{message} (#{sha})" end)
Enum.join(pr_entries ++ commit_entries, "\n") <> "\n"
end
"""
## [v#{new_version}] - #{today}
#{entries}
"""
end
defp update_changelog(new_section) do
changelog = File.read!("CHANGELOG.md")
updated =
case String.split(changelog, ~r/^## \[v/m, parts: 2) do
[header, rest] ->
header <> new_section <> "## [v" <> rest
[_only_header] ->
changelog <> "\n" <> new_section
end
File.write!("CHANGELOG.md", updated)
end
defp get_changes_since_last_tag do
range =
case System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true) do
{tag, 0} -> "#{String.trim(tag)}..HEAD"
_ -> "HEAD"
end
# Get first-parent commits (commits directly on main)
{output, 0} = System.cmd("git", ["log", range, "--first-parent", "--format=%H %s"])
commits = String.split(output, "\n", trim: true)
{merge_commits, direct_commits} =
Enum.split_with(commits, &String.contains?(&1, "Merge pull request"))
prs =
merge_commits
|> Enum.map(&parse_pr_number/1)
|> Enum.reject(&is_nil/1)
|> Enum.map(&fetch_pr_title/1)
direct =
direct_commits
|> Enum.map(&parse_direct_commit/1)
|> Enum.reject(&is_nil/1)
{prs, direct}
end
defp parse_pr_number(line) do
case Regex.run(~r/Merge pull request #(\d+)/, line) do
[_, number] -> number
_ -> nil
end
end
defp parse_direct_commit(line) do
case String.split(line, " ", parts: 2) do
[sha, message] when byte_size(sha) == 40 ->
short_sha = String.slice(sha, 0, 7)
{short_sha, message}
_ ->
nil
end
end
defp fetch_pr_title(number) do
{title, 0} = System.cmd("gh", ["pr", "view", number, "--json", "title", "--jq", ".title"])
{number, String.trim(title)}
end
end