-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.