From 2f96e73b806b9d3ae77dab0998cc1777c84c09db Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 09:45:29 +0200 Subject: [PATCH 1/5] Add compare_to_file --- exasol/toolbox/util/workflows/workflow.py | 14 ++++++++++++ test/unit/util/workflows/workflow_test.py | 27 +++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/exasol/toolbox/util/workflows/workflow.py b/exasol/toolbox/util/workflows/workflow.py index 086414f7d..bc9d16299 100644 --- a/exasol/toolbox/util/workflows/workflow.py +++ b/exasol/toolbox/util/workflows/workflow.py @@ -1,3 +1,4 @@ +import difflib from collections.abc import Mapping from pathlib import Path from typing import ( @@ -70,6 +71,19 @@ def load_from_template( # Wrap all other "non-special" exceptions raise ValueError(f"Error rendering file: {file_path}") from ex + def compare_to_file(self, file_path: Path) -> str: + existing_content = file_path.read_text().strip() + generated_content = self.content.strip() + + diff = difflib.unified_diff( + existing_content.splitlines(), + generated_content.splitlines(), + fromfile=f"existing: {file_path.name}", + tofile="generated", + lineterm="", + ) + return "\n".join(diff) + def write_to_file(self, file_path: Path) -> None: logger.info("Write workflow file %s", file_path.name) file_path.write_text(self.content + "\n") diff --git a/test/unit/util/workflows/workflow_test.py b/test/unit/util/workflows/workflow_test.py index ea6a308b2..cc270632c 100644 --- a/test/unit/util/workflows/workflow_test.py +++ b/test/unit/util/workflows/workflow_test.py @@ -55,6 +55,33 @@ def test_works_as_expected(tmp_path, project_config): assert output_file_path.read_text() == cleandoc(expected_yaml) + "\n" + @staticmethod + def test_compare_to_file_accepts_matching_content(tmp_path): + content = "line 1\nline 2" + file_path = tmp_path / "workflow.yml" + file_path.write_text(f"\n{content}\n") + + workflow = Workflow(content=f"\n{content}\n") + + assert workflow.compare_to_file(file_path=file_path) == "" + + @staticmethod + def test_compare_to_file_reports_diff(tmp_path): + workflow = Workflow(content="line 1\nline 2") + file_path = tmp_path / "workflow.yml" + file_path.write_text("line 1\nline 3\n") + + diff = workflow.compare_to_file(file_path=file_path) + + assert diff == ( + f"--- existing: {file_path.name}\n" + "+++ generated\n" + "@@ -1,2 +1,2 @@\n" + " line 1\n" + "-line 3\n" + "+line 2" + ) + @staticmethod @pytest.mark.parametrize("template_path", WORKFLOW_TEMPLATE_OPTIONS.values()) def test_works_for_all_templates(tmp_path, project_config, template_path): From 1e91aff88d8bb317596eaff211318825df42bfe1 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 09:55:13 +0200 Subject: [PATCH 2/5] Skip writing file if identical --- exasol/toolbox/util/workflows/workflow.py | 5 ++++- test/unit/util/workflows/workflow_test.py | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/exasol/toolbox/util/workflows/workflow.py b/exasol/toolbox/util/workflows/workflow.py index bc9d16299..33460e4b6 100644 --- a/exasol/toolbox/util/workflows/workflow.py +++ b/exasol/toolbox/util/workflows/workflow.py @@ -72,7 +72,7 @@ def load_from_template( raise ValueError(f"Error rendering file: {file_path}") from ex def compare_to_file(self, file_path: Path) -> str: - existing_content = file_path.read_text().strip() + existing_content = file_path.read_text().strip() if file_path.exists() else "" generated_content = self.content.strip() diff = difflib.unified_diff( @@ -85,6 +85,9 @@ def compare_to_file(self, file_path: Path) -> str: return "\n".join(diff) def write_to_file(self, file_path: Path) -> None: + if self.compare_to_file(file_path=file_path) == "": + logger.debug("Skip up-to-date workflow file %s", file_path.name) + return logger.info("Write workflow file %s", file_path.name) file_path.write_text(self.content + "\n") diff --git a/test/unit/util/workflows/workflow_test.py b/test/unit/util/workflows/workflow_test.py index cc270632c..f34b65056 100644 --- a/test/unit/util/workflows/workflow_test.py +++ b/test/unit/util/workflows/workflow_test.py @@ -1,4 +1,5 @@ from inspect import cleandoc +from pathlib import Path from unittest.mock import patch import pytest @@ -82,6 +83,18 @@ def test_compare_to_file_reports_diff(tmp_path): "+line 2" ) + @staticmethod + def test_write_to_file_skips_up_to_date_file(tmp_path): + file_path = tmp_path / "workflow.yml" + file_path.write_text("line 1\nline 2\n") + workflow = Workflow(content="line 1\nline 2") + + with patch.object(Path, "write_text") as write_text: + workflow.write_to_file(file_path=file_path) + + write_text.assert_not_called() + assert file_path.read_text() == "line 1\nline 2\n" + @staticmethod @pytest.mark.parametrize("template_path", WORKFLOW_TEMPLATE_OPTIONS.values()) def test_works_for_all_templates(tmp_path, project_config, template_path): From 3501d1711784b79d04738432bb4af5773cb9166e Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 10:05:25 +0200 Subject: [PATCH 3/5] Switch file_path to be more clearly template_path --- exasol/toolbox/util/workflows/workflow.py | 16 ++++++++-------- test/unit/util/workflows/workflow_test.py | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/exasol/toolbox/util/workflows/workflow.py b/exasol/toolbox/util/workflows/workflow.py index 33460e4b6..28e3bed95 100644 --- a/exasol/toolbox/util/workflows/workflow.py +++ b/exasol/toolbox/util/workflows/workflow.py @@ -47,20 +47,20 @@ class Workflow(BaseModel): @classmethod def load_from_template( cls, - file_path: Path, + template_path: Path, github_template_dict: dict[str, Any], patch_yaml: WorkflowCommentedMap | None = None, ): - with bound_contextvars(template_file_name=file_path.name): - logger.debug("Load workflow template: %s", file_path.name) + with bound_contextvars(template_file_name=template_path.name): + logger.debug("Load workflow template: %s", template_path.name) - if not file_path.exists(): - raise FileNotFoundError(file_path) + if not template_path.exists(): + raise FileNotFoundError(template_path) try: workflow_renderer = WorkflowRenderer( github_template_dict=github_template_dict, - file_path=file_path, + file_path=template_path, patch_yaml=patch_yaml, ) workflow = workflow_renderer.render() @@ -69,7 +69,7 @@ def load_from_template( raise ex except Exception as ex: # Wrap all other "non-special" exceptions - raise ValueError(f"Error rendering file: {file_path}") from ex + raise ValueError(f"Error rendering file: {template_path}") from ex def compare_to_file(self, file_path: Path) -> str: existing_content = file_path.read_text().strip() if file_path.exists() else "" @@ -136,7 +136,7 @@ def update_workflow(workflow_choice: WorkflowChoice, config: BaseConfig) -> None try: workflow = Workflow.load_from_template( - file_path=workflow_dict[workflow_name], + template_path=workflow_dict[workflow_name], github_template_dict=config.github_template_dict, patch_yaml=patch_yaml, ) diff --git a/test/unit/util/workflows/workflow_test.py b/test/unit/util/workflows/workflow_test.py index f34b65056..c139c96b4 100644 --- a/test/unit/util/workflows/workflow_test.py +++ b/test/unit/util/workflows/workflow_test.py @@ -48,7 +48,7 @@ def test_works_as_expected(tmp_path, project_config): input_file_path.write_text(content) workflow = Workflow.load_from_template( - file_path=input_file_path, + template_path=input_file_path, github_template_dict=project_config.github_template_dict, ) output_file_path = tmp_path / f"{input_file_path.name}" @@ -99,7 +99,7 @@ def test_write_to_file_skips_up_to_date_file(tmp_path): @pytest.mark.parametrize("template_path", WORKFLOW_TEMPLATE_OPTIONS.values()) def test_works_for_all_templates(tmp_path, project_config, template_path): workflow = Workflow.load_from_template( - file_path=template_path, + template_path=template_path, github_template_dict=project_config.github_template_dict, ) file_path = tmp_path / f"{template_path.name}" @@ -112,7 +112,7 @@ def test_fails_when_yaml_does_not_exist(tmp_path, project_config): file_path = tmp_path / "test.yaml" with pytest.raises(FileNotFoundError, match="test.yaml"): Workflow.load_from_template( - file_path=file_path, + template_path=file_path, github_template_dict=project_config.github_template_dict, ) @@ -129,7 +129,7 @@ def test_raises_custom_exceptions(tmp_path, project_config, raised_exc): ): with pytest.raises(raised_exc): Workflow.load_from_template( - file_path=file_path, + template_path=file_path, github_template_dict=project_config.github_template_dict, ) @@ -143,7 +143,7 @@ def test_other_exceptions_raised_as_valuerror(tmp_path, project_config): ): with pytest.raises(ValueError): Workflow.load_from_template( - file_path=file_path, + template_path=file_path, github_template_dict=project_config.github_template_dict, ) From 1f7f40fc0b0d6d704384dde361ea1091c183fdfc Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 10:12:34 +0200 Subject: [PATCH 4/5] Make template_path and output_path kept in the Workflow class --- exasol/toolbox/util/workflows/workflow.py | 32 +++++++++++++-------- test/unit/util/workflows/workflow_test.py | 35 +++++++++++++++++------ 2 files changed, 47 insertions(+), 20 deletions(-) diff --git a/exasol/toolbox/util/workflows/workflow.py b/exasol/toolbox/util/workflows/workflow.py index 28e3bed95..ba08d14a5 100644 --- a/exasol/toolbox/util/workflows/workflow.py +++ b/exasol/toolbox/util/workflows/workflow.py @@ -42,12 +42,15 @@ class Workflow(BaseModel): model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) + template_path: Path + output_path: Path content: str @classmethod def load_from_template( cls, template_path: Path, + output_directory: Path, github_template_dict: dict[str, Any], patch_yaml: WorkflowCommentedMap | None = None, ): @@ -63,33 +66,38 @@ def load_from_template( file_path=template_path, patch_yaml=patch_yaml, ) - workflow = workflow_renderer.render() - return cls(content=workflow) + return cls( + template_path=template_path, + output_path=output_directory / template_path.name, + content=workflow_renderer.render(), + ) except (YamlError, YamlKeyError) as ex: raise ex except Exception as ex: # Wrap all other "non-special" exceptions raise ValueError(f"Error rendering file: {template_path}") from ex - def compare_to_file(self, file_path: Path) -> str: - existing_content = file_path.read_text().strip() if file_path.exists() else "" + def compare_to_file(self) -> str: + existing_content = ( + self.output_path.read_text().strip() if self.output_path.exists() else "" + ) generated_content = self.content.strip() diff = difflib.unified_diff( existing_content.splitlines(), generated_content.splitlines(), - fromfile=f"existing: {file_path.name}", + fromfile=f"existing: {self.output_path.name}", tofile="generated", lineterm="", ) return "\n".join(diff) - def write_to_file(self, file_path: Path) -> None: - if self.compare_to_file(file_path=file_path) == "": - logger.debug("Skip up-to-date workflow file %s", file_path.name) + def write_to_file(self) -> None: + if self.compare_to_file() == "": + logger.debug("Skip up-to-date workflow file %s", self.output_path.name) return - logger.info("Write workflow file %s", file_path.name) - file_path.write_text(self.content + "\n") + logger.info("Write workflow file %s", self.output_path.name) + self.output_path.write_text(self.content + "\n") def _select_workflow_template(workflow_name: WorkflowChoice) -> Mapping[str, Path]: @@ -137,11 +145,11 @@ def update_workflow(workflow_choice: WorkflowChoice, config: BaseConfig) -> None try: workflow = Workflow.load_from_template( template_path=workflow_dict[workflow_name], + output_directory=config.github_workflow_directory, github_template_dict=config.github_template_dict, patch_yaml=patch_yaml, ) - file_path = config.github_workflow_directory / f"{workflow_name}.yml" - workflow.write_to_file(file_path=file_path) + workflow.write_to_file() except YamlKeyError as ex: raise InvalidWorkflowPatcherEntryError( file_path=config.github_workflow_patcher_yaml, entry=ex.entry # type: ignore diff --git a/test/unit/util/workflows/workflow_test.py b/test/unit/util/workflows/workflow_test.py index c139c96b4..d17790bca 100644 --- a/test/unit/util/workflows/workflow_test.py +++ b/test/unit/util/workflows/workflow_test.py @@ -49,10 +49,13 @@ def test_works_as_expected(tmp_path, project_config): workflow = Workflow.load_from_template( template_path=input_file_path, + output_directory=tmp_path, github_template_dict=project_config.github_template_dict, ) output_file_path = tmp_path / f"{input_file_path.name}" - workflow.write_to_file(file_path=output_file_path) + assert workflow.template_path == input_file_path + assert workflow.output_path == output_file_path + workflow.write_to_file() assert output_file_path.read_text() == cleandoc(expected_yaml) + "\n" @@ -62,17 +65,25 @@ def test_compare_to_file_accepts_matching_content(tmp_path): file_path = tmp_path / "workflow.yml" file_path.write_text(f"\n{content}\n") - workflow = Workflow(content=f"\n{content}\n") + workflow = Workflow( + template_path=file_path, + output_path=file_path, + content=f"\n{content}\n", + ) - assert workflow.compare_to_file(file_path=file_path) == "" + assert workflow.compare_to_file() == "" @staticmethod def test_compare_to_file_reports_diff(tmp_path): - workflow = Workflow(content="line 1\nline 2") file_path = tmp_path / "workflow.yml" file_path.write_text("line 1\nline 3\n") + workflow = Workflow( + template_path=file_path, + output_path=file_path, + content="line 1\nline 2", + ) - diff = workflow.compare_to_file(file_path=file_path) + diff = workflow.compare_to_file() assert diff == ( f"--- existing: {file_path.name}\n" @@ -87,10 +98,14 @@ def test_compare_to_file_reports_diff(tmp_path): def test_write_to_file_skips_up_to_date_file(tmp_path): file_path = tmp_path / "workflow.yml" file_path.write_text("line 1\nline 2\n") - workflow = Workflow(content="line 1\nline 2") + workflow = Workflow( + template_path=file_path, + output_path=file_path, + content="line 1\nline 2", + ) with patch.object(Path, "write_text") as write_text: - workflow.write_to_file(file_path=file_path) + workflow.write_to_file() write_text.assert_not_called() assert file_path.read_text() == "line 1\nline 2\n" @@ -100,10 +115,11 @@ def test_write_to_file_skips_up_to_date_file(tmp_path): def test_works_for_all_templates(tmp_path, project_config, template_path): workflow = Workflow.load_from_template( template_path=template_path, + output_directory=tmp_path, github_template_dict=project_config.github_template_dict, ) file_path = tmp_path / f"{template_path.name}" - workflow.write_to_file(file_path=file_path) + workflow.write_to_file() assert file_path.read_text() != "" @@ -113,6 +129,7 @@ def test_fails_when_yaml_does_not_exist(tmp_path, project_config): with pytest.raises(FileNotFoundError, match="test.yaml"): Workflow.load_from_template( template_path=file_path, + output_directory=tmp_path, github_template_dict=project_config.github_template_dict, ) @@ -130,6 +147,7 @@ def test_raises_custom_exceptions(tmp_path, project_config, raised_exc): with pytest.raises(raised_exc): Workflow.load_from_template( template_path=file_path, + output_directory=tmp_path, github_template_dict=project_config.github_template_dict, ) @@ -144,6 +162,7 @@ def test_other_exceptions_raised_as_valuerror(tmp_path, project_config): with pytest.raises(ValueError): Workflow.load_from_template( template_path=file_path, + output_directory=tmp_path, github_template_dict=project_config.github_template_dict, ) From dc4d3a6fff818758f0834a3bd99e46994c80d7ed Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 21 May 2026 10:15:13 +0200 Subject: [PATCH 5/5] Add changelog entry --- doc/changes/unreleased.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index acc91638d..dd51210ed 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -5,3 +5,7 @@ ## Bugfix * #840: Added `export` plugin installation within `dependency-update.yml` + +## Feature + +* #722: Added check in `workflow:generate` to compare the generated and existing content before writing out