Skip to main content

src/version_bump@runner.erl

-module(version_bump@runner).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/version_bump/runner.gleam").
-export([run_verify_conditions/2, run_verify_release/2, run_prepare/2, run_success/2, run_fail/2, run_analyze_commits/2, run_generate_notes/2, run_publish/2]).

-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(
    " Hook runners — the glue that drives a list of resolved plugins through one\n"
    " lifecycle hook each, applying the per-hook combination semantics the plugin\n"
    " contract documents.\n"
    "\n"
    " A \"resolved plugin\" is a `#(PluginSpec, Plugin)` pair: the spec carries the\n"
    " user's options, the plugin carries the hook implementations. Each runner\n"
    " folds over the list, looks up the relevant `Option(hook)` on each plugin,\n"
    " skips plugins that don't implement it (`None`), and combines the results:\n"
    "\n"
    "   - verify_conditions / verify_release / prepare / success / fail\n"
    "       run every implementing plugin for effect; ALL errors are collected and,\n"
    "       if any occurred, surfaced together as an `AggregateError`.\n"
    "   - analyze_commits   highest `ReleaseType` (by `release_type_rank`) wins; a\n"
    "       single error short-circuits.\n"
    "   - generate_notes    notes are concatenated in plugin order; a single error\n"
    "       short-circuits.\n"
    "   - publish           every implementing plugin runs; the `Some(release)`\n"
    "       results are collected in order; a single error short-circuits.\n"
).

-file("src/version_bump/runner.gleam", 81).
?DOC(
    " Shared driver for the for-effect hooks. Runs every plugin that implements\n"
    " the selected hook, collecting every error rather than stopping at the first.\n"
    " Returns `Ok(Nil)` when nothing failed, otherwise an `AggregateError` of all\n"
    " collected failures (a single failure is still wrapped for a uniform shape).\n"
).
-spec run_effect_hook(
    list({version_bump@config:plugin_spec(), version_bump@plugin:plugin()}),
    version_bump@context:context(),
    fun((version_bump@plugin:plugin()) -> gleam@option:option(fun((version_bump@config:plugin_spec(), version_bump@context:context()) -> {ok,
            nil} |
        {error, version_bump@error:release_error()})))
) -> {ok, nil} | {error, version_bump@error:release_error()}.
run_effect_hook(Plugins, Context, Select) ->
    Errors = gleam@list:fold(
        Plugins,
        [],
        fun(Acc, Resolved) ->
            {Spec, Plugin} = Resolved,
            case Select(Plugin) of
                none ->
                    Acc;

                {some, Hook} ->
                    case Hook(Spec, Context) of
                        {ok, nil} ->
                            Acc;

                        {error, Err} ->
                            [Err | Acc]
                    end
            end
        end
    ),
    case lists:reverse(Errors) of
        [] ->
            {ok, nil};

        Collected ->
            {error, {aggregate_error, Collected}}
    end.

-file("src/version_bump/runner.gleam", 38).
?DOC(" Run `verify_conditions` across all plugins, aggregating any failures.\n").
-spec run_verify_conditions(
    list({version_bump@config:plugin_spec(), version_bump@plugin:plugin()}),
    version_bump@context:context()
) -> {ok, nil} | {error, version_bump@error:release_error()}.
run_verify_conditions(Plugins, Context) ->
    run_effect_hook(Plugins, Context, fun(P) -> erlang:element(3, P) end).

-file("src/version_bump/runner.gleam", 46).
?DOC(" Run `verify_release` across all plugins, aggregating any failures.\n").
-spec run_verify_release(
    list({version_bump@config:plugin_spec(), version_bump@plugin:plugin()}),
    version_bump@context:context()
) -> {ok, nil} | {error, version_bump@error:release_error()}.
run_verify_release(Plugins, Context) ->
    run_effect_hook(Plugins, Context, fun(P) -> erlang:element(5, P) end).

-file("src/version_bump/runner.gleam", 54).
?DOC(" Run `prepare` across all plugins, aggregating any failures.\n").
-spec run_prepare(
    list({version_bump@config:plugin_spec(), version_bump@plugin:plugin()}),
    version_bump@context:context()
) -> {ok, nil} | {error, version_bump@error:release_error()}.
run_prepare(Plugins, Context) ->
    run_effect_hook(Plugins, Context, fun(P) -> erlang:element(8, P) end).

-file("src/version_bump/runner.gleam", 62).
?DOC(" Run `success` across all plugins, aggregating any failures.\n").
-spec run_success(
    list({version_bump@config:plugin_spec(), version_bump@plugin:plugin()}),
    version_bump@context:context()
) -> {ok, nil} | {error, version_bump@error:release_error()}.
run_success(Plugins, Context) ->
    run_effect_hook(Plugins, Context, fun(P) -> erlang:element(10, P) end).

-file("src/version_bump/runner.gleam", 70).
?DOC(" Run `fail` across all plugins, aggregating any failures.\n").
-spec run_fail(
    list({version_bump@config:plugin_spec(), version_bump@plugin:plugin()}),
    version_bump@context:context()
) -> {ok, nil} | {error, version_bump@error:release_error()}.
run_fail(Plugins, Context) ->
    run_effect_hook(Plugins, Context, fun(P) -> erlang:element(11, P) end).

-file("src/version_bump/runner.gleam", 131).
?DOC(
    " Keep whichever of two optional release types ranks higher; `None` (no\n"
    " release) ranks below any concrete type.\n"
).
-spec keep_highest(
    gleam@option:option(version_bump@semver:release_type()),
    gleam@option:option(version_bump@semver:release_type())
) -> gleam@option:option(version_bump@semver:release_type()).
keep_highest(Current, Candidate) ->
    case {Current, Candidate} of
        {none, Other} ->
            Other;

        {Other@1, none} ->
            Other@1;

        {{some, A}, {some, B}} ->
            case version_bump@semver:release_type_rank(B) > version_bump@semver:release_type_rank(
                A
            ) of
                true ->
                    {some, B};

                false ->
                    {some, A}
            end
    end.

-file("src/version_bump/runner.gleam", 111).
?DOC(
    " Run `analyze_commits` across all plugins and return the highest implied\n"
    " `ReleaseType` (by `release_type_rank`), or `None` when no plugin warrants a\n"
    " release. The first error short-circuits.\n"
).
-spec run_analyze_commits(
    list({version_bump@config:plugin_spec(), version_bump@plugin:plugin()}),
    version_bump@context:context()
) -> {ok, gleam@option:option(version_bump@semver:release_type())} |
    {error, version_bump@error:release_error()}.
run_analyze_commits(Plugins, Context) ->
    gleam@list:try_fold(
        Plugins,
        none,
        fun(Acc, Resolved) ->
            {Spec, Plugin} = Resolved,
            case erlang:element(4, Plugin) of
                none ->
                    {ok, Acc};

                {some, Hook} ->
                    case Hook(Spec, Context) of
                        {ok, Candidate} ->
                            {ok, keep_highest(Acc, Candidate)};

                        {error, Err} ->
                            {error, Err}
                    end
            end
        end
    ).

-file("src/version_bump/runner.gleam", 175).
?DOC(" Join non-empty note sections with a blank line between them.\n").
-spec join_sections(list(binary())) -> binary().
join_sections(Sections) ->
    case Sections of
        [] ->
            <<""/utf8>>;

        [First | Rest] ->
            gleam@list:fold(
                Rest,
                First,
                fun(Acc, Section) ->
                    <<<<Acc/binary, "\n\n"/utf8>>/binary, Section/binary>>
                end
            )
    end.

-file("src/version_bump/runner.gleam", 228).
?DOC(
    " `result.map` written as a `use`-friendly continuation so the post-processing\n"
    " of a `try_fold` result reads top-to-bottom without an extra import alias.\n"
).
-spec result_map({ok, LYC} | {error, LYD}, fun((LYC) -> LYG)) -> {ok, LYG} |
    {error, LYD}.
result_map(Result, Transform) ->
    case Result of
        {ok, Value} ->
            {ok, Transform(Value)};

        {error, Err} ->
            {error, Err}
    end.

-file("src/version_bump/runner.gleam", 151).
?DOC(
    " Run `generate_notes` across all plugins, concatenating each plugin's notes\n"
    " in plugin order. Empty contributions add nothing; non-empty ones are joined\n"
    " with a blank line between sections. The first error short-circuits.\n"
).
-spec run_generate_notes(
    list({version_bump@config:plugin_spec(), version_bump@plugin:plugin()}),
    version_bump@context:context()
) -> {ok, binary()} | {error, version_bump@error:release_error()}.
run_generate_notes(Plugins, Context) ->
    result_map(
        gleam@list:try_fold(
            Plugins,
            [],
            fun(Acc, Resolved) ->
                {Spec, Plugin} = Resolved,
                case erlang:element(6, Plugin) of
                    none ->
                        {ok, Acc};

                    {some, Hook} ->
                        case Hook(Spec, Context) of
                            {ok, Notes} ->
                                {ok, [Notes | Acc]};

                            {error, Err} ->
                                {error, Err}
                        end
                end
            end
        ),
        fun(Sections) -> _pipe = Sections,
            _pipe@1 = lists:reverse(_pipe),
            _pipe@2 = gleam@list:filter(
                _pipe@1,
                fun(Section) -> Section /= <<""/utf8>> end
            ),
            join_sections(_pipe@2) end
    ).

-file("src/version_bump/runner.gleam", 192).
?DOC(
    " Run `publish` across all plugins, collecting the `Some(release)` results in\n"
    " plugin order. Plugins returning `None` (not handled) contribute nothing. The\n"
    " first error short-circuits.\n"
    "\n"
    " `publish` is asynchronous, so the plugins are chained sequentially through a\n"
    " `Task`: each plugin's publish runs after the previous one resolves, and the\n"
    " whole sequence yields a single `Task` of the collected releases.\n"
).
-spec run_publish(
    list({version_bump@config:plugin_spec(), version_bump@plugin:plugin()}),
    version_bump@context:context()
) -> version_bump@task:task({ok, list(version_bump@release:release())} |
    {error, version_bump@error:release_error()}).
run_publish(Plugins, Context) ->
    Accumulated = gleam@list:fold(
        Plugins,
        version_bump_task_ffi:resolve({ok, []}),
        fun(Acc_task, Resolved) ->
            {Spec, Plugin} = Resolved,
            version_bump_task_ffi:await(Acc_task, fun(Acc) -> case Acc of
                        {error, Err} ->
                            version_bump_task_ffi:resolve({error, Err});

                        {ok, Releases} ->
                            case erlang:element(9, Plugin) of
                                none ->
                                    version_bump_task_ffi:resolve(
                                        {ok, Releases}
                                    );

                                {some, Hook} ->
                                    version_bump_task_ffi:map(
                                        Hook(Spec, Context),
                                        fun(Published) -> case Published of
                                                {ok, {some, Release}} ->
                                                    {ok, [Release | Releases]};

                                                {ok, none} ->
                                                    {ok, Releases};

                                                {error, Err@1} ->
                                                    {error, Err@1}
                                            end end
                                    )
                            end
                    end end)
        end
    ),
    version_bump_task_ffi:map(Accumulated, fun(Collected) -> case Collected of
                {ok, Releases@1} ->
                    {ok, lists:reverse(Releases@1)};

                {error, Err@2} ->
                    {error, Err@2}
            end end).