Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/changes/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 14 additions & 1 deletion exasol/toolbox/util/workflows/patch_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
)

from pydantic import (
AfterValidator,
BaseModel,
ConfigDict,
Field,
Expand All @@ -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):
Expand Down Expand Up @@ -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.
Expand All @@ -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)

Expand Down
22 changes: 22 additions & 0 deletions exasol/toolbox/util/workflows/templates.py
Original file line number Diff line number Diff line change
@@ -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()
4 changes: 2 additions & 2 deletions test/unit/util/workflows/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 30 additions & 3 deletions test/unit/util/workflows/patch_workflow_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)
2 changes: 1 addition & 1 deletion test/unit/util/workflows/process_template_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@

@pytest.fixture
def workflow_name():
return "checks.yml"
return "checks"


@pytest.fixture
Expand Down
31 changes: 31 additions & 0 deletions test/unit/util/workflows/templates_test.py
Original file line number Diff line number Diff line change
@@ -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"
)
5 changes: 2 additions & 3 deletions test/unit/util/workflows/workflow_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down