-module(version_bump@plugins@hex).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/version_bump/plugins/hex.gleam").
-export([package_name/1, published_ok/1, set_version/2, plugin/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" The Hex / Gleam publish plugin (plugin name \"hex\").\n"
"\n"
" The Gleam analogue of `@semantic-release/npm`. It publishes the package to\n"
" the Hex package repository with `gleam publish`. Two things differ from npm:\n"
" the version lives in `gleam.toml` (not `package.json`), and Hex has no\n"
" dist-tag / channel concept, so `add_channel` is intentionally NOT implemented\n"
" (a prerelease is published as an ordinary semver prerelease and Hex surfaces\n"
" it as such).\n"
"\n"
" - verify_conditions: a `gleam.toml` exists in `context.cwd` carrying the\n"
" `description` and `licences` fields that `gleam publish` requires, and a\n"
" `HEXPM_API_KEY` is available (skipped on a dry run, since a dry run never\n"
" publishes — matching the npm/github plugins here).\n"
" - prepare: rewrite the top-level `version` field of `gleam.toml`.\n"
" - publish: run `gleam publish --yes` and report the hex.pm release URL.\n"
"\n"
" `set_version` and `package_name` are PURE string functions, exported so the\n"
" version rewrite and URL construction can be unit-tested without any IO.\n"
).
-file("src/version_bump/plugins/hex.gleam", 288).
?DOC(" Compile a multi-line regexp so `^` anchors to each line start.\n").
-spec compile_ml(binary()) -> {ok, gleam@regexp:regexp()} | {error, binary()}.
compile_ml(Pattern) ->
_pipe = gleam@regexp:compile(Pattern, {options, false, true}),
gleam@result:map_error(
_pipe,
fun(Err) ->
<<"invalid regexp: "/utf8, (erlang:element(2, Err))/binary>>
end
).
-file("src/version_bump/plugins/hex.gleam", 273).
?DOC(" Read the package `name` from a `gleam.toml` document. PURE.\n").
-spec package_name(binary()) -> {ok, binary()} |
{error, version_bump@error:release_error()}.
package_name(Gleam_toml) ->
case compile_ml(<<"^[ \t]*name[ \t]*=[ \t]*\"([^\"]*)\""/utf8>>) of
{error, Message} ->
{error, {plugin_error, <<"hex"/utf8>>, Message}};
{ok, Re} ->
case gleam@regexp:scan(Re, Gleam_toml) of
[{match, _, [{some, Name}]} | _] ->
{ok, Name};
_ ->
{error,
{plugin_error,
<<"hex"/utf8>>,
<<"no `name` field found in gleam.toml"/utf8>>}}
end
end.
-file("src/version_bump/plugins/hex.gleam", 309).
?DOC(" The path to `gleam.toml` within the working directory.\n").
-spec gleam_toml_path(binary()) -> binary().
gleam_toml_path(Cwd) ->
case gleam_stdlib:string_ends_with(Cwd, <<"/"/utf8>>) of
true ->
<<Cwd/binary, "gleam.toml"/utf8>>;
false ->
<<Cwd/binary, "/gleam.toml"/utf8>>
end.
-file("src/version_bump/plugins/hex.gleam", 317).
?DOC(" Read a file, mapping any failure to a `PluginError`.\n").
-spec read_file(binary()) -> {ok, binary()} |
{error, version_bump@error:release_error()}.
read_file(Path) ->
_pipe = simplifile:read(Path),
gleam@result:map_error(
_pipe,
fun(Err) ->
{plugin_error,
<<"hex"/utf8>>,
<<<<<<"could not read "/utf8, Path/binary>>/binary, ": "/utf8>>/binary,
(simplifile:describe_error(Err))/binary>>}
end
).
-file("src/version_bump/plugins/hex.gleam", 236).
?DOC(
" Best-effort hex.pm URL for the published release; `None` if the package name\n"
" cannot be read (publishing already succeeded, so this is not fatal).\n"
).
-spec release_url(binary(), binary()) -> gleam@option:option(binary()).
release_url(Cwd, Version) ->
case read_file(gleam_toml_path(Cwd)) of
{ok, Contents} ->
case package_name(Contents) of
{ok, Name} ->
{some,
<<<<<<"https://hex.pm/packages/"/utf8, Name/binary>>/binary,
"/"/utf8>>/binary,
Version/binary>>};
{error, _} ->
none
end;
{error, _} ->
none
end.
-file("src/version_bump/plugins/hex.gleam", 230).
?DOC(
" True when `gleam publish` output confirms a successful publish. gleam prints\n"
" \"Published package and documentation\" on success; the lowercase \"published\"\n"
" in its <1.0.0 warning does not contain this marker, so an aborted publish is\n"
" correctly treated as a failure. Exposed for testing.\n"
).
-spec published_ok(binary()) -> boolean().
published_ok(Output) ->
gleam_stdlib:contains_string(Output, <<"Published package"/utf8>>).
-file("src/version_bump/plugins/hex.gleam", 188).
?DOC(
" Run `gleam publish` and confirm from its output that the package was actually\n"
" published — never trusting the exit code alone.\n"
).
-spec run_gleam_publish(binary()) -> {ok, nil} |
{error, version_bump@error:release_error()}.
run_gleam_publish(Cwd) ->
Publish = <<"echo 'I am not using semantic versioning' | gleam publish --yes"/utf8>>,
case shellout:command(<<"sh"/utf8>>, [<<"-c"/utf8>>, Publish], Cwd, []) of
{ok, Output} ->
case published_ok(Output) of
true ->
{ok, nil};
false ->
{error,
{plugin_error,
<<"hex"/utf8>>,
<<<<<<"`gleam publish` exited 0 but did not report a successful publish "/utf8,
"(no \"Published package\" in its output) — it likely aborted a "/utf8>>/binary,
"prompt. Output:\n"/utf8>>/binary,
(gleam@string:trim(Output))/binary>>}}
end;
{error, {Code, Message}} ->
{error,
{plugin_error,
<<"hex"/utf8>>,
<<<<<<"`gleam publish` failed (exit "/utf8,
(erlang:integer_to_binary(Code))/binary>>/binary,
"): "/utf8>>/binary,
(gleam@string:trim(Message))/binary>>}}
end.
-file("src/version_bump/plugins/hex.gleam", 340).
?DOC(
" Extract the next release from the context, failing when the pipeline has not\n"
" determined one (handled rather than asserted).\n"
).
-spec require_next_release(version_bump@context:context()) -> {ok,
version_bump@release:next_release()} |
{error, version_bump@error:release_error()}.
require_next_release(Context) ->
case erlang:element(9, Context) of
{some, Next} ->
{ok, Next};
none ->
{error,
{plugin_error,
<<"hex"/utf8>>,
<<"no next release determined"/utf8>>}}
end.
-file("src/version_bump/plugins/hex.gleam", 166).
?DOC(
" Run `gleam publish --yes` in `context.cwd` and report the resulting release. The\n"
" `HEXPM_API_KEY` in the environment is inherited by the subprocess.\n"
).
-spec publish(version_bump@config:plugin_spec(), version_bump@context:context()) -> version_bump@task:task({ok,
gleam@option:option(version_bump@release:release())} |
{error, version_bump@error:release_error()}).
publish(_, Context) ->
version_bump_task_ffi:resolve(
begin
gleam@result:'try'(
require_next_release(Context),
fun(Next) ->
gleam@result:'try'(
run_gleam_publish(erlang:element(2, Context)),
fun(_) ->
{ok,
{some,
{release,
<<"hex"/utf8>>,
release_url(
erlang:element(2, Context),
erlang:element(2, Next)
),
erlang:element(2, Next),
erlang:element(4, Next),
erlang:element(6, Next),
<<"hex"/utf8>>}}}
end
)
end
)
end
).
-file("src/version_bump/plugins/hex.gleam", 328).
?DOC(" Write a file, mapping any failure to a `PluginError`.\n").
-spec write_file(binary(), binary()) -> {ok, nil} |
{error, version_bump@error:release_error()}.
write_file(Path, Contents) ->
_pipe = simplifile:write(Path, Contents),
gleam@result:map_error(
_pipe,
fun(Err) ->
{plugin_error,
<<"hex"/utf8>>,
<<<<<<"could not write "/utf8, Path/binary>>/binary, ": "/utf8>>/binary,
(simplifile:describe_error(Err))/binary>>}
end
).
-file("src/version_bump/plugins/hex.gleam", 297).
?DOC(" Replace only the first occurrence of `needle` in `haystack`.\n").
-spec replace_first(binary(), binary(), binary()) -> binary().
replace_first(Haystack, Needle, Replacement) ->
case gleam@string:split_once(Haystack, Needle) of
{ok, {Before, After}} ->
<<<<Before/binary, Replacement/binary>>/binary, After/binary>>;
{error, _} ->
Haystack
end.
-file("src/version_bump/plugins/hex.gleam", 251).
?DOC(
" Replace the top-level `version = \"...\"` value in a `gleam.toml` document with\n"
" `version`, leaving the rest of the file intact. PURE.\n"
).
-spec set_version(binary(), binary()) -> {ok, binary()} |
{error, version_bump@error:release_error()}.
set_version(Gleam_toml, Version) ->
case compile_ml(<<"(^[ \t]*version[ \t]*=[ \t]*\")([^\"]*)\""/utf8>>) of
{error, Message} ->
{error, {plugin_error, <<"hex"/utf8>>, Message}};
{ok, Re} ->
case gleam@regexp:scan(Re, Gleam_toml) of
[{match, Matched, [{some, Prefix}, _]} | _] ->
Replacement = <<<<Prefix/binary, Version/binary>>/binary,
"\""/utf8>>,
{ok, replace_first(Gleam_toml, Matched, Replacement)};
_ ->
{error,
{plugin_error,
<<"hex"/utf8>>,
<<"no top-level `version` field found in gleam.toml"/utf8>>}}
end
end.
-file("src/version_bump/plugins/hex.gleam", 154).
?DOC(" Rewrite `gleam.toml`'s `version` to the next release version, in place.\n").
-spec prepare(version_bump@config:plugin_spec(), version_bump@context:context()) -> {ok,
nil} |
{error, version_bump@error:release_error()}.
prepare(_, Context) ->
gleam@result:'try'(
require_next_release(Context),
fun(Next) ->
Path = gleam_toml_path(erlang:element(2, Context)),
gleam@result:'try'(
read_file(Path),
fun(Contents) ->
gleam@result:'try'(
set_version(Contents, erlang:element(2, Next)),
fun(Updated) -> write_file(Path, Updated) end
)
end
)
end
).
-file("src/version_bump/plugins/hex.gleam", 128).
?DOC(
" Resolve the Hex API key from `context.env`, falling back to the live process\n"
" environment. Empty/whitespace-only values are treated as absent.\n"
).
-spec api_key(version_bump@context:context()) -> gleam@option:option(binary()).
api_key(Context) ->
From_ctx = case gleam_stdlib:map_get(
erlang:element(3, Context),
<<"HEXPM_API_KEY"/utf8>>
) of
{ok, Value} ->
{some, Value};
{error, _} ->
none
end,
Key = case From_ctx of
{some, Value@1} ->
{some, Value@1};
none ->
case envoy_ffi:get(<<"HEXPM_API_KEY"/utf8>>) of
{ok, Value@2} ->
{some, Value@2};
{error, _} ->
none
end
end,
case Key of
{some, Value@3} ->
case gleam@string:trim(Value@3) of
<<""/utf8>> ->
none;
Trimmed ->
{some, Trimmed}
end;
none ->
none
end.
-file("src/version_bump/plugins/hex.gleam", 114).
?DOC(
" Verify that a `HEXPM_API_KEY` is available, checking the context environment\n"
" first and falling back to the live process environment.\n"
).
-spec ensure_api_key(version_bump@context:context()) -> {ok, nil} |
{error, version_bump@error:release_error()}.
ensure_api_key(Context) ->
case api_key(Context) of
{some, _} ->
{ok, nil};
none ->
{error,
{plugin_error,
<<"hex"/utf8>>,
<<"HEXPM_API_KEY is not set (run `gleam hex authenticate` to create a key, "/utf8,
"then expose it as HEXPM_API_KEY in CI)"/utf8>>}}
end.
-file("src/version_bump/plugins/hex.gleam", 101).
?DOC(
" True when `field` appears as an uncommented top-level key. A leading `#`\n"
" (comment) prevents a match, so the commented placeholders in a freshly\n"
" scaffolded `gleam.toml` are correctly treated as absent.\n"
).
-spec field_present(binary(), binary()) -> boolean().
field_present(Contents, Field) ->
case compile_ml(<<<<"^[ \t]*"/utf8, Field/binary>>/binary, "[ \t]*="/utf8>>) of
{ok, Re} ->
case gleam@regexp:scan(Re, Contents) of
[] ->
false;
_ ->
true
end;
{error, _} ->
false
end.
-file("src/version_bump/plugins/hex.gleam", 85).
-spec require_field(binary(), binary()) -> {ok, nil} |
{error, version_bump@error:release_error()}.
require_field(Contents, Field) ->
case field_present(Contents, Field) of
true ->
{ok, nil};
false ->
{error,
{plugin_error,
<<"hex"/utf8>>,
<<<<"gleam.toml is missing the `"/utf8, Field/binary>>/binary,
"` field, which `gleam publish` requires to publish to Hex"/utf8>>}}
end.
-file("src/version_bump/plugins/hex.gleam", 80).
?DOC(
" `gleam publish` refuses to publish a package without `description` and\n"
" `licences`, so verify they are present (and uncommented) up front rather than\n"
" failing after the version has already been bumped and tagged.\n"
).
-spec ensure_publishable_metadata(binary()) -> {ok, nil} |
{error, version_bump@error:release_error()}.
ensure_publishable_metadata(Contents) ->
gleam@result:'try'(
require_field(Contents, <<"description"/utf8>>),
fun(_) -> require_field(Contents, <<"licences"/utf8>>) end
).
-file("src/version_bump/plugins/hex.gleam", 68).
?DOC(" Read `gleam.toml`, mapping a missing/unreadable file to a clear error.\n").
-spec read_gleam_toml(binary()) -> {ok, binary()} |
{error, version_bump@error:release_error()}.
read_gleam_toml(Cwd) ->
Path = gleam_toml_path(Cwd),
case simplifile:read(Path) of
{ok, Contents} ->
{ok, Contents};
{error, _} ->
{error,
{plugin_error,
<<"hex"/utf8>>,
<<"no gleam.toml found at "/utf8, Path/binary>>}}
end.
-file("src/version_bump/plugins/hex.gleam", 55).
?DOC(
" Ensure `gleam.toml` exists with the metadata `gleam publish` requires, and\n"
" that a Hex API key is available (except on a dry run).\n"
).
-spec verify_conditions(
version_bump@config:plugin_spec(),
version_bump@context:context()
) -> {ok, nil} | {error, version_bump@error:release_error()}.
verify_conditions(_, Context) ->
gleam@result:'try'(
read_gleam_toml(erlang:element(2, Context)),
fun(Contents) ->
gleam@result:'try'(
ensure_publishable_metadata(Contents),
fun(_) -> case erlang:element(12, Context) of
true ->
{ok, nil};
false ->
ensure_api_key(Context)
end end
)
end
).
-file("src/version_bump/plugins/hex.gleam", 42).
?DOC(
" Build the Hex plugin: implements `verify_conditions`, `prepare`, and\n"
" `publish`. There is deliberately no `add_channel` — Hex has no dist-tags.\n"
).
-spec plugin() -> version_bump@plugin:plugin().
plugin() ->
_record = version_bump@plugin:new(<<"hex"/utf8>>),
{plugin,
erlang:element(2, _record),
{some, fun verify_conditions/2},
erlang:element(4, _record),
erlang:element(5, _record),
erlang:element(6, _record),
erlang:element(7, _record),
{some, fun prepare/2},
{some, fun publish/2},
erlang:element(10, _record),
erlang:element(11, _record)}.