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