Skip to content

Commit

Permalink
Use GHA env file to set output (#360)
Browse files Browse the repository at this point in the history
* Add notice, echo and output env file to GHA, echo on in debug mode
  • Loading branch information
EnricoMi committed Oct 14, 2022
1 parent 60624fd commit 2535351
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 22 deletions.
54 changes: 43 additions & 11 deletions python/publish/github_action.py
Expand Up @@ -14,6 +14,7 @@ class GithubAction:
# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#environment-files
ENV_FILE_VAR_NAME = 'GITHUB_ENV'
PATH_FILE_VAR_NAME = 'GITHUB_PATH'
OUTPUT_FILE_VAR_NAME = 'GITHUB_OUTPUT'
JOB_SUMMARY_FILE_VAR_NAME = 'GITHUB_STEP_SUMMARY'

def __init__(self, file: Optional[TextIOWrapper] = None):
Expand All @@ -26,9 +27,6 @@ def __init__(self, file: Optional[TextIOWrapper] = None):

self._file: TextIOWrapper = file

def set_output(self, name: str, value: Any):
self._command(self._file, 'set-output', value, {'name': name})

def add_mask(self, value: str):
self._command(self._file, 'add-mask', value)

Expand All @@ -38,19 +36,36 @@ def stop_commands(self, end_token: str):
def continue_commands(self, end_token: str):
self._command(self._file, end_token)

def save_state(self, name: str, value: Any):
self._command(self._file, 'save-state', value, {'name': name})

def group(self, title: str):
self._command(self._file, 'group', title)

def group_end(self, ):
def group_end(self):
self._command(self._file, 'endgroup')

def debug(self, message: str):
logger.debug(message)
self._command(self._file, 'debug', message)

def notice(self,
message: str,
title: Optional[str] = None,
file: Optional[str] = None,
line: Optional[int] = None,
end_line: Optional[int] = None,
column: Optional[int] = None,
end_column: Optional[int] = None):
logger.info(message)

params = {var: val
for var, val in [("title", title),
("file", file),
("col", column),
("endColumn", end_column),
("line", line),
("endLine", end_line)]
if val is not None}
self._command(self._file, 'notice', message, params)

def warning(self, message: str, file: Optional[str] = None, line: Optional[int] = None, column: Optional[int] = None):
logger.warning(message)

Expand Down Expand Up @@ -107,6 +122,9 @@ def error(self,
params.update(col=column)
self._command(self._file, 'error', message, params)

def echo(self, on: bool):
self._command(self._file, 'echo', 'on' if on else 'off')

@staticmethod
def _command(file: TextIOWrapper, command: str, value: str = '', params: Optional[Mapping[str, Any]] = None):
# take first line of value if multiline
Expand Down Expand Up @@ -134,21 +152,35 @@ def add_to_env(self, var: str, val: str):
def add_to_path(self, path: str):
self._append_to_file(f'{path}\n', self.PATH_FILE_VAR_NAME)

def add_to_output(self, var: str, val: str):
if '\n' in val:
# if this is really needed, implement it as describe here:
# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#multiline-strings
raise ValueError('Multiline values not supported for environment variables')

if not self._append_to_file(f'{var}={val}\n', self.OUTPUT_FILE_VAR_NAME, warn=False):
# this has been deprecated but we fall back if there is no env file
self._command(self._file, 'set-output', val, {'name': var})

def add_to_job_summary(self, markdown: str):
self._append_to_file(markdown, self.JOB_SUMMARY_FILE_VAR_NAME)

def _append_to_file(self, content: str, env_file_var_name: str):
def _append_to_file(self, content: str, env_file_var_name: str, warn: bool = True) -> bool:
# appends content to an environment file denoted by an environment variable name
# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#environment-files
filename = os.getenv(env_file_var_name)
if not filename:
self.warning(f'Cannot append to environment file {env_file_var_name} as it is not set. '
f'See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#environment-files')
return
if warn:
self.warning(f'Cannot append to environment file {env_file_var_name} as it is not set. '
f'See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#environment-files')
return False

try:
with open(filename, 'a', encoding='utf-8') as file:
file.write(content)
except Exception as e:
self.warning(f'Failed to write to environment file {filename}: {str(e)}. '
f'See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#environment-files')
return False

return True
2 changes: 1 addition & 1 deletion python/publish/publisher.py
Expand Up @@ -366,7 +366,7 @@ def publish_json(self, data: PublishData):
pass

# provide a reduced version to Github actions
self._gha.set_output('json', json.dumps(data.to_reduced_dict(self._settings.json_thousands_separator), ensure_ascii=False))
self._gha.add_to_output('json', json.dumps(data.to_reduced_dict(self._settings.json_thousands_separator), ensure_ascii=False))

def publish_job_summary(self,
title: str,
Expand Down
2 changes: 2 additions & 0 deletions python/publish_test_results.py
Expand Up @@ -425,6 +425,8 @@ def set_log_level(handler: logging.Logger, level: str, gha: GithubAction):
log_level = get_var('LOG_LEVEL', options) or 'INFO'
set_log_level(logger, log_level, gha)
set_log_level(publish.logger, log_level, gha)
if log_level == 'DEBUG':
gha.echo(True)

settings = get_settings(options, gha)
main(settings, gha)
53 changes: 46 additions & 7 deletions python/test/test_github_action.py
Expand Up @@ -38,9 +38,29 @@ def gh_action_env_file_test(test: unittest.TestCase, env_file_var_name: str, exp

class TestGithubAction(unittest.TestCase):

def test_set_output(self):
with gh_action_command_test(self, '::set-output name=varname::varval') as gha:
gha.set_output('varname', 'varval')
env_file_var_name = None
output_file_var_name = None
path_file_var_name = None
job_summary_file_var_name = None

@classmethod
def setUpClass(cls) -> None:
cls.env_file_var_name = GithubAction.ENV_FILE_VAR_NAME
cls.output_file_var_name = GithubAction.OUTPUT_FILE_VAR_NAME
cls.path_file_var_name = GithubAction.PATH_FILE_VAR_NAME
cls.job_summary_file_var_name = GithubAction.JOB_SUMMARY_FILE_VAR_NAME

GithubAction.ENV_FILE_VAR_NAME = 'TEST_' + cls.env_file_var_name
GithubAction.OUTPUT_FILE_VAR_NAME = 'TEST_' + cls.output_file_var_name
GithubAction.PATH_FILE_VAR_NAME = 'TEST_' + cls.path_file_var_name
GithubAction.JOB_SUMMARY_FILE_VAR_NAME = 'TEST_' + cls.job_summary_file_var_name

@classmethod
def tearDownClass(cls) -> None:
GithubAction.ENV_FILE_VAR_NAME = cls.env_file_var_name
GithubAction.OUTPUT_FILE_VAR_NAME = cls.output_file_var_name
GithubAction.PATH_FILE_VAR_NAME = cls.path_file_var_name
GithubAction.JOB_SUMMARY_FILE_VAR_NAME = cls.job_summary_file_var_name

def test_add_mask(self):
with gh_action_command_test(self, '::add-mask::the mask') as gha:
Expand All @@ -54,10 +74,6 @@ def test_continue_commands(self):
with gh_action_command_test(self, '::the end token::') as gha:
gha.continue_commands('the end token')

def test_save_state(self):
with gh_action_command_test(self, '::save-state name=state-name::state-value') as gha:
gha.save_state('state-name', 'state-value')

def test_group(self):
with gh_action_command_test(self, '::group::group title') as gha:
gha.group('group title')
Expand All @@ -82,6 +98,12 @@ def test_warning(self):
with gh_action_command_test(self, '::warning file=the file,line=1,col=2::the message') as gha:
gha.warning('the message', file='the file', line=1, column=2)

def test_notice(self):
with gh_action_command_test(self, '::notice::the message') as gha:
gha.notice('the message')
with gh_action_command_test(self, '::notice title=a title,file=the file,col=3,endColumn=4,line=1,endLine=2::the message') as gha:
gha.notice('the message', file='the file', line=1, end_line=2, column=3, end_column=4, title='a title')

def test_error(self):
with gh_action_command_test(self, '::error::the message') as gha:
gha.error('the message')
Expand Down Expand Up @@ -181,6 +203,12 @@ def get_error_with_context() -> RuntimeError:
except RuntimeError as re:
return re

def test_echo(self):
with gh_action_command_test(self, '::echo::on') as gha:
gha.echo(True)
with gh_action_command_test(self, '::echo::off') as gha:
gha.echo(False)

def test_add_env(self):
with gh_action_env_file_test(self, GithubAction.ENV_FILE_VAR_NAME, 'var=val\n') as gha:
gha.add_to_env('var', 'val')
Expand All @@ -195,6 +223,17 @@ def test_add_path(self):
with gh_action_env_file_test(self, GithubAction.PATH_FILE_VAR_NAME, 'additional-path\n') as gha:
gha.add_to_path('additional-path')

def test_add_output(self):
with gh_action_env_file_test(self, GithubAction.OUTPUT_FILE_VAR_NAME, 'var=val\n') as gha:
gha.add_to_output('var', 'val')
with gh_action_env_file_test(self, GithubAction.OUTPUT_FILE_VAR_NAME, 'var1=val3\nvar2=val4\n') as gha:
gha.add_to_output('var1', 'val3')
gha.add_to_output('var2', 'val4')

# if there is no env file, the output is set via command
with gh_action_command_test(self, '::set-output name=varname::varval') as gha:
gha.add_to_output('varname', 'varval')

def test_add_job_summary(self):
with gh_action_env_file_test(self, GithubAction.JOB_SUMMARY_FILE_VAR_NAME, '# markdown') as gha:
gha.add_to_job_summary('# markdown')
Expand Down
6 changes: 3 additions & 3 deletions python/test/test_publisher.py
Expand Up @@ -1223,7 +1223,7 @@ def do_test_publish_check_without_base_stats(self, errors: List[ParseError], ann
# check the json output has been provided
title_errors = '{} parse errors, '.format(len(errors)) if len(errors) > 0 else ''
summary_errors = '{} errors\u2004\u2003'.format(len(errors)) if len(errors) > 0 else ''
gha.set_output.assert_called_once_with(
gha.add_to_output.assert_called_once_with(
'json',
'{'
f'"title": "{title_errors}7 errors, 6 fail, 5 skipped, 4 pass in 3s", '
Expand Down Expand Up @@ -1295,7 +1295,7 @@ def do_test_publish_check_with_base_stats(self, errors: List[ParseError]):
# check the json output has been provided
title_errors = '{} parse errors, '.format(len(errors)) if len(errors) > 0 else ''
summary_errors = '{} errors\u2004\u2003'.format(len(errors)) if len(errors) > 0 else ''
gha.set_output.assert_called_once_with(
gha.add_to_output.assert_called_once_with(
'json',
'{'
f'"title": "{title_errors}7 errors, 6 fail, 5 skipped, 4 pass in 3s", '
Expand Down Expand Up @@ -1739,7 +1739,7 @@ def test_publish_json(self):
"reference_type": "type", "reference_commit": "ref"}
}
}
gha.set_output.assert_called_once_with('json', json.dumps(expected, ensure_ascii=False))
gha.add_to_output.assert_called_once_with('json', json.dumps(expected, ensure_ascii=False))

def test_publish_job_summary_without_before(self):
settings = self.create_settings(job_summary=True)
Expand Down

0 comments on commit 2535351

Please sign in to comment.