diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index b3c34b63f..1a6e744a5 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..330eb2059 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(workflow_name: str) -> str: + if workflow_name not in WORKFLOW_TEMPLATE_OPTIONS.keys(): + raise ValueError( + f"Invalid workflow: {workflow_name}. Must be one of {WORKFLOW_TEMPLATE_OPTIONS.keys()}" + ) + return workflow_name + + +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..ef8fef371 --- /dev/null +++ b/exasol/toolbox/util/workflows/templates.py @@ -0,0 +1,22 @@ +from collections.abc import Mapping +from pathlib import Path + +import importlib_resources as resources + +WORKFLOW_TEMPLATES_DIRECTORY = "exasol.toolbox.templates.github.workflows" + + +def get_workflow_templates() -> Mapping[str, Path]: + """ + 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 { + 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") + } + + +WORKFLOW_TEMPLATE_OPTIONS = get_workflow_templates() 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..35ff3c82d 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 @@ -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 + ) 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 diff --git a/test/unit/util/workflows/templates_test.py b/test/unit/util/workflows/templates_test.py new file mode 100644 index 000000000..9f19da78b --- /dev/null +++ b/test/unit/util/workflows/templates_test.py @@ -0,0 +1,31 @@ +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", + "check-release-tag", + "checks", + "ci", + "gh-pages", + "matrix-all", + "matrix-exasol", + "matrix-python", + "merge-gate", + "pr-merge", + "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" + ) 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,