Current section
Files
Jump to
Current section
Files
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
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
bump_type = parse_args(args)
current_version = read_version()
new_version = bump_version(current_version, bump_type)
Mix.shell().info("Bumping version: #{current_version} -> #{new_version}")
write_version(new_version)
update_changelog(new_version)
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
defp parse_args([]), do: :patch
defp parse_args(["patch"]), do: :patch
defp parse_args(["minor"]), do: :minor
defp parse_args(["major"]), do: :major
defp parse_args(_) do
Mix.raise("Usage: mix bump [patch|minor|major]")
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 update_changelog(new_version) do
changelog = File.read!("CHANGELOG.md")
today = Date.utc_today() |> Date.to_iso8601()
prs = get_merged_prs_since_last_tag()
pr_list =
case prs do
[] -> "- No PRs merged\n"
prs -> Enum.map_join(prs, "\n", fn {number, title} -> "- #{title} (##{number})" end) <> "\n"
end
new_section = """
## [v#{new_version}] - #{today}
#{pr_list}
"""
# Insert after the header (first ## line marks the start of versions)
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_merged_prs_since_last_tag do
case System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true) do
{tag, 0} ->
last_tag = String.trim(tag)
{date, 0} = System.cmd("git", ["log", "-1", "--format=%cs", last_tag])
fetch_merged_prs(String.trim(date))
_ ->
# No tags yet, get all merged PRs
fetch_merged_prs(nil)
end
end
defp fetch_merged_prs(since_date) do
args =
["pr", "list", "--state", "merged", "--json", "number,title", "--jq", ".[] | \"\\(.number)\\t\\(.title)\""] ++
if(since_date, do: ["--search", "merged:>=#{since_date}"], else: [])
case System.cmd("gh", args) do
{output, 0} ->
output
|> String.trim()
|> String.split("\n", trim: true)
|> Enum.map(fn line ->
[number, title] = String.split(line, "\t", parts: 2)
{number, title}
end)
_ ->
[]
end
end
end