Skip to main content

src/version_bump@engine.erl

-module(version_bump@engine).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/version_bump/engine.gleam").
-export([run/3]).
-export_type([summary/0, sync_outcome/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 release pipeline orchestrator.\n"
    "\n"
    " `run` wires the leaf modules (git, branches, commit_parser), the plugin\n"
    " registry, and the hook runners into semantic-release's lifecycle:\n"
    "\n"
    "   1. resolve the current branch and build the shared `Context`\n"
    "   2. resolve each configured plugin against the registry\n"
    "   3. verify_conditions\n"
    "   4. find the last release from the git tags\n"
    "   5. read & parse the commits since that release\n"
    "   6. analyze_commits -> a release type (or stop: \"no release\")\n"
    "   7. compute the next version and build the `NextRelease`\n"
    "   8. verify_release\n"
    "   9. generate_notes -> attach to the next release\n"
    "   10. (dry-run) report and stop\n"
    "   11. prepare, then create & push the git tag\n"
    "   12. publish -> collect the produced releases\n"
    "   13. success\n"
    "\n"
    " Any error after `verify_conditions` triggers the plugins' `fail` hooks\n"
    " before the error is returned to the caller.\n"
).

-type summary() :: {summary,
        boolean(),
        gleam@option:option(binary()),
        gleam@option:option(binary()),
        list(version_bump@release:release())}.

-type sync_outcome() :: {halt, summary()} |
    {ready, version_bump@context:context(), binary(), binary(), binary()}.

-file("src/version_bump/engine.gleam", 321).
?DOC(" Run `prepare`, then create and push the git tag. All synchronous.\n").
-spec prepare_and_tag(
    version_bump@context:context(),
    list({version_bump@config:plugin_spec(), version_bump@plugin:plugin()}),
    binary(),
    binary()
) -> {ok, nil} | {error, version_bump@error:release_error()}.
prepare_and_tag(Context, Plugins, Version, Git_tag) ->
    version_bump@logging:info(<<"Preparing release"/utf8>>),
    gleam@result:'try'(
        version_bump@runner:run_prepare(Plugins, Context),
        fun(_) ->
            version_bump@logging:info(
                <<"Creating git tag "/utf8, Git_tag/binary>>
            ),
            gleam@result:'try'(
                version_bump@git:create_tag(
                    erlang:element(2, Context),
                    Git_tag,
                    Version
                ),
                fun(_) ->
                    gleam@result:'try'(
                        version_bump@git:push(
                            erlang:element(2, Context),
                            <<"origin"/utf8>>,
                            <<"HEAD:"/utf8,
                                (erlang:element(2, erlang:element(5, Context)))/binary>>
                        ),
                        fun(_) ->
                            version_bump@git:push(
                                erlang:element(2, Context),
                                <<"origin"/utf8>>,
                                Git_tag
                            )
                        end
                    )
                end
            )
        end
    ).

-file("src/version_bump/engine.gleam", 282).
?DOC(
    " The asynchronous effecting tail: prepare & tag (synchronous), then publish\n"
    " (asynchronous) and success.\n"
).
-spec finalize_release(
    version_bump@context:context(),
    list({version_bump@config:plugin_spec(), version_bump@plugin:plugin()}),
    binary(),
    binary(),
    binary()
) -> version_bump@task:task({ok, summary()} |
    {error, version_bump@error:release_error()}).
finalize_release(Context, Plugins, Version, Git_tag, Notes) ->
    case prepare_and_tag(Context, Plugins, Version, Git_tag) of
        {error, Err} ->
            version_bump_task_ffi:resolve({error, Err});

        {ok, nil} ->
            version_bump@logging:info(<<"Publishing release"/utf8>>),
            version_bump_task_ffi:map(
                version_bump@runner:run_publish(Plugins, Context),
                fun(Published) -> case Published of
                        {error, Err@1} ->
                            {error, Err@1};

                        {ok, Releases} ->
                            Context@1 = {context,
                                erlang:element(2, Context),
                                erlang:element(3, Context),
                                erlang:element(4, Context),
                                erlang:element(5, Context),
                                erlang:element(6, Context),
                                erlang:element(7, Context),
                                erlang:element(8, Context),
                                erlang:element(9, Context),
                                Releases,
                                erlang:element(11, Context),
                                erlang:element(12, Context)},
                            version_bump@logging:info(
                                <<"Running success hooks"/utf8>>
                            ),
                            case version_bump@runner:run_success(
                                Plugins,
                                Context@1
                            ) of
                                {error, Err@2} ->
                                    {error, Err@2};

                                {ok, nil} ->
                                    version_bump@logging:success(
                                        <<"Published release "/utf8,
                                            Version/binary>>
                                    ),
                                    {ok,
                                        {summary,
                                            true,
                                            {some, Version},
                                            {some, Notes},
                                            Releases}}
                            end
                    end end
            )
    end.

-file("src/version_bump/engine.gleam", 384).
?DOC(" Render a tag from a `tag_format` by substituting the version placeholder.\n").
-spec render_tag(binary(), binary()) -> binary().
render_tag(Tag_format, Version) ->
    gleam@string:replace(Tag_format, <<"${version}"/utf8>>, Version).

-file("src/version_bump/engine.gleam", 209).
?DOC(
    " Synchronous continuation once a release is warranted: compute the version,\n"
    " verify the release, generate notes, then decide between a dry-run halt and a\n"
    " ready-to-publish outcome.\n"
).
-spec sync_continue(
    version_bump@config:config(),
    version_bump@context:context(),
    list({version_bump@config:plugin_spec(), version_bump@plugin:plugin()}),
    gleam@option:option(version_bump@release:last_release()),
    version_bump@semver:release_type()
) -> {ok, sync_outcome()} | {error, version_bump@error:release_error()}.
sync_continue(Config, Context, Plugins, Last_release, Rtype) ->
    gleam@result:'try'(
        version_bump@branch:next_version(
            Last_release,
            Rtype,
            erlang:element(5, Context),
            erlang:element(8, Config)
        ),
        fun(Version) ->
            gleam@result:'try'(
                version_bump@git:head_sha(erlang:element(2, Context)),
                fun(Head) ->
                    Git_tag = render_tag(erlang:element(3, Config), Version),
                    Next = {next_release,
                        Version,
                        Rtype,
                        Git_tag,
                        Head,
                        erlang:element(4, erlang:element(5, Context)),
                        <<""/utf8>>},
                    Context@1 = {context,
                        erlang:element(2, Context),
                        erlang:element(3, Context),
                        erlang:element(4, Context),
                        erlang:element(5, Context),
                        erlang:element(6, Context),
                        erlang:element(7, Context),
                        erlang:element(8, Context),
                        {some, Next},
                        erlang:element(10, Context),
                        erlang:element(11, Context),
                        erlang:element(12, Context)},
                    version_bump@logging:info(
                        <<<<<<<<"The next release version is "/utf8,
                                        Version/binary>>/binary,
                                    " ("/utf8>>/binary,
                                (version_bump@semver:release_type_to_string(
                                    Rtype
                                ))/binary>>/binary,
                            ")"/utf8>>
                    ),
                    version_bump@logging:info(<<"Verifying release"/utf8>>),
                    gleam@result:'try'(
                        version_bump@runner:run_verify_release(
                            Plugins,
                            Context@1
                        ),
                        fun(_) ->
                            version_bump@logging:info(
                                <<"Generating release notes"/utf8>>
                            ),
                            gleam@result:'try'(
                                version_bump@runner:run_generate_notes(
                                    Plugins,
                                    Context@1
                                ),
                                fun(Notes) ->
                                    Next@1 = {next_release,
                                        erlang:element(2, Next),
                                        erlang:element(3, Next),
                                        erlang:element(4, Next),
                                        erlang:element(5, Next),
                                        erlang:element(6, Next),
                                        Notes},
                                    Context@2 = {context,
                                        erlang:element(2, Context@1),
                                        erlang:element(3, Context@1),
                                        erlang:element(4, Context@1),
                                        erlang:element(5, Context@1),
                                        erlang:element(6, Context@1),
                                        erlang:element(7, Context@1),
                                        erlang:element(8, Context@1),
                                        {some, Next@1},
                                        erlang:element(10, Context@1),
                                        erlang:element(11, Context@1),
                                        erlang:element(12, Context@1)},
                                    case erlang:element(6, Config) of
                                        true ->
                                            version_bump@logging:warn(
                                                <<"Dry-run: skipping prepare, tag, publish and success"/utf8>>
                                            ),
                                            version_bump@logging:info(
                                                <<<<"Release note for version "/utf8,
                                                        Version/binary>>/binary,
                                                    ":"/utf8>>
                                            ),
                                            version_bump@logging:info(Notes),
                                            {ok,
                                                {halt,
                                                    {summary,
                                                        false,
                                                        {some, Version},
                                                        {some, Notes},
                                                        []}}};

                                        false ->
                                            {ok,
                                                {ready,
                                                    Context@2,
                                                    Version,
                                                    Git_tag,
                                                    Notes}}
                                    end
                                end
                            )
                        end
                    )
                end
            )
        end
    ).

-file("src/version_bump/engine.gleam", 376).
?DOC(" The git_head to range from, defaulting to the tag when no SHA was recorded.\n").
-spec option_from_tag(version_bump@release:last_release()) -> gleam@option:option(binary()).
option_from_tag(Release) ->
    case gleam@string:trim(erlang:element(3, Release)) of
        <<""/utf8>> ->
            none;

        Tag ->
            {some, Tag}
    end.

-file("src/version_bump/engine.gleam", 357).
?DOC(
    " Load the commits since the last release into the context, parsing each into\n"
    " a `ConventionalCommit`.\n"
).
-spec load_commits(
    version_bump@context:context(),
    gleam@option:option(version_bump@release:last_release())
) -> {ok, version_bump@context:context()} |
    {error, version_bump@error:release_error()}.
load_commits(Context, Last_release) ->
    From = case Last_release of
        {some, Release} ->
            case gleam@string:trim(erlang:element(4, Release)) of
                <<""/utf8>> ->
                    option_from_tag(Release);

                Head ->
                    {some, Head}
            end;

        none ->
            none
    end,
    gleam@result:map(
        version_bump@git:log_since(erlang:element(2, Context), From),
        fun(Commits) ->
            Parsed = gleam@list:map(
                Commits,
                fun version_bump@commit_parser:parse/1
            ),
            version_bump@logging:info(
                <<<<"Found "/utf8,
                        (erlang:integer_to_binary(erlang:length(Parsed)))/binary>>/binary,
                    " commit(s)"/utf8>>
            ),
            {context,
                erlang:element(2, Context),
                erlang:element(3, Context),
                erlang:element(4, Context),
                erlang:element(5, Context),
                erlang:element(6, Context),
                Parsed,
                erlang:element(8, Context),
                erlang:element(9, Context),
                erlang:element(10, Context),
                erlang:element(11, Context),
                erlang:element(12, Context)}
        end
    ).

-file("src/version_bump/engine.gleam", 389).
?DOC(" Log the discovered last release, or note that this is the first one.\n").
-spec log_last_release(gleam@option:option(version_bump@release:last_release())) -> nil.
log_last_release(Last_release) ->
    case Last_release of
        {some, Release} ->
            version_bump@logging:info(
                <<"Found previous release "/utf8,
                    (erlang:element(2, Release))/binary>>
            );

        none ->
            version_bump@logging:info(<<"No previous release found"/utf8>>)
    end.

-file("src/version_bump/engine.gleam", 347).
?DOC(" Read the repository's tags and pick the last release for the current branch.\n").
-spec resolve_last_release(
    version_bump@config:config(),
    version_bump@context:context()
) -> {ok, gleam@option:option(version_bump@release:last_release())} |
    {error, version_bump@error:release_error()}.
resolve_last_release(Config, Context) ->
    gleam@result:map(
        version_bump@git:get_tags(erlang:element(2, Context)),
        fun(Tags) ->
            version_bump@branch:last_release(
                Tags,
                erlang:element(5, Context),
                erlang:element(3, Config)
            )
        end
    ).

-file("src/version_bump/engine.gleam", 172).
?DOC(
    " The fully synchronous part of the pipeline: verify_conditions, find the last\n"
    " release, read & analyze commits, compute the next version, verify_release,\n"
    " generate notes, and apply the dry-run short-circuit.\n"
).
-spec sync_pipeline(
    version_bump@config:config(),
    version_bump@context:context(),
    list({version_bump@config:plugin_spec(), version_bump@plugin:plugin()})
) -> {ok, sync_outcome()} | {error, version_bump@error:release_error()}.
sync_pipeline(Config, Context, Plugins) ->
    version_bump@logging:info(<<"Verifying conditions"/utf8>>),
    gleam@result:'try'(
        version_bump@runner:run_verify_conditions(Plugins, Context),
        fun(_) ->
            gleam@result:'try'(
                resolve_last_release(Config, Context),
                fun(Last_release) ->
                    Context@1 = {context,
                        erlang:element(2, Context),
                        erlang:element(3, Context),
                        erlang:element(4, Context),
                        erlang:element(5, Context),
                        erlang:element(6, Context),
                        erlang:element(7, Context),
                        Last_release,
                        erlang:element(9, Context),
                        erlang:element(10, Context),
                        erlang:element(11, Context),
                        erlang:element(12, Context)},
                    log_last_release(Last_release),
                    gleam@result:'try'(
                        load_commits(Context@1, Last_release),
                        fun(Context@2) ->
                            version_bump@logging:info(
                                <<"Analyzing commits"/utf8>>
                            ),
                            gleam@result:'try'(
                                version_bump@runner:run_analyze_commits(
                                    Plugins,
                                    Context@2
                                ),
                                fun(Release_type) -> case Release_type of
                                        none ->
                                            version_bump@logging:info(
                                                <<"There are no relevant changes, so no new version is released"/utf8>>
                                            ),
                                            {ok,
                                                {halt,
                                                    {summary,
                                                        false,
                                                        none,
                                                        none,
                                                        []}}};

                                        {some, Rtype} ->
                                            sync_continue(
                                                Config,
                                                Context@2,
                                                Plugins,
                                                Last_release,
                                                Rtype
                                            )
                                    end end
                            )
                        end
                    )
                end
            )
        end
    ).

-file("src/version_bump/engine.gleam", 156).
?DOC(
    " The pipeline body. The synchronous stages (3-10) run in `sync_pipeline`;\n"
    " only the publish tail is asynchronous, so it is lifted into a `Task` here.\n"
).
-spec pipeline(
    version_bump@config:config(),
    version_bump@context:context(),
    list({version_bump@config:plugin_spec(), version_bump@plugin:plugin()})
) -> version_bump@task:task({ok, summary()} |
    {error, version_bump@error:release_error()}).
pipeline(Config, Context, Plugins) ->
    case sync_pipeline(Config, Context, Plugins) of
        {error, Err} ->
            version_bump_task_ffi:resolve({error, Err});

        {ok, {halt, Summary}} ->
            version_bump_task_ffi:resolve({ok, Summary});

        {ok, {ready, Context@1, Version, Git_tag, Notes}} ->
            finalize_release(Context@1, Plugins, Version, Git_tag, Notes)
    end.

-file("src/version_bump/engine.gleam", 129).
?DOC(
    " Drive the verify -> analyze -> prepare -> publish -> success pipeline. On\n"
    " any error from `verify_conditions` onward, the plugins' `fail` hooks run\n"
    " before the error is propagated. Asynchronous (publish), hence a `Task`.\n"
).
-spec run_pipeline(
    version_bump@config:config(),
    version_bump@context:context(),
    list({version_bump@config:plugin_spec(), version_bump@plugin:plugin()})
) -> version_bump@task:task({ok, summary()} |
    {error, version_bump@error:release_error()}).
run_pipeline(Config, Context, Plugins) ->
    version_bump_task_ffi:map(
        pipeline(Config, Context, Plugins),
        fun(Result) -> case Result of
                {ok, Summary} ->
                    {ok, Summary};

                {error, Err} ->
                    version_bump@logging:error(
                        version_bump@error:to_string(Err)
                    ),
                    _ = version_bump@runner:run_fail(Plugins, Context),
                    {error, Err}
            end end
    ).

-file("src/version_bump/engine.gleam", 111).
?DOC(
    " Zip each configured `PluginSpec` with its registry `Plugin`, failing with a\n"
    " `ConfigError` for any spec whose name is not a known built-in plugin.\n"
).
-spec resolve_plugins(
    version_bump@config:config(),
    version_bump@context:context()
) -> {ok,
        list({version_bump@config:plugin_spec(), version_bump@plugin:plugin()})} |
    {error, version_bump@error:release_error()}.
resolve_plugins(Config, _) ->
    Known = version_bump@registry:default(),
    gleam@list:try_map(
        erlang:element(5, Config),
        fun(Spec) ->
            case gleam_stdlib:map_get(Known, erlang:element(2, Spec)) of
                {ok, Plugin} ->
                    {ok, {Spec, Plugin}};

                {error, _} ->
                    {error,
                        {config_error,
                            <<<<"Unknown plugin '"/utf8,
                                    (erlang:element(2, Spec))/binary>>/binary,
                                "'"/utf8>>}}
            end
        end
    ).

-file("src/version_bump/engine.gleam", 87).
?DOC(" Resolve branches against the repository and build the initial context.\n").
-spec build_context(
    version_bump@config:config(),
    binary(),
    gleam@dict:dict(binary(), binary())
) -> {ok, version_bump@context:context()} |
    {error, version_bump@error:release_error()}.
build_context(Config, Cwd, Env) ->
    gleam@result:'try'(
        version_bump@git:list_branches(Cwd),
        fun(Git_branches) ->
            gleam@result:'try'(
                version_bump@git:current_branch(Cwd),
                fun(Current) ->
                    gleam@result:'try'(
                        version_bump@branch:resolve(
                            Config,
                            Git_branches,
                            Current
                        ),
                        fun(_use0) ->
                            {Branch, All_branches} = _use0,
                            version_bump@logging:info(
                                <<<<"Running on branch '"/utf8,
                                        (erlang:element(2, Branch))/binary>>/binary,
                                    "'"/utf8>>
                            ),
                            {ok,
                                version_bump@context:new(
                                    Cwd,
                                    Env,
                                    Config,
                                    Branch,
                                    All_branches
                                )}
                        end
                    )
                end
            )
        end
    ).

-file("src/version_bump/engine.gleam", 66).
?DOC(
    " Run the full release pipeline for the project rooted at `cwd`.\n"
    "\n"
    " Returns a `Task` because the `publish` stage is asynchronous: on Erlang the\n"
    " task is synchronous; on JavaScript it is a promise. Callers run it with\n"
    " `task.run`.\n"
).
-spec run(
    version_bump@config:config(),
    binary(),
    gleam@dict:dict(binary(), binary())
) -> version_bump@task:task({ok, summary()} |
    {error, version_bump@error:release_error()}).
run(Config, Cwd, Env) ->
    case build_context(Config, Cwd, Env) of
        {error, Err} ->
            version_bump_task_ffi:resolve({error, Err});

        {ok, Ctx0} ->
            case resolve_plugins(Config, Ctx0) of
                {error, Err@1} ->
                    version_bump_task_ffi:resolve({error, Err@1});

                {ok, Plugins} ->
                    run_pipeline(Config, Ctx0, Plugins)
            end
    end.