Skip to main content

src/version_bump@env_ci.erl

-module(version_bump@env_ci).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/version_bump/env_ci.gleam").
-export([detect/1]).
-export_type([ci_env/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(
    " CI environment detection from environment variables. This is a small, pure\n"
    " reimplementation of the parts of the `env-ci` package that the release\n"
    " pipeline relies on: figuring out whether we're running in CI, which provider,\n"
    " the branch and commit under test, and whether the build is for a pull/merge\n"
    " request.\n"
    "\n"
    " Everything here is a pure function of an environment dictionary so it can be\n"
    " unit-tested without touching the real process environment. Callers wire up\n"
    " the live environment (e.g. via `envoy`) and pass it in.\n"
).

-type ci_env() :: {ci_env,
        boolean(),
        binary(),
        gleam@option:option(binary()),
        gleam@option:option(binary()),
        boolean()}.

-file("src/version_bump/env_ci.gleam", 144).
?DOC(" Look up a key, treating empty/whitespace-only values as absent.\n").
-spec get(gleam@dict:dict(binary(), binary()), binary()) -> gleam@option:option(binary()).
get(Env, Key) ->
    case gleam_stdlib:map_get(Env, Key) of
        {ok, Value} ->
            case gleam@string:trim(Value) of
                <<""/utf8>> ->
                    none;

                Trimmed ->
                    {some, Trimmed}
            end;

        {error, _} ->
            none
    end.

-file("src/version_bump/env_ci.gleam", 165).
?DOC(
    " Whether an optional value is a \"truthy\" flag. CI providers conventionally set\n"
    " these to \"true\" or \"1\", but any present non-empty value counts as enabled.\n"
).
-spec is_truthy(gleam@option:option(binary())) -> boolean().
is_truthy(Value) ->
    case Value of
        {some, V} ->
            case string:lowercase(V) of
                <<"false"/utf8>> ->
                    false;

                <<"0"/utf8>> ->
                    false;

                <<"no"/utf8>> ->
                    false;

                <<"off"/utf8>> ->
                    false;

                _ ->
                    true
            end;

        none ->
            false
    end.

-file("src/version_bump/env_ci.gleam", 129).
?DOC(
    " Generic fallback for any CI service that only sets `CI=true`. No branch or\n"
    " commit information is assumed, and it is never treated as a PR build.\n"
).
-spec detect_generic(gleam@dict:dict(binary(), binary())) -> gleam@option:option(ci_env()).
detect_generic(Env) ->
    case is_truthy(get(Env, <<"CI"/utf8>>)) of
        false ->
            none;

        true ->
            {some, {ci_env, true, <<"generic"/utf8>>, none, none, false}}
    end.

-file("src/version_bump/env_ci.gleam", 180).
?DOC(" Return the first present value from a list of candidates.\n").
-spec first_present(list(gleam@option:option(binary()))) -> gleam@option:option(binary()).
first_present(Candidates) ->
    case Candidates of
        [] ->
            none;

        [{some, Value} | _] ->
            {some, Value};

        [none | Rest] ->
            first_present(Rest)
    end.

-file("src/version_bump/env_ci.gleam", 156).
?DOC(" Whether an optional value carries a non-empty string.\n").
-spec is_present(gleam@option:option(binary())) -> boolean().
is_present(Value) ->
    case Value of
        {some, _} ->
            true;

        none ->
            false
    end.

-file("src/version_bump/env_ci.gleam", 101).
?DOC(
    " GitLab CI: identified by `GITLAB_CI=true`.\n"
    "\n"
    " A merge-request pipeline is detected via `CI_MERGE_REQUEST_ID` /\n"
    " `CI_MERGE_REQUEST_IID`, in which case the source branch is\n"
    " `CI_MERGE_REQUEST_SOURCE_BRANCH_NAME`; otherwise the branch is\n"
    " `CI_COMMIT_REF_NAME`. The commit is `CI_COMMIT_SHA`.\n"
).
-spec detect_gitlab(gleam@dict:dict(binary(), binary())) -> gleam@option:option(ci_env()).
detect_gitlab(Env) ->
    case is_truthy(get(Env, <<"GITLAB_CI"/utf8>>)) of
        false ->
            none;

        true ->
            Is_pr = is_present(get(Env, <<"CI_MERGE_REQUEST_ID"/utf8>>)) orelse is_present(
                get(Env, <<"CI_MERGE_REQUEST_IID"/utf8>>)
            ),
            Branch = case Is_pr of
                true ->
                    first_present(
                        [get(
                                Env,
                                <<"CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"/utf8>>
                            ),
                            get(Env, <<"CI_COMMIT_REF_NAME"/utf8>>)]
                    );

                false ->
                    get(Env, <<"CI_COMMIT_REF_NAME"/utf8>>)
            end,
            {some,
                {ci_env,
                    true,
                    <<"gitlab"/utf8>>,
                    Branch,
                    get(Env, <<"CI_COMMIT_SHA"/utf8>>),
                    Is_pr}}
    end.

-file("src/version_bump/env_ci.gleam", 190).
?DOC(
    " Strip a leading `refs/heads/` prefix from a git ref, leaving other refs and\n"
    " plain branch names untouched.\n"
).
-spec strip_ref(gleam@option:option(binary())) -> gleam@option:option(binary()).
strip_ref(Value) ->
    case Value of
        {some, Ref} ->
            case gleam_stdlib:string_starts_with(Ref, <<"refs/heads/"/utf8>>) of
                true ->
                    {some,
                        gleam@string:replace(
                            Ref,
                            <<"refs/heads/"/utf8>>,
                            <<""/utf8>>
                        )};

                false ->
                    {some, Ref}
            end;

        none ->
            none
    end.

-file("src/version_bump/env_ci.gleam", 66).
?DOC(
    " GitHub Actions: identified by `GITHUB_ACTIONS=true`.\n"
    "\n"
    " On a pull-request build the source branch lives in `GITHUB_HEAD_REF`; on a\n"
    " push build the branch is in `GITHUB_REF_NAME` (falling back to stripping the\n"
    " `refs/heads/` prefix off `GITHUB_REF`). The commit is `GITHUB_SHA`, and a PR\n"
    " build is signalled by `GITHUB_EVENT_NAME` being `pull_request` or\n"
    " `pull_request_target`.\n"
).
-spec detect_github(gleam@dict:dict(binary(), binary())) -> gleam@option:option(ci_env()).
detect_github(Env) ->
    case is_truthy(get(Env, <<"GITHUB_ACTIONS"/utf8>>)) of
        false ->
            none;

        true ->
            Event = get(Env, <<"GITHUB_EVENT_NAME"/utf8>>),
            Is_pr = case Event of
                {some, <<"pull_request"/utf8>>} ->
                    true;

                {some, <<"pull_request_target"/utf8>>} ->
                    true;

                _ ->
                    false
            end,
            Branch = case Is_pr of
                true ->
                    first_present([get(Env, <<"GITHUB_HEAD_REF"/utf8>>)]);

                false ->
                    first_present(
                        [get(Env, <<"GITHUB_REF_NAME"/utf8>>),
                            strip_ref(get(Env, <<"GITHUB_REF"/utf8>>))]
                    )
            end,
            {some,
                {ci_env,
                    true,
                    <<"github"/utf8>>,
                    Branch,
                    get(Env, <<"GITHUB_SHA"/utf8>>),
                    Is_pr}}
    end.

-file("src/version_bump/env_ci.gleam", 37).
?DOC(
    " Detect the CI environment from the given environment variables.\n"
    "\n"
    " Providers are checked from most-specific to least-specific so that a generic\n"
    " `CI=true` only wins when no known provider matched. Returns a `CiEnv` with\n"
    " `is_ci: False` and an empty provider when nothing CI-like is present.\n"
).
-spec detect(gleam@dict:dict(binary(), binary())) -> ci_env().
detect(Env) ->
    case detect_github(Env) of
        {some, Result} ->
            Result;

        none ->
            case detect_gitlab(Env) of
                {some, Result@1} ->
                    Result@1;

                none ->
                    case detect_generic(Env) of
                        {some, Result@2} ->
                            Result@2;

                        none ->
                            {ci_env, false, <<""/utf8>>, none, none, false}
                    end
            end
    end.