Skip to content

Commit

Permalink
fix: enable user configuration of build_command env vars (#925)
Browse files Browse the repository at this point in the history
- test(version): add test of user defined env variables in build command

  ref: #922

- fix(version): enable user config of `build_command` env variables

  Resolves: #922

- docs(configuration): document `build_command_env` configuration option
  • Loading branch information
codejedi365 committed May 13, 2024
1 parent b573c4d commit 6b5b271
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 3 deletions.
36 changes: 35 additions & 1 deletion docs/configuration.rst
Expand Up @@ -139,7 +139,7 @@ This setting is discussed in more detail at :ref:`multibranch-releases`
----
.. _config-build-command:
.. _config-build_command:
``build_command``
"""""""""""""""""
Expand All @@ -155,6 +155,9 @@ version. The following table summarizes all the environment variables that
are passed on to the ``build_command`` runtime if they exist in the parent
process.
If you would like to pass additional environment variables to your build
command, see :ref:`config-build_command_env`.
======================== ======================================================================
Variable Name Description
======================== ======================================================================
Expand All @@ -174,6 +177,37 @@ VIRTUAL_ENV Pass-through ``VIRTUAL_ENV`` if exists in process env,
----
.. _config-build_command_env:
``build_command_env``
"""""""""""""""""""""
**Type:** ``Optional[list[str]]``
List of environment variables to include or pass-through on to the build command that executes
during :ref:`cmd-version`.
This configuration option allows the user to extend the list of environment variables
from the table above in :ref:`config-build_command`. The input is a list of strings
where each individual string handles a single variable definition. There are two formats
accepted and are detailed in the following table:
================== ===================================================================
FORMAT Description
================== ===================================================================
``VAR_NAME`` Detects value from the PSR process environment, and passes value to
``build_command`` process
``VAR_NAME=value`` Sets variable name to value inside of ``build_command`` process
================== ===================================================================
.. note:: Although variable name capitalization is not required, it is recommended as
to be in-line with the POSIX-compliant recommendation for shell variable names.
**Default:** ``None`` (not specified)
----
.. _config-commit_author:
``commit_author (str)``
Expand Down
6 changes: 5 additions & 1 deletion semantic_release/cli/commands/version.py
Expand Up @@ -517,7 +517,7 @@ def version( # noqa: C901
filter(
lambda k_v: k_v[1] is not None, # type: ignore
{
"NEW_VERSION": str(new_version),
# Common values
"PATH": os.getenv("PATH", ""),
"HOME": os.getenv("HOME", None),
"VIRTUAL_ENV": os.getenv("VIRTUAL_ENV", None),
Expand All @@ -535,6 +535,10 @@ def version( # noqa: C901
"PSR_DOCKER_GITHUB_ACTION": os.getenv(
"PSR_DOCKER_GITHUB_ACTION", None
),
# User defined overrides of environment (from config)
**runtime.build_command_env,
# PSR injected environment variables
"NEW_VERSION": str(new_version),
}.items(),
)
),
Expand Down
33 changes: 33 additions & 0 deletions semantic_release/cli/config.py
Expand Up @@ -212,6 +212,7 @@ class RawConfig(BaseModel):
assets: List[str] = []
branches: Dict[str, BranchConfig] = {"main": BranchConfig()}
build_command: Optional[str] = None
build_command_env: List[str] = []
changelog: ChangelogConfig = ChangelogConfig()
commit_author: MaybeFromEnv = EnvConfigVar(
env="GIT_COMMIT_AUTHOR", default=DEFAULT_COMMIT_AUTHOR
Expand All @@ -229,6 +230,11 @@ class RawConfig(BaseModel):
version_toml: Optional[Tuple[str, ...]] = None
version_variables: Optional[Tuple[str, ...]] = None

@field_validator("build_command_env", mode="after")
@classmethod
def remove_whitespace(cls, val: list[str]) -> list[str]:
return [entry.strip() for entry in val]

@model_validator(mode="after")
def set_default_opts(self) -> Self:
# Set the default parser options for the given commit parser when no user input is given
Expand Down Expand Up @@ -352,6 +358,7 @@ class RuntimeContext:
template_environment: Environment
template_dir: Path
build_command: Optional[str]
build_command_env: dict[str, str]
dist_glob_patterns: Tuple[str, ...]
upload_to_vcs_release: bool
global_cli_options: GlobalCommandLineOptions
Expand Down Expand Up @@ -541,13 +548,39 @@ def from_raw_config(
tag_format=raw.tag_format, prerelease_token=branch_config.prerelease_token
)

build_cmd_env = {}

for i, env_var_def in enumerate(raw.build_command_env):
# creative hack to handle, missing =, but also = that then can be unpacked
# as the resulting parts array can be either 2 or 3 in length. it becomes 3
# with our forced empty value at the end which can be dropped
parts = [*env_var_def.split("=", 1), ""]
# removes any odd spacing around =, and extracts name=value
name, env_val = [part.strip() for part in parts[:2]]

if not name:
# Skip when invalid format (ex. starting with = and no name)
logging.warning(
"Skipping invalid build_command_env[%s] definition",
i,
)
continue

if not env_val and env_var_def[-1] != "=":
# avoid the edge case that user wants to define a value as empty
# and don't autoresolve it
env_val = os.getenv(name, "")

build_cmd_env[name] = env_val

self = cls(
repo=repo,
commit_parser=commit_parser,
version_translator=version_translator,
major_on_zero=raw.major_on_zero,
allow_zero_version=raw.allow_zero_version,
build_command=raw.build_command,
build_command_env=build_cmd_env,
version_declarations=tuple(version_declarations),
hvcs_client=hvcs_client,
changelog_file=changelog_file,
Expand Down
79 changes: 78 additions & 1 deletion tests/command_line/test_version.py
Expand Up @@ -551,7 +551,6 @@ def test_version_prints_current_version_if_no_new_version(
def test_version_runs_build_command(
repo_with_git_flow_angular_commits: Repo,
cli_runner: CliRunner,
example_pyproject_toml: Path,
update_pyproject_toml: UpdatePyprojectTomlFn,
shell: str,
):
Expand Down Expand Up @@ -604,6 +603,84 @@ def test_version_runs_build_command(
)


def test_version_runs_build_command_w_user_env(
repo_with_git_flow_angular_commits: Repo,
cli_runner: CliRunner,
update_pyproject_toml: UpdatePyprojectTomlFn,
):
# Setup
patched_os_environment = {
"CI": "true",
"PATH": os.getenv("PATH"),
"HOME": os.getenv("HOME"),
"VIRTUAL_ENV": os.getenv("VIRTUAL_ENV", "./.venv"),
# Simulate that all CI's are set
"GITHUB_ACTIONS": "true",
"GITLAB_CI": "true",
"GITEA_ACTIONS": "true",
"BITBUCKET_REPO_FULL_NAME": "python-semantic-release/python-semantic-release.git",
"PSR_DOCKER_GITHUB_ACTION": "true",
# User environment variables (varying passthrough results)
"MY_CUSTOM_VARIABLE": "custom",
"IGNORED_VARIABLE": "ignore_me",
"OVERWRITTEN_VAR": "initial",
"SET_AS_EMPTY_VAR": "not_empty",
}
build_command = "bash -c \"echo 'hello world'\""
update_pyproject_toml("tool.semantic_release.build_command", build_command)
update_pyproject_toml(
"tool.semantic_release.build_command_env",
[
# Includes arbitrary whitespace which will be removed
" MY_CUSTOM_VARIABLE ", # detect and pass from environment
" OVERWRITTEN_VAR = overrided", # pass hardcoded value which overrides environment
" SET_AS_EMPTY_VAR = ", # keep variable initialized but as empty string
" HARDCODED_VAR=hardcoded ", # pass hardcoded value that doesn't override anything
"VAR_W_EQUALS = a-var===condition", # only splits on 1st equals sign
"=ignored-invalid-named-var", # TODO: validation error instead, but currently just ignore
]
)

# Mock out subprocess.run
with mock.patch(
"subprocess.run", return_value=CompletedProcess(args=(), returncode=0)
) as patched_subprocess_run, mock.patch(
"shellingham.detect_shell", return_value=("bash", "/usr/bin/bash")
), mock.patch.dict("os.environ", patched_os_environment, clear=True):
# ACT: run & force a new version that will trigger the build command
result = cli_runner.invoke(
main, [version_subcmd, "--patch", "--no-commit", "--no-tag", "--no-changelog", "--no-push"]
)

patched_subprocess_run.assert_called_once_with(
["bash", "-c", build_command],
check=True,
env={
"NEW_VERSION": "1.2.1", # injected into environment
"CI": patched_os_environment["CI"],
"BITBUCKET_CI": "true", # Converted
"GITHUB_ACTIONS": patched_os_environment["GITHUB_ACTIONS"],
"GITEA_ACTIONS": patched_os_environment["GITEA_ACTIONS"],
"GITLAB_CI": patched_os_environment["GITLAB_CI"],
"HOME": patched_os_environment["HOME"],
"PATH": patched_os_environment["PATH"],
"VIRTUAL_ENV": patched_os_environment["VIRTUAL_ENV"],
"PSR_DOCKER_GITHUB_ACTION": patched_os_environment[
"PSR_DOCKER_GITHUB_ACTION"
],
"MY_CUSTOM_VARIABLE": patched_os_environment["MY_CUSTOM_VARIABLE"],
"OVERWRITTEN_VAR": "overrided",
"SET_AS_EMPTY_VAR": "",
"HARDCODED_VAR": "hardcoded",
# Note that IGNORED_VARIABLE is not here.
"VAR_W_EQUALS": "a-var===condition",
},
)

# Make sure it did not error internally
assert result.exit_code == 0


def test_version_skips_build_command_with_skip_build(
repo_with_git_flow_angular_commits, cli_runner
):
Expand Down

0 comments on commit 6b5b271

Please sign in to comment.