Current section
Files
Jump to
Current section
Files
lib/mix/tasks/verify.phase11.ex
defmodule Mix.Tasks.Verify.Phase11 do
use Mix.Task
@shortdoc "Runs the release-alignment gate (package, consumer, docs, Release Please wiring)"
@moduledoc """
Runs the automated release-alignment verification flow.
This task keeps the existing auth-free release gate shape and extends it with
checks that the package version, packaged consumer path, Release Please
manifest, and publish workflow still describe the same tagged release path.
"""
@impl true
def run(args) do
Mix.Task.run("app.start")
ensure_no_args!(args)
run_test!(
[
"test/release/package_metadata_test.exs",
"test/release/consumer_smoke_test.exs",
"test/scrypath/docs_contract_test.exs"
],
"Release contract tests"
)
Mix.shell().info("==> Building docs with warnings as errors")
Mix.Task.reenable("docs")
Mix.Task.run("docs", ["--warnings-as-errors"])
validate_release_contract!()
run_command!(["hex.build", "--unpack"], "Building and unpacking Hex package")
end
defp run_test!(args, label) do
Mix.shell().info("==> Running #{label}")
Mix.Task.reenable("test")
Mix.Task.run("test", args)
end
defp validate_release_contract! do
Mix.shell().info("==> Validating release workflow contract")
run_system_command!(
"grep",
["-nF", "config-file: release-please-config.json", ".github/workflows/release-please.yml"],
"release workflow config-file validation"
)
run_system_command!(
"grep",
[
"-nF",
"manifest-file: .release-please-manifest.json",
".github/workflows/release-please.yml"
],
"release workflow manifest-file validation"
)
run_system_command!(
"grep",
[
"-nF",
"if: ${{ needs.release-please.outputs.release_created == 'true' }}",
".github/workflows/release-please.yml"
],
"publish job release_created guard validation"
)
run_system_command!(
"grep",
[
"-nF",
"ref: ${{ needs.release-please.outputs.tag_name }}",
".github/workflows/release-please.yml"
],
"publish job tag checkout validation"
)
run_system_command!(
"grep",
[
"-nF",
"run: grep -n \"@version \\\"${{ needs.release-please.outputs.version }}\\\"\" mix.exs",
".github/workflows/release-please.yml"
],
"publish job version validation"
)
run_system_command!(
"grep",
["-nF", "run: mix verify.phase11", ".github/workflows/release-please.yml"],
"publish job release gate validation"
)
run_system_command!(
"grep",
["-nF", "run: mix hex.publish --dry-run --yes", ".github/workflows/release-please.yml"],
"publish job dry-run validation"
)
run_system_command!(
"grep",
["-nF", "run: mix hex.publish --yes", ".github/workflows/release-please.yml"],
"publish job hex publish validation"
)
run_system_command!(
"grep",
[
"-nF",
"run: mix verify.release_publish \"${{ needs.release-please.outputs.version }}\"",
".github/workflows/release-please.yml"
],
"publish job post-publish verification validation"
)
release_sources = release_sources!()
case validate_release_agreement(release_sources) do
:ok ->
:ok
{:error, messages} ->
Mix.raise("release agreement check failed:\n- " <> Enum.join(messages, "\n- "))
end
end
def validate_release_agreement(%{
mix_version: mix_version,
source_ref: source_ref,
manifest_version: manifest_version,
release_type: release_type,
include_v_in_tag: include_v_in_tag,
top_changelog_version: top_changelog_version
}) do
[]
|> maybe_add_mismatch(
mix_version != manifest_version,
"mix.exs version=#{mix_version} does not match .release-please-manifest.json version=#{manifest_version}"
)
|> maybe_add_mismatch(
source_ref != "v#{mix_version}",
"mix.exs source_ref=#{source_ref} does not match expected v#{mix_version}"
)
|> maybe_add_mismatch(
release_type != "elixir",
"release-please-config.json release-type=#{inspect(release_type)} is not \"elixir\""
)
|> maybe_add_mismatch(
include_v_in_tag != true,
"release-please-config.json packages[\".\"].include-v-in-tag=#{inspect(include_v_in_tag)} is not true"
)
|> maybe_add_mismatch(
top_changelog_version != mix_version,
"CHANGELOG.md top release heading version=#{inspect(top_changelog_version)} does not match mix.exs version=#{mix_version}"
)
|> case do
[] -> :ok
messages -> {:error, messages}
end
end
defp maybe_add_mismatch(messages, true, message), do: [message | messages]
defp maybe_add_mismatch(messages, false, _message), do: messages
defp release_sources! do
# Mix.Project.config/0 (not Scrypath.MixProject.project/0) so Dialyzer can resolve the
# call — mix.exs is not part of Dialyzer's analyzed beam set.
project = Mix.Project.config()
manifest = File.read!(".release-please-manifest.json") |> Jason.decode!()
release_config = File.read!("release-please-config.json") |> Jason.decode!()
%{
mix_version: project[:version],
source_ref: project[:source_ref],
manifest_version: manifest["."],
release_type: release_config["release-type"],
include_v_in_tag: get_in(release_config, ["packages", ".", "include-v-in-tag"]),
top_changelog_version: top_changelog_version!()
}
end
defp top_changelog_version! do
changelog = File.read!("CHANGELOG.md")
case Regex.run(~r/^## \[(\d+\.\d+\.\d+)\]/m, changelog, capture: :all_but_first) do
[version] -> version
_ -> nil
end
end
defp run_command!(args, label) do
Mix.shell().info("==> #{label}")
{output, exit_status} =
System.cmd("mix", args, stderr_to_stdout: true)
Mix.shell().info(output)
if exit_status != 0 do
Mix.raise("#{label} failed")
end
end
defp run_system_command!(command, args, label) do
{output, exit_status} =
System.cmd(command, args, stderr_to_stdout: true)
Mix.shell().info(output)
if exit_status != 0 do
Mix.raise("#{label} failed")
end
end
defp ensure_no_args!([]), do: :ok
defp ensure_no_args!(args) do
Mix.raise("verify.phase11 does not accept arguments, got: #{Enum.join(args, " ")}")
end
end