Skip to main content

src/version_bump@plugins@exec.erl

-module(version_bump@plugins@exec).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/version_bump/plugins/exec.gleam").
-export([parse_release_type/1, 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 `exec` plugin — semantic-release's escape hatch.\n"
    "\n"
    " Instead of implementing a hook in Gleam, the user supplies a shell command\n"
    " for any lifecycle step via `PluginSpec.options`. Each option key maps to one\n"
    " hook; when present, that hook runs the command through `sh -c` in the\n"
    " project's `cwd`:\n"
    "\n"
    "   - `verifyConditionsCmd` -> verify_conditions\n"
    "   - `analyzeCommitsCmd`   -> analyze_commits\n"
    "   - `verifyReleaseCmd`    -> verify_release\n"
    "   - `generateNotesCmd`    -> generate_notes\n"
    "   - `prepareCmd`          -> prepare\n"
    "   - `publishCmd`          -> publish\n"
    "   - `successCmd`          -> success\n"
    "   - `failCmd`             -> fail\n"
    "\n"
    " Hook semantics:\n"
    "   - analyze_commits: the trimmed stdout is parsed into a `ReleaseType`\n"
    "     (\"major\"/\"minor\"/\"patch\" -> `Some(..)`, empty / anything else -> `None`).\n"
    "   - generate_notes:  the trimmed stdout becomes the release notes.\n"
    "   - publish:         currently signals \"not handled\" (`None`) on success,\n"
    "     since the command produces no structured `Release`.\n"
    "   - everything else: a non-zero exit aborts the pipeline with a `PluginError`.\n"
).

-file("src/version_bump/plugins/exec.gleam", 198).
?DOC(
    " Execute `cmd` through `sh -c` in `cwd`, returning captured stdout (with\n"
    " stderr folded in by shellout's default) or a `PluginError` carrying the exit\n"
    " code and output on failure.\n"
).
-spec run(binary(), binary()) -> {ok, binary()} |
    {error, version_bump@error:release_error()}.
run(Cmd, Cwd) ->
    _pipe = shellout:command(<<"sh"/utf8>>, [<<"-c"/utf8>>, Cmd], Cwd, []),
    gleam@result:map_error(
        _pipe,
        fun(Failure) ->
            {Code, Message} = Failure,
            {plugin_error,
                <<"exec"/utf8>>,
                <<<<<<<<<<"command `"/utf8, Cmd/binary>>/binary,
                                "` failed (exit "/utf8>>/binary,
                            (erlang:integer_to_binary(Code))/binary>>/binary,
                        "): "/utf8>>/binary,
                    (gleam@string:trim(Message))/binary>>}
        end
    ).

-file("src/version_bump/plugins/exec.gleam", 170).
?DOC(" `Some(value)` when `key` is present in the options dict, else `None`.\n").
-spec get_option(gleam@dict:dict(binary(), binary()), binary()) -> gleam@option:option(binary()).
get_option(Options, Key) ->
    case gleam_stdlib:map_get(Options, Key) of
        {ok, Value} ->
            {some, Value};

        {error, _} ->
            none
    end.

-file("src/version_bump/plugins/exec.gleam", 158).
?DOC(
    " Look up the command string for an option key, treating a present-but-blank\n"
    " value the same as an absent key (`None`).\n"
).
-spec command_for(version_bump@config:plugin_spec(), binary()) -> gleam@option:option(binary()).
command_for(Spec, Key) ->
    case get_option(erlang:element(3, Spec), Key) of
        {some, Cmd} ->
            case gleam@string:trim(Cmd) of
                <<""/utf8>> ->
                    none;

                _ ->
                    {some, Cmd}
            end;

        none ->
            none
    end.

-file("src/version_bump/plugins/exec.gleam", 181).
?DOC(
    " Run the command for the given option key only for its exit status; a\n"
    " non-zero exit becomes a `PluginError`. A missing key is a successful no-op.\n"
).
-spec run_for_effect(
    version_bump@config:plugin_spec(),
    version_bump@context:context(),
    binary()
) -> {ok, nil} | {error, version_bump@error:release_error()}.
run_for_effect(Spec, Context, Key) ->
    case command_for(Spec, Key) of
        none ->
            {ok, nil};

        {some, Cmd} ->
            gleam@result:map(
                run(Cmd, erlang:element(2, Context)),
                fun(_) -> nil end
            )
    end.

-file("src/version_bump/plugins/exec.gleam", 135).
?DOC(" Run `failCmd` for effect; absent key is a no-op.\n").
-spec fail(version_bump@config:plugin_spec(), version_bump@context:context()) -> {ok,
        nil} |
    {error, version_bump@error:release_error()}.
fail(Spec, Context) ->
    run_for_effect(Spec, Context, <<"failCmd"/utf8>>).

-file("src/version_bump/plugins/exec.gleam", 130).
?DOC(" Run `successCmd` for effect; absent key is a no-op.\n").
-spec success(version_bump@config:plugin_spec(), version_bump@context:context()) -> {ok,
        nil} |
    {error, version_bump@error:release_error()}.
success(Spec, Context) ->
    run_for_effect(Spec, Context, <<"successCmd"/utf8>>).

-file("src/version_bump/plugins/exec.gleam", 116).
?DOC(
    " Run `publishCmd` for effect. The command yields no structured `Release`, so\n"
    " a successful run reports \"not handled\" (`None`); the engine still treats the\n"
    " step as having run.\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(Spec, Context) ->
    version_bump_task_ffi:resolve(
        case command_for(Spec, <<"publishCmd"/utf8>>) of
            none ->
                {ok, none};

            {some, Cmd} ->
                gleam@result:map(
                    run(Cmd, erlang:element(2, Context)),
                    fun(_) -> none end
                )
        end
    ).

-file("src/version_bump/plugins/exec.gleam", 109).
?DOC(" Run `prepareCmd` for effect; absent key is a no-op.\n").
-spec prepare(version_bump@config:plugin_spec(), version_bump@context:context()) -> {ok,
        nil} |
    {error, version_bump@error:release_error()}.
prepare(Spec, Context) ->
    run_for_effect(Spec, Context, <<"prepareCmd"/utf8>>).

-file("src/version_bump/plugins/exec.gleam", 95).
?DOC(
    " Run `generateNotesCmd`; its trimmed stdout is the notes. With no command the\n"
    " plugin contributes no notes (the empty string).\n"
).
-spec generate_notes(
    version_bump@config:plugin_spec(),
    version_bump@context:context()
) -> {ok, binary()} | {error, version_bump@error:release_error()}.
generate_notes(Spec, Context) ->
    case command_for(Spec, <<"generateNotesCmd"/utf8>>) of
        none ->
            {ok, <<""/utf8>>};

        {some, Cmd} ->
            gleam@result:map(
                run(Cmd, erlang:element(2, Context)),
                fun(Stdout) -> gleam@string:trim(Stdout) end
            )
    end.

-file("src/version_bump/plugins/exec.gleam", 86).
?DOC(" Run `verifyReleaseCmd` for effect; absent key is a no-op.\n").
-spec verify_release(
    version_bump@config:plugin_spec(),
    version_bump@context:context()
) -> {ok, nil} | {error, version_bump@error:release_error()}.
verify_release(Spec, Context) ->
    run_for_effect(Spec, Context, <<"verifyReleaseCmd"/utf8>>).

-file("src/version_bump/plugins/exec.gleam", 147).
?DOC(
    " Parse the trimmed stdout of an `analyzeCommitsCmd` into a `ReleaseType`.\n"
    "\n"
    " PURE. Matches semantic-release/exec semantics: the command prints the bump\n"
    " type on stdout. `\"major\"`/`\"minor\"`/`\"patch\"` (case-insensitive, surrounding\n"
    " whitespace ignored) map to `Some(..)`; empty output or any other value means\n"
    " \"no release\" (`None`).\n"
).
-spec parse_release_type(binary()) -> gleam@option:option(version_bump@semver:release_type()).
parse_release_type(Stdout) ->
    case string:lowercase(gleam@string:trim(Stdout)) of
        <<"major"/utf8>> ->
            {some, major};

        <<"minor"/utf8>> ->
            {some, minor};

        <<"patch"/utf8>> ->
            {some, patch};

        _ ->
            none
    end.

-file("src/version_bump/plugins/exec.gleam", 72).
?DOC(
    " Run `analyzeCommitsCmd` and parse its stdout into a `ReleaseType`. With no\n"
    " command configured the plugin contributes no opinion (`None`).\n"
).
-spec analyze_commits(
    version_bump@config:plugin_spec(),
    version_bump@context:context()
) -> {ok, gleam@option:option(version_bump@semver:release_type())} |
    {error, version_bump@error:release_error()}.
analyze_commits(Spec, Context) ->
    case command_for(Spec, <<"analyzeCommitsCmd"/utf8>>) of
        none ->
            {ok, none};

        {some, Cmd} ->
            gleam@result:map(
                run(Cmd, erlang:element(2, Context)),
                fun(Stdout) -> parse_release_type(Stdout) end
            )
    end.

-file("src/version_bump/plugins/exec.gleam", 63).
?DOC(" Run `verifyConditionsCmd` for effect; absent key is a no-op.\n").
-spec verify_conditions(
    version_bump@config:plugin_spec(),
    version_bump@context:context()
) -> {ok, nil} | {error, version_bump@error:release_error()}.
verify_conditions(Spec, Context) ->
    run_for_effect(Spec, Context, <<"verifyConditionsCmd"/utf8>>).

-file("src/version_bump/plugins/exec.gleam", 46).
?DOC(
    " Build the `exec` plugin, wiring every hook to the command runner. Each hook\n"
    " looks up its corresponding option key at call time; if the key is absent the\n"
    " hook is a no-op (it returns the neutral value for that step), so a single\n"
    " plugin record can serve any subset of configured commands.\n"
).
-spec plugin() -> version_bump@plugin:plugin().
plugin() ->
    _record = version_bump@plugin:new(<<"exec"/utf8>>),
    {plugin,
        erlang:element(2, _record),
        {some, fun verify_conditions/2},
        {some, fun analyze_commits/2},
        {some, fun verify_release/2},
        {some, fun generate_notes/2},
        erlang:element(7, _record),
        {some, fun prepare/2},
        {some, fun publish/2},
        {some, fun success/2},
        {some, fun fail/2}}.