diff --git a/python/publish/github_action.py b/python/publish/github_action.py index b98d9e12..5ae1764b 100644 --- a/python/publish/github_action.py +++ b/python/publish/github_action.py @@ -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): @@ -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) @@ -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) @@ -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 @@ -134,17 +152,28 @@ 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: @@ -152,3 +181,6 @@ def _append_to_file(self, content: str, env_file_var_name: str): 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 diff --git a/python/publish/publisher.py b/python/publish/publisher.py index 2618a51f..08e1c730 100644 --- a/python/publish/publisher.py +++ b/python/publish/publisher.py @@ -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, diff --git a/python/publish_test_results.py b/python/publish_test_results.py index 2e89ec52..15901d71 100644 --- a/python/publish_test_results.py +++ b/python/publish_test_results.py @@ -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) diff --git a/python/test/test_github_action.py b/python/test/test_github_action.py index e41f88f2..d8bb7d0a 100644 --- a/python/test/test_github_action.py +++ b/python/test/test_github_action.py @@ -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: @@ -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') @@ -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') @@ -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') @@ -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') diff --git a/python/test/test_publisher.py b/python/test/test_publisher.py index 8557a88d..35df2606 100644 --- a/python/test/test_publisher.py +++ b/python/test/test_publisher.py @@ -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", ' @@ -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", ' @@ -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)