From fac18109a9f3d3dca4501f4532e52bb887da9361 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 22 May 2026 08:51:45 +0200 Subject: [PATCH 01/13] Add to .gitignore --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index bfe5173af3..08b168a800 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,8 @@ nosetests.xml # Emacs TAGS + +# AI +.codex +.serena +specs/ From 0e7ed47fc8e1f17709372f1d82f4753998f2cdde Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 22 May 2026 09:14:29 +0200 Subject: [PATCH 02/13] Add workflow:check nox session --- doc/changes/unreleased.md | 2 +- exasol/toolbox/nox/_workflow.py | 31 ++++++++++++++++--- .../util/workflows/workflow_orchestrator.py | 14 +++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index 7ea1b208d3..5cce1871cd 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -8,7 +8,7 @@ ## Feature -* #722: Added check in `workflow:generate` to compare the generated and existing content before writing out +* #722: Added check in `workflow:generate` to compare the generated and existing content before writing out and nox session `workflow:check` ## Refactoring diff --git a/exasol/toolbox/nox/_workflow.py b/exasol/toolbox/nox/_workflow.py index 9508941a87..f73a701b14 100644 --- a/exasol/toolbox/nox/_workflow.py +++ b/exasol/toolbox/nox/_workflow.py @@ -12,10 +12,10 @@ from noxconfig import PROJECT_CONFIG -def _create_parser() -> argparse.ArgumentParser: +def _create_parser(session_name: str) -> argparse.ArgumentParser: parser = argparse.ArgumentParser( - prog="nox -s workflow:generate", - usage="nox -s workflow:generate -- [-h] ", + prog=f"nox -s {session_name}", + usage=f"nox -s {session_name} -- [-h] ", formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) @@ -27,12 +27,35 @@ def _create_parser() -> argparse.ArgumentParser: return parser +@nox.session(name="workflow:check", python=False) +def check_workflow(session: Session) -> None: + """ + Check the specified GitHub workflow or all of them to see if any differ from + the generated values. If any differ, an error is raised. + """ + parser = _create_parser("workflow:check") + args = parser.parse_args(session.posargs) + + PROJECT_CONFIG.github_workflow_directory.mkdir(parents=True, exist_ok=True) + + outdated_workflows = WorkflowOrchestrator( + workflow_choice=args.workflow_choice, + config=PROJECT_CONFIG, + ).find_differing_workflows() + + if outdated_workflows: + count = len(outdated_workflows) + count_label = "workflow is" if count == 1 else "workflows are" + workflow_list = "\n".join(f"- {workflow}" for workflow in outdated_workflows) + session.error(f"\n{count} {count_label} out of date:\n" f"{workflow_list}") + + @nox.session(name="workflow:generate", python=False) def generate_workflow(session: Session) -> None: """ Generate or update the specified GitHub workflow or all of them. """ - parser = _create_parser() + parser = _create_parser("workflow:generate") args = parser.parse_args(session.posargs) PROJECT_CONFIG.github_workflow_directory.mkdir(parents=True, exist_ok=True) diff --git a/exasol/toolbox/util/workflows/workflow_orchestrator.py b/exasol/toolbox/util/workflows/workflow_orchestrator.py index 95977454de..61e0ba6710 100644 --- a/exasol/toolbox/util/workflows/workflow_orchestrator.py +++ b/exasol/toolbox/util/workflows/workflow_orchestrator.py @@ -129,3 +129,17 @@ def generate_workflows(self) -> None: """ for workflow in self._iter_workflows(): workflow.write_to_file() + + def find_differing_workflows(self) -> list[str]: + """ + Find selected workflows that differ from the generated output. + + Returns the names of the workflows that differ from the file on disk. + """ + outdated_workflows: list[str] = [] + for workflow in self._iter_workflows(): + comparison = workflow.compare_to_file() + if comparison != "": + outdated_workflows.append(workflow.output_path.stem) + print(comparison) + return outdated_workflows From 2347c533171e4423592bd8994bba925ff1aa5ca9 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 22 May 2026 09:21:25 +0200 Subject: [PATCH 03/13] Modify documentation --- .../features/github_workflows/create_and_update.rst | 6 ++++++ doc/user_guide/features/github_workflows/index.rst | 7 +++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/doc/user_guide/features/github_workflows/create_and_update.rst b/doc/user_guide/features/github_workflows/create_and_update.rst index 2273d95a5f..b1da0db262 100644 --- a/doc/user_guide/features/github_workflows/create_and_update.rst +++ b/doc/user_guide/features/github_workflows/create_and_update.rst @@ -87,6 +87,12 @@ Add all Workflows to Your Project poetry run -- nox -s workflow:generate -- all +After regenerating the workflows, you can verify that they are up-to-date with: + +.. code-block:: shell + + poetry run -- nox -s workflow:check -- all + .. warning:: Some workflows depend on other workflows. Please ensure you have all the required workflows if you do not install all of them. diff --git a/doc/user_guide/features/github_workflows/index.rst b/doc/user_guide/features/github_workflows/index.rst index 10f7f5b8c3..548e2cab65 100644 --- a/doc/user_guide/features/github_workflows/index.rst +++ b/doc/user_guide/features/github_workflows/index.rst @@ -16,12 +16,15 @@ The PTB ships with configurable GitHub workflow templates covering the most comm CI/CD setup variants for Python projects. The templates are defined in: `exasol/toolbox/templates/github/workflows `__. -The PTB provides a command line interface (CLI) for generating and updating actual -workflows from the templates. +The PTB provides a command line interface (CLI) for managing workflows from the templates. .. code-block:: bash poetry run -- nox -s workflow:generate --help + poetry run -- nox -s workflow:check --help + +Use ``workflow:generate`` to create or update workflows and ``workflow:check`` to +compare the generated workflows against the files in ``.github/workflows``. .. attention:: From 74005d755537fe2ad213ed61ea9bce68df2e7c7b Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 22 May 2026 09:23:37 +0200 Subject: [PATCH 04/13] Add tests --- test/unit/nox/_workflow_test.py | 72 ++++++++++++++++++- .../workflows/workflow_orchestrator_test.py | 43 +++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/test/unit/nox/_workflow_test.py b/test/unit/nox/_workflow_test.py index 75cf445a64..983d162561 100644 --- a/test/unit/nox/_workflow_test.py +++ b/test/unit/nox/_workflow_test.py @@ -1,10 +1,14 @@ from unittest.mock import patch import pytest +from nox.sessions import _SessionQuit from pydantic import computed_field from exasol.toolbox.config import BaseConfig -from exasol.toolbox.nox._workflow import generate_workflow +from exasol.toolbox.nox._workflow import ( + check_workflow, + generate_workflow, +) from exasol.toolbox.util.workflows.templates import WORKFLOW_TEMPLATE_OPTIONS from exasol.toolbox.util.workflows.workflow_orchestrator import ALL @@ -76,3 +80,69 @@ def test_raises_exception_when_name_incorrect( generate_workflow(nox_session) assert "invalid choice: 'not-a-valid-name'" in capsys.readouterr().err + + +class TestCheckWorkflow: + @staticmethod + @pytest.mark.parametrize( + "nox_session_runner_posargs", + [ALL], + indirect=["nox_session_runner_posargs"], + ) + def test_passes_when_workflows_are_up_to_date_after_generation( + nox_session, + project_config_without_patcher, + capsys, + nox_session_runner_posargs, + ): + with patch( + "exasol.toolbox.nox._workflow.PROJECT_CONFIG", + new=project_config_without_patcher, + ): + generate_workflow(nox_session) + capsys.readouterr() + + check_workflow(nox_session) + + assert "--- existing:" not in capsys.readouterr().out + + @staticmethod + @pytest.mark.parametrize( + "nox_session_runner_posargs", + [ALL], + indirect=["nox_session_runner_posargs"], + ) + def test_raises_session_quit_when_workflows_are_out_of_date( + nox_session, + project_config_without_patcher, + nox_session_runner_posargs, + ): + with ( + patch("exasol.toolbox.util.workflows.workflow_orchestrator.logger.info"), + patch( + "exasol.toolbox.nox._workflow.PROJECT_CONFIG", + new=project_config_without_patcher, + ), + ): + with pytest.raises(_SessionQuit) as exc: + check_workflow(nox_session) + + assert str(exc.value) == ( + "\n16 workflows are out of date:\n" + "- build-and-publish\n" + "- cd\n" + "- check-release-tag\n" + "- checks\n" + "- ci\n" + "- dependency-update\n" + "- fast-tests\n" + "- gh-pages\n" + "- matrix-all\n" + "- matrix-exasol\n" + "- matrix-python\n" + "- merge-gate\n" + "- periodic-validation\n" + "- pr-merge\n" + "- report\n" + "- slow-checks" + ) diff --git a/test/unit/util/workflows/workflow_orchestrator_test.py b/test/unit/util/workflows/workflow_orchestrator_test.py index 6a79df6113..8086a3e58a 100644 --- a/test/unit/util/workflows/workflow_orchestrator_test.py +++ b/test/unit/util/workflows/workflow_orchestrator_test.py @@ -265,3 +265,46 @@ def test_overwrites_existing_workflow_file(project_config_without_patcher): config=project_config_without_patcher, ).generate_workflows() assert workflow_path.read_text() != original_content + + +class TestFindDifferingWorkflows: + @staticmethod + def test_returns_empty_list_when_workflow_is_up_to_date( + project_config_without_patcher, capsys + ): + directory = project_config_without_patcher.github_workflow_directory + directory.mkdir(parents=True) + + workflow_name = "merge-gate" + WorkflowOrchestrator( + workflow_choice=workflow_name, + config=project_config_without_patcher, + ).generate_workflows() + capsys.readouterr() + + outdated_workflows = WorkflowOrchestrator( + workflow_choice=workflow_name, + config=project_config_without_patcher, + ).find_differing_workflows() + + assert outdated_workflows == [] + assert "--- existing:" not in capsys.readouterr().out + + @staticmethod + def test_returns_workflow_name_and_prints_diff_when_workflow_differs( + project_config_without_patcher, capsys + ): + directory = project_config_without_patcher.github_workflow_directory + directory.mkdir(parents=True) + + workflow_name = "merge-gate" + workflow_path = directory / f"{workflow_name}.yml" + workflow_path.write_text("line 3\n") + + outdated_workflows = WorkflowOrchestrator( + workflow_choice=workflow_name, + config=project_config_without_patcher, + ).find_differing_workflows() + + assert outdated_workflows == ["merge-gate"] + assert "--- existing: merge-gate.yml" in capsys.readouterr().out From cb2f05d7fb72560d28e9ec0e66f1fa7eba19c468 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 22 May 2026 09:28:58 +0200 Subject: [PATCH 05/13] Add new nox session to checks.yml --- .github/workflows/checks.yml | 22 ++++++++++++++++++- doc/changes/unreleased.md | 4 +++- .../features/github_workflows/index.rst | 4 ++-- .../templates/github/workflows/checks.yml | 22 ++++++++++++++++++- 4 files changed, 47 insertions(+), 5 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 4d9adb6720..e4a5086c5f 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -168,7 +168,6 @@ jobs: id: check-format run: poetry run -- nox -s format:check - build-package: name: Build Package runs-on: "ubuntu-24.04" @@ -189,3 +188,24 @@ jobs: - name: Build Package id: build-package run: poetry run -- nox -s package:check + + check-workflows: + name: Check Workflows + runs-on: "ubuntu-24.04" + permissions: + contents: read + steps: + - name: Check out Repository + id: check-out-repository + uses: actions/checkout@v6 + + - name: Set up Python & Poetry Environment + id: set-up-python-and-poetry-environment + uses: exasol/python-toolbox/.github/actions/python-environment@v8 + with: + python-version: "3.10" + poetry-version: "2.3.0" + + - name: Check Workflows + id: check-workflows + run: poetry run -- nox -s workflow:check -- all diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index 5cce1871cd..2124665145 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -2,13 +2,15 @@ ## Summary +In this minor release, the nox session `workflow:check` was added and is now used in the `checks.yml`. + ## 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 and nox session `workflow:check` +* #722: Added check in `workflow:generate` to compare the generated and existing content before writing out, nox session `workflow:check`, and `workflow:check` into the `checks.yml` ## Refactoring diff --git a/doc/user_guide/features/github_workflows/index.rst b/doc/user_guide/features/github_workflows/index.rst index 548e2cab65..e5d280a6a8 100644 --- a/doc/user_guide/features/github_workflows/index.rst +++ b/doc/user_guide/features/github_workflows/index.rst @@ -65,8 +65,8 @@ Maintained by the PTB * - ``checks.yml`` - Workflow call - Executes many small & fast checks: builds documentation, validates - cross-references and links in the documentation to be valid, and runs various - linters (security, type checks, etc.). + cross-references and links in the documentation to be valid, runs various + linters (security, type checks, etc.), and validates PTB generated workflows. * - ``ci.yml`` - Pull request - Executes the continuous integration suite by calling ``merge-gate.yml`` and diff --git a/exasol/toolbox/templates/github/workflows/checks.yml b/exasol/toolbox/templates/github/workflows/checks.yml index 35a3ef777b..740da6ccce 100644 --- a/exasol/toolbox/templates/github/workflows/checks.yml +++ b/exasol/toolbox/templates/github/workflows/checks.yml @@ -168,7 +168,6 @@ jobs: id: check-format run: poetry run -- nox -s format:check - build-package: name: Build Package runs-on: "(( os_version ))" @@ -189,3 +188,24 @@ jobs: - name: Build Package id: build-package run: poetry run -- nox -s package:check + + check-workflows: + name: Check Workflows + runs-on: "(( os_version ))" + permissions: + contents: read + steps: + - name: Check out Repository + id: check-out-repository + uses: actions/checkout@v6 + + - name: Set up Python & Poetry Environment + id: set-up-python-and-poetry-environment + uses: exasol/python-toolbox/.github/actions/python-environment@v8 + with: + python-version: "(( minimum_python_version ))" + poetry-version: "(( dependency_manager_version ))" + + - name: Check Workflows + id: check-workflows + run: poetry run -- nox -s workflow:check -- all From 5afd4f74a95289f2fcd7ce91bd746cdd85a86207 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 22 May 2026 09:37:45 +0200 Subject: [PATCH 06/13] Resolve sonar smell --- exasol/toolbox/nox/_workflow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exasol/toolbox/nox/_workflow.py b/exasol/toolbox/nox/_workflow.py index f73a701b14..2c07c0093d 100644 --- a/exasol/toolbox/nox/_workflow.py +++ b/exasol/toolbox/nox/_workflow.py @@ -47,7 +47,7 @@ def check_workflow(session: Session) -> None: count = len(outdated_workflows) count_label = "workflow is" if count == 1 else "workflows are" workflow_list = "\n".join(f"- {workflow}" for workflow in outdated_workflows) - session.error(f"\n{count} {count_label} out of date:\n" f"{workflow_list}") + session.error(f"\n{count} {count_label} out of date:\n{workflow_list}") @nox.session(name="workflow:generate", python=False) From 9e2c77832de386a8a7d3f8a96c2f667e462d1e7f Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 22 May 2026 09:38:00 +0200 Subject: [PATCH 07/13] Update documentation --- doc/changes/unreleased.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index 2124665145..b92591fa35 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -3,6 +3,7 @@ ## Summary In this minor release, the nox session `workflow:check` was added and is now used in the `checks.yml`. +If this job is active in your CI, please double-check if additional files should be added into your project's `.gitattributes`. ## Bugfix @@ -10,7 +11,8 @@ In this minor release, the nox session `workflow:check` was added and is now use ## Feature -* #722: Added check in `workflow:generate` to compare the generated and existing content before writing out, nox session `workflow:check`, and `workflow:check` into the `checks.yml` +* #722: Added check in `workflow:generate` to compare the generated and existing content before writing out and nox session `workflow:check` +* #642: Added nox session `workflow:check` into the `checks.yml` ## Refactoring From 6a11baa11cd09fcf6bb9492cf40dfedf8859771e Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 22 May 2026 09:44:04 +0200 Subject: [PATCH 08/13] Add comment at top of maintained workflows --- doc/changes/unreleased.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index b92591fa35..a3c1f7efd4 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -13,6 +13,7 @@ If this job is active in your CI, please double-check if additional files should * #722: Added check in `workflow:generate` to compare the generated and existing content before writing out and nox session `workflow:check` * #642: Added nox session `workflow:check` into the `checks.yml` +* #698: Added a comment in the top of all workflows maintained by the PTB ## Refactoring From 444ff9e7bfa9ce23021ccb633ea1e11b77ffad50 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 22 May 2026 10:06:30 +0200 Subject: [PATCH 09/13] Add jinja variable for header --- exasol/toolbox/config.py | 10 ++++++++++ .../templates/github/workflows/build-and-publish.yml | 1 + exasol/toolbox/templates/github/workflows/cd.yml | 1 + .../templates/github/workflows/check-release-tag.yml | 1 + exasol/toolbox/templates/github/workflows/checks.yml | 1 + exasol/toolbox/templates/github/workflows/ci.yml | 1 + .../templates/github/workflows/dependency-update.yml | 1 + .../toolbox/templates/github/workflows/fast-tests.yml | 1 + exasol/toolbox/templates/github/workflows/gh-pages.yml | 1 + .../toolbox/templates/github/workflows/matrix-all.yml | 1 + .../templates/github/workflows/matrix-exasol.yml | 1 + .../templates/github/workflows/matrix-python.yml | 1 + .../toolbox/templates/github/workflows/merge-gate.yml | 1 + .../templates/github/workflows/periodic-validation.yml | 1 + exasol/toolbox/templates/github/workflows/pr-merge.yml | 1 + exasol/toolbox/templates/github/workflows/report.yml | 1 + test/unit/config_test.py | 10 +++++++++- 17 files changed, 34 insertions(+), 1 deletion(-) diff --git a/exasol/toolbox/config.py b/exasol/toolbox/config.py index dec28063a8..00f64d8b6b 100644 --- a/exasol/toolbox/config.py +++ b/exasol/toolbox/config.py @@ -1,4 +1,5 @@ import inspect +import re import warnings from collections.abc import Callable from pathlib import Path @@ -19,12 +20,20 @@ ) from pydantic_core.core_schema import ValidationInfo +from exasol.toolbox import __version__ from exasol.toolbox.nox.plugin import ( METHODS_SPECIFIED_FOR_HOOKS, PLUGIN_ATTR_NAME, ) from exasol.toolbox.util.version import Version +WORKFLOW_HEADER_PREFIX = ( + "# Generated and maintained by the exasol-toolbox.\n" + "# Last generated with exasol-toolbox version " +) + +WORKFLOW_HEADER_PATTERN = rf"\A{re.escape(WORKFLOW_HEADER_PREFIX)}[^\n]+\.\n" + def get_methods_with_hook_implementation( plugin_class: type[Any], @@ -291,6 +300,7 @@ def github_template_dict(self) -> dict[str, Any]: "minimum_python_version": self.minimum_python_version, "os_version": self.os_version, "python_versions": self.python_versions, + "workflow_header": f"{WORKFLOW_HEADER_PREFIX}{__version__}.", "workflow_extension": { "fast_tests": fast_tests_extension.is_file(), "merge_gate": merge_gate_extension.is_file(), diff --git a/exasol/toolbox/templates/github/workflows/build-and-publish.yml b/exasol/toolbox/templates/github/workflows/build-and-publish.yml index fe4b5ec73b..60ded6a57e 100644 --- a/exasol/toolbox/templates/github/workflows/build-and-publish.yml +++ b/exasol/toolbox/templates/github/workflows/build-and-publish.yml @@ -1,3 +1,4 @@ +(( workflow_header )) name: Build & Publish on: diff --git a/exasol/toolbox/templates/github/workflows/cd.yml b/exasol/toolbox/templates/github/workflows/cd.yml index 23176904f0..53c1e332ce 100644 --- a/exasol/toolbox/templates/github/workflows/cd.yml +++ b/exasol/toolbox/templates/github/workflows/cd.yml @@ -1,3 +1,4 @@ +(( workflow_header )) name: CD on: diff --git a/exasol/toolbox/templates/github/workflows/check-release-tag.yml b/exasol/toolbox/templates/github/workflows/check-release-tag.yml index 7e1598f5ab..26bfd78334 100644 --- a/exasol/toolbox/templates/github/workflows/check-release-tag.yml +++ b/exasol/toolbox/templates/github/workflows/check-release-tag.yml @@ -1,3 +1,4 @@ +(( workflow_header )) name: Check Release Tag on: diff --git a/exasol/toolbox/templates/github/workflows/checks.yml b/exasol/toolbox/templates/github/workflows/checks.yml index 740da6ccce..ea70f6c5c1 100644 --- a/exasol/toolbox/templates/github/workflows/checks.yml +++ b/exasol/toolbox/templates/github/workflows/checks.yml @@ -1,3 +1,4 @@ +(( workflow_header )) name: Checks on: diff --git a/exasol/toolbox/templates/github/workflows/ci.yml b/exasol/toolbox/templates/github/workflows/ci.yml index b6171e32b6..b160efea70 100644 --- a/exasol/toolbox/templates/github/workflows/ci.yml +++ b/exasol/toolbox/templates/github/workflows/ci.yml @@ -1,3 +1,4 @@ +(( workflow_header )) name: CI on: diff --git a/exasol/toolbox/templates/github/workflows/dependency-update.yml b/exasol/toolbox/templates/github/workflows/dependency-update.yml index 549a8d4c43..3800e15321 100644 --- a/exasol/toolbox/templates/github/workflows/dependency-update.yml +++ b/exasol/toolbox/templates/github/workflows/dependency-update.yml @@ -1,3 +1,4 @@ +(( workflow_header )) name: Dependency Update on: diff --git a/exasol/toolbox/templates/github/workflows/fast-tests.yml b/exasol/toolbox/templates/github/workflows/fast-tests.yml index 5d5696166a..39cee71daf 100644 --- a/exasol/toolbox/templates/github/workflows/fast-tests.yml +++ b/exasol/toolbox/templates/github/workflows/fast-tests.yml @@ -1,3 +1,4 @@ +(( workflow_header )) name: Fast-Tests on: diff --git a/exasol/toolbox/templates/github/workflows/gh-pages.yml b/exasol/toolbox/templates/github/workflows/gh-pages.yml index 4fc438c356..6f5d5a150b 100644 --- a/exasol/toolbox/templates/github/workflows/gh-pages.yml +++ b/exasol/toolbox/templates/github/workflows/gh-pages.yml @@ -1,3 +1,4 @@ +(( workflow_header )) name: Publish Documentation on: diff --git a/exasol/toolbox/templates/github/workflows/matrix-all.yml b/exasol/toolbox/templates/github/workflows/matrix-all.yml index 000358a06e..f5695bde77 100644 --- a/exasol/toolbox/templates/github/workflows/matrix-all.yml +++ b/exasol/toolbox/templates/github/workflows/matrix-all.yml @@ -1,3 +1,4 @@ +(( workflow_header )) name: Build Matrix (All Versions) on: diff --git a/exasol/toolbox/templates/github/workflows/matrix-exasol.yml b/exasol/toolbox/templates/github/workflows/matrix-exasol.yml index e13b7347fa..0e3b93f980 100644 --- a/exasol/toolbox/templates/github/workflows/matrix-exasol.yml +++ b/exasol/toolbox/templates/github/workflows/matrix-exasol.yml @@ -1,3 +1,4 @@ +(( workflow_header )) name: Build Matrix (Exasol) on: diff --git a/exasol/toolbox/templates/github/workflows/matrix-python.yml b/exasol/toolbox/templates/github/workflows/matrix-python.yml index 5524a02270..9d330d1374 100644 --- a/exasol/toolbox/templates/github/workflows/matrix-python.yml +++ b/exasol/toolbox/templates/github/workflows/matrix-python.yml @@ -1,3 +1,4 @@ +(( workflow_header )) name: Build Matrix (Python) on: diff --git a/exasol/toolbox/templates/github/workflows/merge-gate.yml b/exasol/toolbox/templates/github/workflows/merge-gate.yml index f58a2d54d9..c738dad1f2 100644 --- a/exasol/toolbox/templates/github/workflows/merge-gate.yml +++ b/exasol/toolbox/templates/github/workflows/merge-gate.yml @@ -1,3 +1,4 @@ +(( workflow_header )) name: Merge-Gate on: diff --git a/exasol/toolbox/templates/github/workflows/periodic-validation.yml b/exasol/toolbox/templates/github/workflows/periodic-validation.yml index 219de1bd62..ff0734e86d 100644 --- a/exasol/toolbox/templates/github/workflows/periodic-validation.yml +++ b/exasol/toolbox/templates/github/workflows/periodic-validation.yml @@ -1,3 +1,4 @@ +(( workflow_header )) name: Periodic-Validation on: diff --git a/exasol/toolbox/templates/github/workflows/pr-merge.yml b/exasol/toolbox/templates/github/workflows/pr-merge.yml index 861e45e8a9..4a76a2103a 100644 --- a/exasol/toolbox/templates/github/workflows/pr-merge.yml +++ b/exasol/toolbox/templates/github/workflows/pr-merge.yml @@ -1,3 +1,4 @@ +(( workflow_header )) name: PR-Merge on: diff --git a/exasol/toolbox/templates/github/workflows/report.yml b/exasol/toolbox/templates/github/workflows/report.yml index 2ad381fbe7..da14a6f0ef 100644 --- a/exasol/toolbox/templates/github/workflows/report.yml +++ b/exasol/toolbox/templates/github/workflows/report.yml @@ -1,3 +1,4 @@ +(( workflow_header )) name: Status Report on: diff --git a/test/unit/config_test.py b/test/unit/config_test.py index 0b80c5a059..931858f9bf 100644 --- a/test/unit/config_test.py +++ b/test/unit/config_test.py @@ -1,3 +1,4 @@ +import re from collections.abc import Iterable from pathlib import Path from unittest.mock import Mock @@ -28,8 +29,15 @@ class TestBaseConfig: @staticmethod def test_works_as_defined(tmp_path, test_project_config_factory): config = test_project_config_factory() + root_path = config.root_path - assert config.model_dump() == { + config_dump = config.model_dump() + workflow_header = config_dump["github_template_dict"].pop("workflow_header") + assert re.sub(r"version [^\n]+\.", "version X.Y.Z.", workflow_header) == ( + "# Generated and maintained by the exasol-toolbox.\n" + "# Last generated with exasol-toolbox version X.Y.Z." + ) + assert config_dump == { "add_to_excluded_python_paths": (), "create_major_version_tags": False, "dependency_manager": {"name": "poetry", "version": "2.3.0"}, From 5bfa06698fa9b5ec6edb06ad319a7d49ccb79e6b Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 22 May 2026 10:41:59 +0200 Subject: [PATCH 10/13] Make it so compare ignores the header but write doesn't so it updates it --- exasol/toolbox/util/workflows/workflow.py | 29 +++++++++++++--- test/unit/util/workflows/workflow_test.py | 41 +++++++++++++++++++++++ 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/exasol/toolbox/util/workflows/workflow.py b/exasol/toolbox/util/workflows/workflow.py index 577b885744..9d96293be1 100644 --- a/exasol/toolbox/util/workflows/workflow.py +++ b/exasol/toolbox/util/workflows/workflow.py @@ -1,4 +1,5 @@ import difflib +import re from pathlib import Path from typing import ( Any, @@ -12,6 +13,7 @@ bound_contextvars, ) +from exasol.toolbox.config import WORKFLOW_HEADER_PATTERN from exasol.toolbox.util.workflows import logger from exasol.toolbox.util.workflows.exceptions import ( YamlError, @@ -30,6 +32,20 @@ class Workflow(BaseModel): output_path: Path content: str + @staticmethod + def _normalize_content(content: str, strip_header: bool = True) -> str: + """ + Normalize workflow content for comparison. + """ + normalized_content = content.strip() + if strip_header: + normalized_content = re.sub( + pattern=WORKFLOW_HEADER_PATTERN, + repl="", + string=normalized_content, + ).strip() + return normalized_content + @classmethod def load_from_template( cls, @@ -61,12 +77,17 @@ def load_from_template( # Wrap all other "non-special" exceptions raise ValueError(f"Error rendering file: {template_path}") from ex - def compare_to_file(self) -> str: + def compare_to_file(self, strip_header: bool = True) -> str: existing_content = "" if self.output_path.is_file(): - existing_content = self.output_path.read_text().strip() + existing_content = self.output_path.read_text() - generated_content = self.content.strip() + existing_content = self._normalize_content( + existing_content, strip_header=strip_header + ) + generated_content = self._normalize_content( + self.content, strip_header=strip_header + ) diff = difflib.unified_diff( existing_content.splitlines(), @@ -78,7 +99,7 @@ def compare_to_file(self) -> str: return "\n".join(diff) def write_to_file(self) -> None: - if self.compare_to_file() == "": + if self.compare_to_file(strip_header=False) == "": logger.debug("Skip up-to-date workflow file %s", self.output_path.name) return logger.info("Write workflow file %s", self.output_path.name) diff --git a/test/unit/util/workflows/workflow_test.py b/test/unit/util/workflows/workflow_test.py index 8148fcc75a..b8d8acdfb7 100644 --- a/test/unit/util/workflows/workflow_test.py +++ b/test/unit/util/workflows/workflow_test.py @@ -4,6 +4,7 @@ import pytest +from exasol.toolbox.config import WORKFLOW_HEADER_PREFIX from exasol.toolbox.util.workflows.exceptions import ( TemplateRenderingError, YamlOutputError, @@ -207,6 +208,46 @@ def test_load_from_template_raises_custom_exceptions( github_template_dict=project_config.github_template_dict, ) + +class TestNormalizeContent: + @staticmethod + def test_strips_workflow_header_when_requested(): + content = ( + f"{WORKFLOW_HEADER_PREFIX}8.0.0.\n\n" + "jobs:\n" + " build:\n" + " runs-on: ubuntu-24.04\n" + ) + + assert Workflow._normalize_content(content, strip_header=True) == ( + "jobs:\n" " build:\n" " runs-on: ubuntu-24.04" + ) + + @staticmethod + def test_keeps_workflow_header_when_not_requested(): + content = ( + f"{WORKFLOW_HEADER_PREFIX}8.0.0.\n\n" + "jobs:\n" + " build:\n" + " runs-on: ubuntu-24.04\n" + ) + + assert Workflow._normalize_content(content, strip_header=False) == ( + f"{WORKFLOW_HEADER_PREFIX}8.0.0.\n\n" + "jobs:\n" + " build:\n" + " runs-on: ubuntu-24.04" + ) + + @staticmethod + @pytest.mark.parametrize("strip_header", [True, False]) + def test_always_strips_outer_whitespace(strip_header): + content = " jobs:\n build:\n runs-on: ubuntu-24.04\n\n" + + assert Workflow._normalize_content(content, strip_header=strip_header) == ( + "jobs:\n" " build:\n" " runs-on: ubuntu-24.04" + ) + @staticmethod def test_load_from_template_reraises_other_exceptions_raised_as_valuerror( tmp_path, project_config From 9792372be1e5a374866a5418ef6a931602c37c47 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 22 May 2026 10:57:52 +0200 Subject: [PATCH 11/13] Fix unit tests --- .../workflows/workflow_orchestrator_test.py | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/test/unit/util/workflows/workflow_orchestrator_test.py b/test/unit/util/workflows/workflow_orchestrator_test.py index 8086a3e58a..640a15e28d 100644 --- a/test/unit/util/workflows/workflow_orchestrator_test.py +++ b/test/unit/util/workflows/workflow_orchestrator_test.py @@ -11,9 +11,17 @@ NOT_MAINTAINED_WORKFLOW_NAMES, WORKFLOW_TEMPLATE_OPTIONS, ) +from exasol.toolbox.util.workflows.workflow import Workflow from exasol.toolbox.util.workflows.workflow_orchestrator import WorkflowOrchestrator +def _remove_header(template_text: str) -> str: + """ + Remove the Jinja header placeholder line from a workflow template. + """ + return template_text.split("\n", 1)[1].strip() + + class TestTemplates: @staticmethod def test_all_works_as_expected(project_config): @@ -102,7 +110,10 @@ def test_works_as_expected_without_patcher(project_config_without_patcher): assert len(result) == 1 assert result[0].template_path == WORKFLOW_TEMPLATE_OPTIONS[workflow_name] assert result[0].output_path.name == f"{workflow_name}.yml" - assert result[0].content[:10] == input_text[:10] + assert ( + Workflow._normalize_content(result[0].content)[:10] + == _remove_header(input_text)[:10] + ) @staticmethod def test_works_as_expected_with_relevant_patcher(project_config, remove_job_yaml): @@ -121,7 +132,10 @@ def test_works_as_expected_with_relevant_patcher(project_config, remove_job_yaml result = list(result) assert len(result) == 1 assert result[0].output_path.name == f"{workflow_name}.yml" - assert result[0].content[:10] == input_text[:10] + assert ( + Workflow._normalize_content(result[0].content)[:10] + == _remove_header(input_text)[:10] + ) assert removed_job_name not in result[0].content @staticmethod @@ -140,7 +154,10 @@ def test_works_as_expected_with_not_relevant_patcher( result = list(result) assert len(result) == 1 assert result[0].output_path.name == f"{workflow_name}.yml" - assert result[0].content[:10] == input_text[:10] + assert ( + Workflow._normalize_content(result[0].content)[:10] + == _remove_header(input_text)[:10] + ) @staticmethod def test_not_maintained_workflows_added_to_new_project( From 806232be69b6682f4b8d1f58c1eedee74f906517 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 22 May 2026 11:02:56 +0200 Subject: [PATCH 12/13] Update with comment with last generated with 8.0.0 --- .github/workflows/build-and-publish.yml | 2 ++ .github/workflows/cd.yml | 2 ++ .github/workflows/check-release-tag.yml | 2 ++ .github/workflows/checks.yml | 2 ++ .github/workflows/ci.yml | 2 ++ .github/workflows/dependency-update.yml | 2 ++ .github/workflows/fast-tests.yml | 2 ++ .github/workflows/gh-pages.yml | 2 ++ .github/workflows/matrix-all.yml | 2 ++ .github/workflows/matrix-exasol.yml | 2 ++ .github/workflows/matrix-python.yml | 2 ++ .github/workflows/merge-gate.yml | 2 ++ .github/workflows/periodic-validation.yml | 2 ++ .github/workflows/pr-merge.yml | 2 ++ .github/workflows/report.yml | 2 ++ 15 files changed, 30 insertions(+) diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index 351a75d48b..341dc7d576 100644 --- a/.github/workflows/build-and-publish.yml +++ b/.github/workflows/build-and-publish.yml @@ -1,3 +1,5 @@ +# Generated and maintained by the exasol-toolbox. +# Last generated with exasol-toolbox version 8.0.0. name: Build & Publish on: diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 23176904f0..d171b63f21 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -1,3 +1,5 @@ +# Generated and maintained by the exasol-toolbox. +# Last generated with exasol-toolbox version 8.0.0. name: CD on: diff --git a/.github/workflows/check-release-tag.yml b/.github/workflows/check-release-tag.yml index 5f51fc408e..2f3fa647f3 100644 --- a/.github/workflows/check-release-tag.yml +++ b/.github/workflows/check-release-tag.yml @@ -1,3 +1,5 @@ +# Generated and maintained by the exasol-toolbox. +# Last generated with exasol-toolbox version 8.0.0. name: Check Release Tag on: diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index e4a5086c5f..d9815f1838 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -1,3 +1,5 @@ +# Generated and maintained by the exasol-toolbox. +# Last generated with exasol-toolbox version 8.0.0. name: Checks on: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6171e32b6..6635826dbe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,3 +1,5 @@ +# Generated and maintained by the exasol-toolbox. +# Last generated with exasol-toolbox version 8.0.0. name: CI on: diff --git a/.github/workflows/dependency-update.yml b/.github/workflows/dependency-update.yml index be32b3a640..40cf00442f 100644 --- a/.github/workflows/dependency-update.yml +++ b/.github/workflows/dependency-update.yml @@ -1,3 +1,5 @@ +# Generated and maintained by the exasol-toolbox. +# Last generated with exasol-toolbox version 8.0.0. name: Dependency Update on: diff --git a/.github/workflows/fast-tests.yml b/.github/workflows/fast-tests.yml index 56b85bb792..b144773ce7 100644 --- a/.github/workflows/fast-tests.yml +++ b/.github/workflows/fast-tests.yml @@ -1,3 +1,5 @@ +# Generated and maintained by the exasol-toolbox. +# Last generated with exasol-toolbox version 8.0.0. name: Fast-Tests on: diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 2a67b9bf7f..93f50c5f0e 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -1,3 +1,5 @@ +# Generated and maintained by the exasol-toolbox. +# Last generated with exasol-toolbox version 8.0.0. name: Publish Documentation on: diff --git a/.github/workflows/matrix-all.yml b/.github/workflows/matrix-all.yml index 175fc2b075..dd0b8cc1a0 100644 --- a/.github/workflows/matrix-all.yml +++ b/.github/workflows/matrix-all.yml @@ -1,3 +1,5 @@ +# Generated and maintained by the exasol-toolbox. +# Last generated with exasol-toolbox version 8.0.0. name: Build Matrix (All Versions) on: diff --git a/.github/workflows/matrix-exasol.yml b/.github/workflows/matrix-exasol.yml index 66911a473e..f2a63e30f4 100644 --- a/.github/workflows/matrix-exasol.yml +++ b/.github/workflows/matrix-exasol.yml @@ -1,3 +1,5 @@ +# Generated and maintained by the exasol-toolbox. +# Last generated with exasol-toolbox version 8.0.0. name: Build Matrix (Exasol) on: diff --git a/.github/workflows/matrix-python.yml b/.github/workflows/matrix-python.yml index 929af96ee1..c56f26c0f6 100644 --- a/.github/workflows/matrix-python.yml +++ b/.github/workflows/matrix-python.yml @@ -1,3 +1,5 @@ +# Generated and maintained by the exasol-toolbox. +# Last generated with exasol-toolbox version 8.0.0. name: Build Matrix (Python) on: diff --git a/.github/workflows/merge-gate.yml b/.github/workflows/merge-gate.yml index 00c29af9e8..060e25d7e7 100644 --- a/.github/workflows/merge-gate.yml +++ b/.github/workflows/merge-gate.yml @@ -1,3 +1,5 @@ +# Generated and maintained by the exasol-toolbox. +# Last generated with exasol-toolbox version 8.0.0. name: Merge-Gate on: diff --git a/.github/workflows/periodic-validation.yml b/.github/workflows/periodic-validation.yml index 219de1bd62..384d552ad1 100644 --- a/.github/workflows/periodic-validation.yml +++ b/.github/workflows/periodic-validation.yml @@ -1,3 +1,5 @@ +# Generated and maintained by the exasol-toolbox. +# Last generated with exasol-toolbox version 8.0.0. name: Periodic-Validation on: diff --git a/.github/workflows/pr-merge.yml b/.github/workflows/pr-merge.yml index 861e45e8a9..c422a65ca6 100644 --- a/.github/workflows/pr-merge.yml +++ b/.github/workflows/pr-merge.yml @@ -1,3 +1,5 @@ +# Generated and maintained by the exasol-toolbox. +# Last generated with exasol-toolbox version 8.0.0. name: PR-Merge on: diff --git a/.github/workflows/report.yml b/.github/workflows/report.yml index 90c1e983ab..42cd7bf327 100644 --- a/.github/workflows/report.yml +++ b/.github/workflows/report.yml @@ -1,3 +1,5 @@ +# Generated and maintained by the exasol-toolbox. +# Last generated with exasol-toolbox version 8.0.0. name: Status Report on: From 053d5e99bad7854c42ee3df6743508e2b75bb8e7 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Fri, 22 May 2026 11:31:14 +0200 Subject: [PATCH 13/13] Update string so meaning is clearer in user guide --- doc/user_guide/features/github_workflows/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user_guide/features/github_workflows/index.rst b/doc/user_guide/features/github_workflows/index.rst index e5d280a6a8..225df16f84 100644 --- a/doc/user_guide/features/github_workflows/index.rst +++ b/doc/user_guide/features/github_workflows/index.rst @@ -24,7 +24,7 @@ The PTB provides a command line interface (CLI) for managing workflows from the poetry run -- nox -s workflow:check --help Use ``workflow:generate`` to create or update workflows and ``workflow:check`` to -compare the generated workflows against the files in ``.github/workflows``. +compare the rendered workflow templates against the files in ``.github/workflows``. .. attention::