Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 6 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,36 +210,30 @@ GitHub Actions Docs: [error](https://docs.github.com/en/actions/using-workflows/
# ::error title=test title,file=abc.py,col=1,endColumn=2,line=4,endLine=5::test message
```

### **`set_output(name, value, use_subprocess=False)`**
### **`set_output(name, value)`**

Sets an action's output parameter for the running workflow.
Sets a step's output parameter by writing to `GITHUB_OUTPUT` environment file. Note that the step will need an `id` to be defined to later retrieve the output value.
GitHub Actions Docs: [set_output](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter)

**example:**

```python
>> from github_action_utils import set_output

>> set_output("test_name", "test_value",)

# Output:
# ::set-output name=test_name::test_value
>> set_output("my_output", "test value")
```

### **`save_state(name, value, use_subprocess=False)`**
### **`save_state(name, value)`**

Creates environment variable for sharing state with workflow's pre: or post: actions.
Creates an environment variable by writing this to the `GITHUB_STATE` environment file which is available to workflow's pre: or post: actions.
GitHub Actions Docs: [save_state](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#sending-values-to-the-pre-and-post-actions)

**example:**

```python
>> from github_action_utils import save_state

>> save_state("test_name", "test_value",)

# Output:
# ::save-state name=test_name::test_value
>> save_state("my_state", "test value")
```

### **`get_state(name)`**
Expand Down
64 changes: 34 additions & 30 deletions github_action_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from contextlib import contextmanager
from functools import lru_cache
from typing import Any, Dict, Generator, Union
from warnings import warn

if sys.version_info >= (3, 8):
from typing import Literal
Expand Down Expand Up @@ -136,24 +137,32 @@ def _build_options_string(**kwargs: Any) -> str:
)


def set_output(name: str, value: Any, use_subprocess: bool = False) -> None:
"""
Sets an action's output parameter.
def _build_file_input(name: str, value: Any) -> bytes:
return (
f"{_escape_property(name)}"
f"<<{ACTION_ENV_DELIMITER}\n"
f"{_escape_data(value)}\n"
f"{ACTION_ENV_DELIMITER}\n".encode("utf-8")
)

Template: ::set-output name={name}::{value}
Example: echo "::set-output name=action_fruit::strawberry"

def set_output(name: str, value: Any, use_subprocess: Union[bool, None] = None) -> None:
"""
sets out for your workflow using GITHUB_OUTPUT file.

:param name: name of the output
:param value: value of the output
:param use_subprocess: use subprocess module to echo command
:returns: None
"""
_print_command(
"set-output",
value,
options_string=_build_options_string(name=name),
use_subprocess=use_subprocess,
)
if use_subprocess is not None:
warn(
"Argument `use_subprocess` for `set_output()` is deprecated and "
"going to be removed in the next version.",
DeprecationWarning,
)

with open(os.environ["GITHUB_OUTPUT"], "ab") as f:
f.write(_build_file_input(name, value))


def echo(message: Any, use_subprocess: bool = False) -> None:
Expand Down Expand Up @@ -317,24 +326,24 @@ def error(
)


def save_state(name: str, value: Any, use_subprocess: bool = False) -> None:
def save_state(name: str, value: Any, use_subprocess: Union[bool, None] = None) -> None:
"""
creates environment variable for sharing with your workflow's pre: or post: actions.

Template: ::save-state name={name}::{value}
Example: echo "::save-state name=processID::12345"
sets state for your workflow using $GITHUB_STATE file
for sharing it with your workflow's pre: or post: actions.

:param name: Name of the state environment variable (e.g: STATE_{name})
:param value: value of the state environment variable
:param use_subprocess: use subprocess module to echo command
:returns: None
"""
_print_command(
"save-state",
value,
options_string=_build_options_string(name=name),
use_subprocess=use_subprocess,
)
if use_subprocess is not None:
warn(
"Argument `use_subprocess` for `save_state()` is deprecated and "
"going to be removed in the next version.",
DeprecationWarning,
)

with open(os.environ["GITHUB_STATE"], "ab") as f:
f.write(_build_file_input(name, value))


def get_state(name: str) -> Union[str, None]:
Expand Down Expand Up @@ -484,12 +493,7 @@ def set_env(name: str, value: Any) -> None:
:returns: None
"""
with open(os.environ["GITHUB_ENV"], "ab") as f:
f.write(
f"{_escape_property(name)}"
f"<<{ACTION_ENV_DELIMITER}\n"
f"{_escape_data(value)}\n"
f"{ACTION_ENV_DELIMITER}\n".encode("utf-8")
)
f.write(_build_file_input(name, value))


def get_workflow_environment_variables() -> Dict[str, Any]:
Expand Down
83 changes: 49 additions & 34 deletions tests/test_github_action_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ def test__build_options_string(input_kwargs: Any, expected: str) -> None:
assert gha_utils._build_options_string(**input_kwargs) == expected


def test__build_file_input() -> None:
assert (
gha_utils._build_file_input("test", "value")
== b"test<<__ENV_DELIMITER__\nvalue\n__ENV_DELIMITER__\n"
)


@pytest.mark.parametrize(
"test_input,expected",
[
Expand Down Expand Up @@ -235,42 +242,50 @@ def test_error(
assert out == expected


@pytest.mark.parametrize(
"input_args,expected",
[
(["abc", 123], "::set-output name=abc::123\n"),
(["abc", "test"], "::set-output name=abc::test\n"),
(["abc", {"k": "v"}], '::set-output name=abc::{"k": "v"}\n'),
],
)
def test_set_output(
capfd: Any,
input_args: Any,
expected: str,
) -> None:
gha_utils.set_output(*input_args)
out, err = capfd.readouterr()
print(out)
assert out == expected
def test_set_output(tmpdir: Any) -> None:
file = tmpdir.join("output_file")

with mock.patch.dict(os.environ, {"GITHUB_OUTPUT": file.strpath}):
gha_utils.set_output("test", "test")
gha_utils.set_output("another", 2)

@pytest.mark.parametrize(
"input_args,expected",
[
(["abc", 123], "::save-state name=abc::123\n"),
(["abc", "test"], "::save-state name=abc::test\n"),
(["abc", {"k": "v"}], '::save-state name=abc::{"k": "v"}\n'),
],
)
def test_save_state(
capfd: Any,
input_args: Any,
expected: str,
) -> None:
gha_utils.save_state(*input_args)
out, err = capfd.readouterr()
print(out)
assert out == expected
assert file.read() == (
"test<<__ENV_DELIMITER__\n"
"test\n__ENV_DELIMITER__\n"
"another<<__ENV_DELIMITER__\n2\n"
"__ENV_DELIMITER__\n"
)


def test_set_output_deprecation_warning(tmpdir: Any) -> None:
file = tmpdir.join("output_file")

with pytest.deprecated_call():
with mock.patch.dict(os.environ, {"GITHUB_OUTPUT": file.strpath}):
gha_utils.set_output("test", "test", use_subprocess=True)


def test_save_state(tmpdir: Any) -> None:
file = tmpdir.join("state_file")

with mock.patch.dict(os.environ, {"GITHUB_STATE": file.strpath}):
gha_utils.save_state("test", "test")
gha_utils.save_state("another", 2)

assert file.read() == (
"test<<__ENV_DELIMITER__\n"
"test\n__ENV_DELIMITER__\n"
"another<<__ENV_DELIMITER__\n2\n"
"__ENV_DELIMITER__\n"
)


def test_save_state_deprecation_warning(tmpdir: Any) -> None:
file = tmpdir.join("state_file")

with pytest.deprecated_call():
with mock.patch.dict(os.environ, {"GITHUB_STATE": file.strpath}):
gha_utils.save_state("test", "test", use_subprocess=True)


@mock.patch.dict(os.environ, {"STATE_test_state": "test", "abc": "another test"})
Expand Down