From ff19912cfc65f2809c06bc0c929c93a619212bc2 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 19 Feb 2026 10:39:38 +0100 Subject: [PATCH 1/6] Add get_workflow_templates and use to validate workflow names --- doc/changes/unreleased.md | 1 + .../toolbox/util/workflows/patch_workflow.py | 15 ++++++++++++- exasol/toolbox/util/workflows/templates.py | 21 +++++++++++++++++++ test/unit/util/workflows/templates_test.py | 20 ++++++++++++++++++ test/unit/util/workflows/workflow_test.py | 5 ++--- 5 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 exasol/toolbox/util/workflows/templates.py create mode 100644 test/unit/util/workflows/templates_test.py diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index 442d5000a..725138be5 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -7,6 +7,7 @@ * #691: Started customization of PTB workflows by defining the YML schema * #712: Added basic logging to workflow processing * #714: Added logic to modify a workflow using the .workflow-patcher.yml +* #717: Restricted workflow names in .workflow-patcher.yml to template workflow names ## Documentation diff --git a/exasol/toolbox/util/workflows/patch_workflow.py b/exasol/toolbox/util/workflows/patch_workflow.py index 180ab4274..d3e18ab26 100644 --- a/exasol/toolbox/util/workflows/patch_workflow.py +++ b/exasol/toolbox/util/workflows/patch_workflow.py @@ -7,6 +7,7 @@ ) from pydantic import ( + AfterValidator, BaseModel, ConfigDict, Field, @@ -16,6 +17,7 @@ from exasol.toolbox.util.workflows.exceptions import InvalidWorkflowPatcherYamlError from exasol.toolbox.util.workflows.render_yaml import YamlRenderer +from exasol.toolbox.util.workflows.templates import WORKFLOW_TEMPLATE_OPTIONS class ActionType(str, Enum): @@ -64,6 +66,17 @@ class StepCustomization(BaseModel): content: list[StepContent] +def validate_workflow_name(v: str) -> str: + if v not in WORKFLOW_TEMPLATE_OPTIONS.keys(): + raise ValueError( + f"Invalid workflow: {v}. Must be one of {WORKFLOW_TEMPLATE_OPTIONS.keys()}" + ) + return v + + +WorkflowName = Annotated[str, AfterValidator(validate_workflow_name)] + + class Workflow(BaseModel): """ The :class:`Workflow` is used to specify which workflow should be modified. @@ -73,7 +86,7 @@ class Workflow(BaseModel): should be modified. """ - name: str + name: WorkflowName remove_jobs: list[str] = Field(default_factory=list) step_customizations: list[StepCustomization] = Field(default_factory=list) diff --git a/exasol/toolbox/util/workflows/templates.py b/exasol/toolbox/util/workflows/templates.py new file mode 100644 index 000000000..c23dbe1ab --- /dev/null +++ b/exasol/toolbox/util/workflows/templates.py @@ -0,0 +1,21 @@ +from collections.abc import Mapping +from importlib.abc import Traversable + +import importlib_resources as resources + +WORKFLOW_TEMPLATES_DIRECTORY = "exasol.toolbox.templates.github.workflows" + + +def get_workflow_templates() -> Mapping[str, Traversable]: + """ + Returns a mapping where keys are filenames without the '.yml' extension. + """ + package_resources = resources.files(WORKFLOW_TEMPLATES_DIRECTORY) + return { + workflow_path.name.removesuffix(".yml"): workflow_path + for workflow_path in package_resources.iterdir() + if workflow_path.is_file() and workflow_path.name.endswith(".yml") + } + + +WORKFLOW_TEMPLATE_OPTIONS = get_workflow_templates() diff --git a/test/unit/util/workflows/templates_test.py b/test/unit/util/workflows/templates_test.py new file mode 100644 index 000000000..1c114b001 --- /dev/null +++ b/test/unit/util/workflows/templates_test.py @@ -0,0 +1,20 @@ +from exasol.toolbox.util.workflows.templates import get_workflow_templates + + +def test_get_workflow_templates(): + result = get_workflow_templates() + assert result.keys() == { + "build-and-publish", + "cd", + "check-release-tag", + "checks", + "ci", + "gh-pages", + "matrix-all", + "matrix-exasol", + "matrix-python", + "merge-gate", + "pr-merge", + "report", + "slow-checks", + } diff --git a/test/unit/util/workflows/workflow_test.py b/test/unit/util/workflows/workflow_test.py index 69d9e0d49..c12f7ab7e 100644 --- a/test/unit/util/workflows/workflow_test.py +++ b/test/unit/util/workflows/workflow_test.py @@ -8,15 +8,14 @@ YamlParsingError, ) from exasol.toolbox.util.workflows.process_template import WorkflowRenderer +from exasol.toolbox.util.workflows.templates import WORKFLOW_TEMPLATE_OPTIONS from exasol.toolbox.util.workflows.workflow import Workflow from noxconfig import PROJECT_CONFIG -TEMPLATE_DIR = PROJECT_CONFIG.source_code_path / "templates" / "github" / "workflows" - class TestWorkflow: @staticmethod - @pytest.mark.parametrize("template_path", list(TEMPLATE_DIR.glob("*.yml"))) + @pytest.mark.parametrize("template_path", WORKFLOW_TEMPLATE_OPTIONS.values()) def test_works_for_all_templates(template_path): Workflow.load_from_template( file_path=template_path, From 835ab31991c436ddc9a0aae6dcf5cc87aa8fff3f Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 19 Feb 2026 11:04:10 +0100 Subject: [PATCH 2/6] Adapt tests based on new standard --- test/unit/util/workflows/conftest.py | 4 ++-- test/unit/util/workflows/patch_workflow_test.py | 6 +++--- test/unit/util/workflows/process_template_test.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/unit/util/workflows/conftest.py b/test/unit/util/workflows/conftest.py index f2ad2a416..217ba009b 100644 --- a/test/unit/util/workflows/conftest.py +++ b/test/unit/util/workflows/conftest.py @@ -12,13 +12,13 @@ class ExamplePatcherYaml: remove_jobs = """ workflows: - - name: "checks.yml" + - name: "checks" remove_jobs: - build-documentation-and-check-links """ step_customization = """ workflows: - - name: "checks.yml" + - name: "checks" step_customizations: - action: {action} job: run-unit-tests diff --git a/test/unit/util/workflows/patch_workflow_test.py b/test/unit/util/workflows/patch_workflow_test.py index a3618b792..e057f3dc6 100644 --- a/test/unit/util/workflows/patch_workflow_test.py +++ b/test/unit/util/workflows/patch_workflow_test.py @@ -48,16 +48,16 @@ def test_extract_by_workflow_works_as_expected( ): content = f""" {example_patcher_yaml.remove_jobs} - - name: "pr-merge.yml" + - name: "pr-merge" remove_jobs: - publish-docs """ content = cleandoc(content) workflow_patcher_yaml.write_text(content) - result = workflow_patcher.extract_by_workflow("pr-merge.yml") + result = workflow_patcher.extract_by_workflow("pr-merge") assert result == CommentedMap( - {"name": "pr-merge.yml", "remove_jobs": ["publish-docs"]} + {"name": "pr-merge", "remove_jobs": ["publish-docs"]} ) @staticmethod diff --git a/test/unit/util/workflows/process_template_test.py b/test/unit/util/workflows/process_template_test.py index 76f851add..96780169a 100644 --- a/test/unit/util/workflows/process_template_test.py +++ b/test/unit/util/workflows/process_template_test.py @@ -47,7 +47,7 @@ @pytest.fixture def workflow_name(): - return "checks.yml" + return "checks" @pytest.fixture From d3fc8bd94310a718be43d020cdb00a8efd37bd37 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 19 Feb 2026 11:14:18 +0100 Subject: [PATCH 3/6] Convert to Path --- exasol/toolbox/util/workflows/templates.py | 6 +++--- test/unit/util/workflows/templates_test.py | 11 +++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/exasol/toolbox/util/workflows/templates.py b/exasol/toolbox/util/workflows/templates.py index c23dbe1ab..e49cdd7f4 100644 --- a/exasol/toolbox/util/workflows/templates.py +++ b/exasol/toolbox/util/workflows/templates.py @@ -1,18 +1,18 @@ from collections.abc import Mapping -from importlib.abc import Traversable +from pathlib import Path import importlib_resources as resources WORKFLOW_TEMPLATES_DIRECTORY = "exasol.toolbox.templates.github.workflows" -def get_workflow_templates() -> Mapping[str, Traversable]: +def get_workflow_templates() -> Mapping[str, Path]: """ Returns a mapping where keys are filenames without the '.yml' extension. """ package_resources = resources.files(WORKFLOW_TEMPLATES_DIRECTORY) return { - workflow_path.name.removesuffix(".yml"): workflow_path + workflow_path.name.removesuffix(".yml"): Path(str(workflow_path)) for workflow_path in package_resources.iterdir() if workflow_path.is_file() and workflow_path.name.endswith(".yml") } diff --git a/test/unit/util/workflows/templates_test.py b/test/unit/util/workflows/templates_test.py index 1c114b001..9f19da78b 100644 --- a/test/unit/util/workflows/templates_test.py +++ b/test/unit/util/workflows/templates_test.py @@ -1,8 +1,10 @@ from exasol.toolbox.util.workflows.templates import get_workflow_templates +from noxconfig import PROJECT_CONFIG def test_get_workflow_templates(): result = get_workflow_templates() + assert result.keys() == { "build-and-publish", "cd", @@ -18,3 +20,12 @@ def test_get_workflow_templates(): "report", "slow-checks", } + # check only one path, as all formatted the same by convention + assert ( + result["build-and-publish"] + == PROJECT_CONFIG.source_code_path + / "templates" + / "github" + / "workflows" + / "build-and-publish.yml" + ) From ab3e06086a8f3bb47ffe84182eec80049ef0a987 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 19 Feb 2026 11:24:36 +0100 Subject: [PATCH 4/6] Improve docstring --- exasol/toolbox/util/workflows/templates.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/exasol/toolbox/util/workflows/templates.py b/exasol/toolbox/util/workflows/templates.py index e49cdd7f4..ef8fef371 100644 --- a/exasol/toolbox/util/workflows/templates.py +++ b/exasol/toolbox/util/workflows/templates.py @@ -8,7 +8,8 @@ def get_workflow_templates() -> Mapping[str, Path]: """ - Returns a mapping where keys are filenames without the '.yml' extension. + Returns a mapping for workflow templates, where the keys are filenames without the + '.yml' extension and the values are the filepaths. """ package_resources = resources.files(WORKFLOW_TEMPLATES_DIRECTORY) return { From 989759162d6483bf7b12d8934ac92bfed43b460f Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 19 Feb 2026 12:00:24 +0100 Subject: [PATCH 5/6] Add test to verify raises exception when not correctly set --- .../util/workflows/patch_workflow_test.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/unit/util/workflows/patch_workflow_test.py b/test/unit/util/workflows/patch_workflow_test.py index e057f3dc6..35ff3c82d 100644 --- a/test/unit/util/workflows/patch_workflow_test.py +++ b/test/unit/util/workflows/patch_workflow_test.py @@ -101,3 +101,30 @@ def test_raises_error_for_unknown_action( underlying_error = ex.value.__cause__ assert isinstance(underlying_error, ValidationError) + assert "Input should be 'INSERT_AFTER' or 'REPLACE'" in str(underlying_error) + + +class TestWorkflow: + @staticmethod + def test_raises_error_for_unknown_workflow_name( + workflow_patcher_yaml, workflow_patcher + ): + content = """ + workflows: + - name: "unknown-workflow" + remove_jobs: + - build-documentation-and-check-links + """ + workflow_patcher_yaml.write_text(cleandoc(content)) + + with pytest.raises( + InvalidWorkflowPatcherYamlError, + match="is malformed; it failed Pydantic validation", + ) as ex: + workflow_patcher.content + + underlying_error = ex.value.__cause__ + assert isinstance(underlying_error, ValidationError) + assert "Invalid workflow: unknown-workflow. Must be one of dict_keys([" in str( + underlying_error + ) From 88fa57fd5ea54735e3b2579954e2a996633eadfb Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Thu, 19 Feb 2026 12:03:03 +0100 Subject: [PATCH 6/6] Rename variable to be clearer and not follow unneeded pydantic convention --- exasol/toolbox/util/workflows/patch_workflow.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/exasol/toolbox/util/workflows/patch_workflow.py b/exasol/toolbox/util/workflows/patch_workflow.py index d3e18ab26..330eb2059 100644 --- a/exasol/toolbox/util/workflows/patch_workflow.py +++ b/exasol/toolbox/util/workflows/patch_workflow.py @@ -66,12 +66,12 @@ class StepCustomization(BaseModel): content: list[StepContent] -def validate_workflow_name(v: str) -> str: - if v not in WORKFLOW_TEMPLATE_OPTIONS.keys(): +def validate_workflow_name(workflow_name: str) -> str: + if workflow_name not in WORKFLOW_TEMPLATE_OPTIONS.keys(): raise ValueError( - f"Invalid workflow: {v}. Must be one of {WORKFLOW_TEMPLATE_OPTIONS.keys()}" + f"Invalid workflow: {workflow_name}. Must be one of {WORKFLOW_TEMPLATE_OPTIONS.keys()}" ) - return v + return workflow_name WorkflowName = Annotated[str, AfterValidator(validate_workflow_name)]