Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use GHA env file to set output #360

Merged
merged 3 commits into from Oct 14, 2022
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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