diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index 351a75d48..341dc7d57 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 23176904f..d171b63f2 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 5f51fc408..2f3fa647f 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 4d9adb672..d9815f183 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: @@ -168,7 +170,6 @@ jobs: id: check-format run: poetry run -- nox -s format:check - build-package: name: Build Package runs-on: "ubuntu-24.04" @@ -189,3 +190,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/.github/workflows/ci.yml b/.github/workflows/ci.yml index b6171e32b..6635826db 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 be32b3a64..40cf00442 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 56b85bb79..b144773ce 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 2a67b9bf7..93f50c5f0 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 175fc2b07..dd0b8cc1a 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 66911a473..f2a63e30f 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 929af96ee..c56f26c0f 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 00c29af9e..060e25d7e 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 219de1bd6..384d552ad 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 861e45e8a..c422a65ca 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 90c1e983a..42cd7bf32 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: diff --git a/.gitignore b/.gitignore index bfe5173af..08b168a80 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,8 @@ nosetests.xml # Emacs TAGS + +# AI +.codex +.serena +specs/ diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index 7ea1b208d..a3c1f7efd 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -2,13 +2,18 @@ ## 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 * #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 +* #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 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 2273d95a5..b1da0db26 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 10f7f5b8c..225df16f8 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 rendered workflow templates against the files in ``.github/workflows``. .. attention:: @@ -62,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/config.py b/exasol/toolbox/config.py index dec28063a..00f64d8b6 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/nox/_workflow.py b/exasol/toolbox/nox/_workflow.py index 9508941a8..2c07c0093 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{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/templates/github/workflows/build-and-publish.yml b/exasol/toolbox/templates/github/workflows/build-and-publish.yml index fe4b5ec73..60ded6a57 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 23176904f..53c1e332c 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 7e1598f5a..26bfd7833 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 35a3ef777..ea70f6c5c 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: @@ -168,7 +169,6 @@ jobs: id: check-format run: poetry run -- nox -s format:check - build-package: name: Build Package runs-on: "(( os_version ))" @@ -189,3 +189,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 diff --git a/exasol/toolbox/templates/github/workflows/ci.yml b/exasol/toolbox/templates/github/workflows/ci.yml index b6171e32b..b160efea7 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 549a8d4c4..3800e1532 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 5d5696166..39cee71da 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 4fc438c35..6f5d5a150 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 000358a06..f5695bde7 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 e13b7347f..0e3b93f98 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 5524a0227..9d330d137 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 f58a2d54d..c738dad1f 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 219de1bd6..ff0734e86 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 861e45e8a..4a76a2103 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 2ad381fbe..da14a6f0e 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/exasol/toolbox/util/workflows/workflow.py b/exasol/toolbox/util/workflows/workflow.py index 577b88574..9d96293be 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/exasol/toolbox/util/workflows/workflow_orchestrator.py b/exasol/toolbox/util/workflows/workflow_orchestrator.py index 95977454d..61e0ba671 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 diff --git a/test/unit/config_test.py b/test/unit/config_test.py index 0b80c5a05..931858f9b 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"}, diff --git a/test/unit/nox/_workflow_test.py b/test/unit/nox/_workflow_test.py index 75cf445a6..983d16256 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 6a79df611..640a15e28 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( @@ -265,3 +282,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 diff --git a/test/unit/util/workflows/workflow_test.py b/test/unit/util/workflows/workflow_test.py index 8148fcc75..b8d8acdfb 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