From ddc7fd7afa8e8a87095982b4648d145257e06c76 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Wed, 11 Feb 2026 12:40:15 +0100 Subject: [PATCH 1/5] Switch StepCustomization.content to list[StepContent] --- exasol/toolbox/util/workflows/patch_workflow.py | 2 +- test/unit/util/workflows/patch_workflow_test.py | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/exasol/toolbox/util/workflows/patch_workflow.py b/exasol/toolbox/util/workflows/patch_workflow.py index c4bc10d5a..9188e94c7 100644 --- a/exasol/toolbox/util/workflows/patch_workflow.py +++ b/exasol/toolbox/util/workflows/patch_workflow.py @@ -32,7 +32,7 @@ class StepCustomization(BaseModel): action: ActionType job: str step_id: str - content: StepContent + content: list[StepContent] class Workflow(BaseModel): diff --git a/test/unit/util/workflows/patch_workflow_test.py b/test/unit/util/workflows/patch_workflow_test.py index aab1acd76..dc6d98151 100644 --- a/test/unit/util/workflows/patch_workflow_test.py +++ b/test/unit/util/workflows/patch_workflow_test.py @@ -28,11 +28,11 @@ class ExampleYaml: job: Tests step_id: checkout-repo content: - name: SCM Checkout - id: checkout-repo - uses: actions/checkout@v6 - with: - fetch-depth: 0 + - name: SCM Checkout + id: checkout-repo + uses: actions/checkout@v6 + with: + fetch-depth: 0 """ From 866d0a36d4f984d974b325acd396632befc84dda Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Wed, 11 Feb 2026 12:55:32 +0100 Subject: [PATCH 2/5] Add docstrings to classes --- .../toolbox/util/workflows/patch_workflow.py | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/exasol/toolbox/util/workflows/patch_workflow.py b/exasol/toolbox/util/workflows/patch_workflow.py index 9188e94c7..9b48fac6e 100644 --- a/exasol/toolbox/util/workflows/patch_workflow.py +++ b/exasol/toolbox/util/workflows/patch_workflow.py @@ -18,6 +18,20 @@ class ActionType(str, Enum): class StepContent(BaseModel): + """ + The :class:`StepContent` is used to lightly validate the content which + would be used to REPLACE or INSERT_AFTER the specified step in the GitHub workflow. + + With the value `ConfigDict(extra="allow")`, this model allows for further fields + (e.g. `dummy`) to be specified without any validation. This design choice was + intentional, as GitHub already allows additional fields and may specify more fields + than what has been specified in this model. + + As the validation here is light, it is left to GitHub to validate the content. + For further information on what is allowed & expected for the fields, refer to + `GitHub's documentation on jobs..steps `__. + """ + model_config = ConfigDict(extra="allow") # This allows extra fields name: str @@ -29,6 +43,15 @@ class StepContent(BaseModel): class StepCustomization(BaseModel): + """ + The :class:`StepCustomization` is used to specify the desired modification: + * REPLACE - means that the contents of the specified `step_id` should be replaced + with whatever `content` is provided. + * INSERT_AFTER - means that the specified `content` should be inserted after + the specified `step_id`. + For a given step + """ + action: ActionType job: str step_id: str @@ -36,12 +59,26 @@ class StepCustomization(BaseModel): class Workflow(BaseModel): + """ + The :class:`Workflow` is used to specify which workflow should be modified. + This is determined by the workflow `name`. A workflow can be modified by specifying: + * `remove_jobs` - job names in this list will be removed from the workflow. + * `step_customization` - items in this list indicate which job's step + should be modified. + """ + name: str remove_jobs: list[str] = Field(default_factory=list) step_customizations: list[StepCustomization] = Field(default_factory=list) class WorkflowPatcherConfig(BaseModel): + """ + The :class:`WorkflowPatcherConfig` is used to validate the expected format for + the `.workflow-patcher.yml`, which is used to modify the workflow templates provided + by the PTB. + """ + workflows: list[Workflow] From 7d2333835052c6581ad0b8aa8ac50d25112bbd39 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Wed, 11 Feb 2026 13:46:36 +0100 Subject: [PATCH 3/5] Fix security concern by removing user input --- exasol/toolbox/util/release/cookiecutter.py | 9 ++-- noxconfig.py | 15 +++---- test/unit/util/release/cookiecutter_test.py | 46 +++++++++++---------- 3 files changed, 37 insertions(+), 33 deletions(-) diff --git a/exasol/toolbox/util/release/cookiecutter.py b/exasol/toolbox/util/release/cookiecutter.py index 82b2a89bb..67b70d6af 100644 --- a/exasol/toolbox/util/release/cookiecutter.py +++ b/exasol/toolbox/util/release/cookiecutter.py @@ -6,12 +6,15 @@ from exasol.toolbox.util.version import Version +PROJECT_ROOT = Path(__file__).resolve().parents[4] +COOKIECUTTER_JSON = PROJECT_ROOT / "project-template" / "cookiecutter.json" -def update_cookiecutter_default(cookiecutter_json: Path, version: Version) -> None: - contents = cookiecutter_json.read_text() + +def update_cookiecutter_default(version: Version) -> None: + contents = COOKIECUTTER_JSON.read_text() contents_as_dict = loads(contents) contents_as_dict["exasol_toolbox_version_range"] = f">={version},<{version.major+1}" updated_contents = dumps(contents_as_dict, indent=2) - cookiecutter_json.write_text(updated_contents) + COOKIECUTTER_JSON.write_text(updated_contents) diff --git a/noxconfig.py b/noxconfig.py index 8f20383e3..74e1ce021 100644 --- a/noxconfig.py +++ b/noxconfig.py @@ -9,7 +9,10 @@ from exasol.toolbox.config import BaseConfig from exasol.toolbox.nox.plugin import hookimpl from exasol.toolbox.tools.replace_version import update_github_yml -from exasol.toolbox.util.release.cookiecutter import update_cookiecutter_default +from exasol.toolbox.util.release.cookiecutter import ( + COOKIECUTTER_JSON, + update_cookiecutter_default, +) from exasol.toolbox.util.version import Version @@ -33,10 +36,6 @@ def github_actions(self) -> list[Path]: gh_actions = self.PARENT_PATH / ".github" / "actions" return [f for f in gh_actions.rglob("*") if f.is_file()] - @property - def cookiecutter_json(self) -> Path: - return self.PARENT_PATH / "project-template" / "cookiecutter.json" - @hookimpl def prepare_release_update_version(self, session, config, version: Version) -> None: for workflow in self.github_template_workflows: @@ -45,14 +44,12 @@ def prepare_release_update_version(self, session, config, version: Version) -> N for action in self.github_actions: update_github_yml(action, version) - update_cookiecutter_default(self.cookiecutter_json, version) + update_cookiecutter_default(version) @hookimpl def prepare_release_add_files(self, session, config) -> list[Path]: return ( - self.github_template_workflows - + self.github_actions - + [self.cookiecutter_json] + self.github_template_workflows + self.github_actions + [COOKIECUTTER_JSON] ) diff --git a/test/unit/util/release/cookiecutter_test.py b/test/unit/util/release/cookiecutter_test.py index e363d20a6..a742a591e 100644 --- a/test/unit/util/release/cookiecutter_test.py +++ b/test/unit/util/release/cookiecutter_test.py @@ -1,6 +1,7 @@ from inspect import cleandoc from json import loads from pathlib import Path +from unittest.mock import patch import pytest @@ -12,26 +13,26 @@ def cookiecutter_json(tmp_path: Path) -> Path: cookiecutter_json = tmp_path / "cookiecutter.json" contents = """ - { - "project_name": "Yet Another Project", - "repo_name": "{{cookiecutter.project_name | lower | replace(' ', '-')}}", - "package_name": "{{cookiecutter.repo_name | replace('-', '_')}}", - "pypi_package_name": "exasol-{{cookiecutter.repo_name}}", - "import_package": "exasol.{{cookiecutter.package_name}}", - "description": "", - "author_full_name": "Exasol AG", - "author_email": "opensource@exasol.com", - "project_short_tag": "", - "python_version_min": "3.10", - "exasol_toolbox_version_range": ">=4.0.1,<5", - "license_year": "{% now 'utc', '%Y' %}", - "__repo_name_slug": "{{cookiecutter.package_name}}", - "__package_name_slug": "{{cookiecutter.package_name}}", - "_extensions": [ - "cookiecutter.extensions.TimeExtension" - ] - } - """ + { + "project_name": "Yet Another Project", + "repo_name": "{{cookiecutter.project_name | lower | replace(' ', '-')}}", + "package_name": "{{cookiecutter.repo_name | replace('-', '_')}}", + "pypi_package_name": "exasol-{{cookiecutter.repo_name}}", + "import_package": "exasol.{{cookiecutter.package_name}}", + "description": "", + "author_full_name": "Exasol AG", + "author_email": "opensource@exasol.com", + "project_short_tag": "", + "python_version_min": "3.10", + "exasol_toolbox_version_range": ">=4.0.1,<5", + "license_year": "{% now 'utc', '%Y' %}", + "__repo_name_slug": "{{cookiecutter.package_name}}", + "__package_name_slug": "{{cookiecutter.package_name}}", + "_extensions": [ + "cookiecutter.extensions.TimeExtension" + ] + } + """ cookiecutter_json.write_text(cleandoc(contents)) return cookiecutter_json @@ -46,7 +47,10 @@ def cookiecutter_json(tmp_path: Path) -> Path: def test_update_cookiecutter_default( cookiecutter_json, version: Version, expected: str ): - update_cookiecutter_default(cookiecutter_json=cookiecutter_json, version=version) + with patch( + "exasol.toolbox.util.release.cookiecutter.COOKIECUTTER_JSON", cookiecutter_json + ): + update_cookiecutter_default(version=version) updated_json = cookiecutter_json.read_text() updated_dict = loads(updated_json) From d4fb942494fc3ed3577579ef97f975720d6d5093 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Wed, 11 Feb 2026 13:55:04 +0100 Subject: [PATCH 4/5] Add changelog entry --- doc/changes/unreleased.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index 4dfeba0af..29f939fba 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -10,3 +10,4 @@ * #664: Removed deprecation warning for projects to switch over to BaseConfig * #637: Added id to workflow templates & synchronized on naming conventions +* #702: Fixed StepCustomization.content to list[StepContent] and security concern for `update_cookiecutter_default` From 850d31fcbba6ca0e4797089951255297490842e3 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Wed, 11 Feb 2026 13:58:23 +0100 Subject: [PATCH 5/5] Undo formatting issue --- test/unit/util/release/cookiecutter_test.py | 40 ++++++++++----------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/test/unit/util/release/cookiecutter_test.py b/test/unit/util/release/cookiecutter_test.py index a742a591e..ed6dee49a 100644 --- a/test/unit/util/release/cookiecutter_test.py +++ b/test/unit/util/release/cookiecutter_test.py @@ -13,26 +13,26 @@ def cookiecutter_json(tmp_path: Path) -> Path: cookiecutter_json = tmp_path / "cookiecutter.json" contents = """ - { - "project_name": "Yet Another Project", - "repo_name": "{{cookiecutter.project_name | lower | replace(' ', '-')}}", - "package_name": "{{cookiecutter.repo_name | replace('-', '_')}}", - "pypi_package_name": "exasol-{{cookiecutter.repo_name}}", - "import_package": "exasol.{{cookiecutter.package_name}}", - "description": "", - "author_full_name": "Exasol AG", - "author_email": "opensource@exasol.com", - "project_short_tag": "", - "python_version_min": "3.10", - "exasol_toolbox_version_range": ">=4.0.1,<5", - "license_year": "{% now 'utc', '%Y' %}", - "__repo_name_slug": "{{cookiecutter.package_name}}", - "__package_name_slug": "{{cookiecutter.package_name}}", - "_extensions": [ - "cookiecutter.extensions.TimeExtension" - ] - } - """ + { + "project_name": "Yet Another Project", + "repo_name": "{{cookiecutter.project_name | lower | replace(' ', '-')}}", + "package_name": "{{cookiecutter.repo_name | replace('-', '_')}}", + "pypi_package_name": "exasol-{{cookiecutter.repo_name}}", + "import_package": "exasol.{{cookiecutter.package_name}}", + "description": "", + "author_full_name": "Exasol AG", + "author_email": "opensource@exasol.com", + "project_short_tag": "", + "python_version_min": "3.10", + "exasol_toolbox_version_range": ">=4.0.1,<5", + "license_year": "{% now 'utc', '%Y' %}", + "__repo_name_slug": "{{cookiecutter.package_name}}", + "__package_name_slug": "{{cookiecutter.package_name}}", + "_extensions": [ + "cookiecutter.extensions.TimeExtension" + ] + } + """ cookiecutter_json.write_text(cleandoc(contents)) return cookiecutter_json