diff --git a/.github/workflows/build-and-publish.yml b/.github/workflows/build-and-publish.yml index 3067968b0..e16c84b05 100644 --- a/.github/workflows/build-and-publish.yml +++ b/.github/workflows/build-and-publish.yml @@ -4,7 +4,7 @@ on: workflow_call: secrets: PYPI_TOKEN: - required: true + required: true jobs: @@ -35,7 +35,7 @@ jobs: - name: GitHub Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: > + run: >- gh release create ${GITHUB_REF_NAME} --title ${GITHUB_REF_NAME} --notes-file ./doc/changes/changes_${GITHUB_REF_NAME}.md diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 1fb0e2e00..98a11f62a 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -15,7 +15,8 @@ jobs: contents: read cd-job: - needs: [ check-tag-version-job ] + needs: + - check-tag-version-job name: Continuous Delivery uses: ./.github/workflows/build-and-publish.yml permissions: @@ -24,7 +25,8 @@ jobs: PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} publish-docs: - needs: [ cd-job ] + needs: + - cd-job name: Publish Documentation uses: ./.github/workflows/gh-pages.yml permissions: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d7f0a0f3f..5cba3e817 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: pull_request: - types: [opened, synchronize, reopened] + types: [opened, synchronize, reopened] schedule: # At 00:00 on every 7th day-of-month from 1 through 31. (https://crontab.guru) - cron: "0 0 1/7 * *" @@ -16,7 +16,8 @@ jobs: contents: read Metrics: - needs: [ CI ] + needs: + - CI uses: ./.github/workflows/report.yml secrets: inherit permissions: diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index d47ce1f18..330218150 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -33,7 +33,8 @@ jobs: path: html-documentation deploy-documentation: - needs: [ build-documentation ] + needs: + - build-documentation permissions: contents: read pages: write diff --git a/.github/workflows/merge-gate.yml b/.github/workflows/merge-gate.yml index dc57b3599..74b3d93f9 100644 --- a/.github/workflows/merge-gate.yml +++ b/.github/workflows/merge-gate.yml @@ -27,7 +27,8 @@ jobs: slow-checks: name: Slow - needs: [ run-slow-tests ] + needs: + - run-slow-tests uses: ./.github/workflows/slow-checks.yml secrets: inherit permissions: @@ -40,10 +41,11 @@ jobs: permissions: contents: read # If you need additional jobs to be part of the merge gate, add them below - needs: [ fast-checks, slow-checks ] + needs: + - fast-checks + - slow-checks # Each job requires a step, so we added this dummy step. steps: - name: Approve - run: | - echo "Merge Approved" + run: echo "Merge Approved" diff --git a/.github/workflows/pr-merge.yml b/.github/workflows/pr-merge.yml index a95cee5c3..544c8a94e 100644 --- a/.github/workflows/pr-merge.yml +++ b/.github/workflows/pr-merge.yml @@ -26,7 +26,8 @@ jobs: id-token: write metrics: - needs: [ ci-job ] + needs: + - ci-job uses: ./.github/workflows/report.yml secrets: inherit permissions: diff --git a/.github/workflows/report.yml b/.github/workflows/report.yml index 7a4a4857a..39502c9db 100644 --- a/.github/workflows/report.yml +++ b/.github/workflows/report.yml @@ -50,7 +50,7 @@ jobs: path: metrics.json - name: Generate GitHub Summary - run: | + run: |- echo -e "# Summary\n" >> $GITHUB_STEP_SUMMARY poetry run -- nox -s project:report -- --format markdown >> $GITHUB_STEP_SUMMARY poetry run -- nox -s dependency:licenses >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/slow-checks.yml b/.github/workflows/slow-checks.yml index ae3aa4e9d..4707eaf26 100644 --- a/.github/workflows/slow-checks.yml +++ b/.github/workflows/slow-checks.yml @@ -12,7 +12,8 @@ jobs: tests: name: Integration-Tests (Python-${{ matrix.python-version }}, Exasol-${{ matrix.exasol-version}}) - needs: [ build-matrix ] + needs: + - build-matrix runs-on: "ubuntu-24.04" permissions: contents: read @@ -44,7 +45,8 @@ jobs: verify-poetry-installation: - needs: [ build-matrix ] + needs: + - build-matrix # This Job verifies if pipx installation is successful on each of the # selected GitHub Runners. strategy: diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index ab9f1d8c0..ea8fce542 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -5,3 +5,7 @@ ## Documentation * #585: Added instructions how to ignore sonar issues to the User Guide + +## Refactoring + +* #686: Switched GitHub templates to be fully parsed by ruamel-yaml diff --git a/exasol/toolbox/templates/github/workflows/build-and-publish.yml b/exasol/toolbox/templates/github/workflows/build-and-publish.yml index cdf7ac950..74e4e5e21 100644 --- a/exasol/toolbox/templates/github/workflows/build-and-publish.yml +++ b/exasol/toolbox/templates/github/workflows/build-and-publish.yml @@ -4,7 +4,7 @@ on: workflow_call: secrets: PYPI_TOKEN: - required: true + required: true jobs: diff --git a/exasol/toolbox/templates/github/workflows/cd.yml b/exasol/toolbox/templates/github/workflows/cd.yml index 1fb0e2e00..99e2201f4 100644 --- a/exasol/toolbox/templates/github/workflows/cd.yml +++ b/exasol/toolbox/templates/github/workflows/cd.yml @@ -15,7 +15,8 @@ jobs: contents: read cd-job: - needs: [ check-tag-version-job ] + needs: + - check-tag-version-job name: Continuous Delivery uses: ./.github/workflows/build-and-publish.yml permissions: @@ -24,7 +25,8 @@ jobs: PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }} publish-docs: - needs: [ cd-job ] + needs: + - cd-job name: Publish Documentation uses: ./.github/workflows/gh-pages.yml permissions: diff --git a/exasol/toolbox/templates/github/workflows/ci.yml b/exasol/toolbox/templates/github/workflows/ci.yml index d7f0a0f3f..2cdd4db79 100644 --- a/exasol/toolbox/templates/github/workflows/ci.yml +++ b/exasol/toolbox/templates/github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: pull_request: - types: [opened, synchronize, reopened] + types: [opened, synchronize, reopened] schedule: # At 00:00 on every 7th day-of-month from 1 through 31. (https://crontab.guru) - cron: "0 0 1/7 * *" @@ -16,7 +16,8 @@ jobs: contents: read Metrics: - needs: [ CI ] + needs: + - CI uses: ./.github/workflows/report.yml secrets: inherit permissions: diff --git a/exasol/toolbox/templates/github/workflows/gh-pages.yml b/exasol/toolbox/templates/github/workflows/gh-pages.yml index 71a3bdff3..87f57ad54 100644 --- a/exasol/toolbox/templates/github/workflows/gh-pages.yml +++ b/exasol/toolbox/templates/github/workflows/gh-pages.yml @@ -33,7 +33,8 @@ jobs: path: html-documentation deploy-documentation: - needs: [ build-documentation ] + needs: + - build-documentation permissions: contents: read pages: write diff --git a/exasol/toolbox/templates/github/workflows/merge-gate.yml b/exasol/toolbox/templates/github/workflows/merge-gate.yml index e1c2174a3..516110dc7 100644 --- a/exasol/toolbox/templates/github/workflows/merge-gate.yml +++ b/exasol/toolbox/templates/github/workflows/merge-gate.yml @@ -27,7 +27,8 @@ jobs: slow-checks: name: Slow - needs: [ run-slow-tests ] + needs: + - run-slow-tests uses: ./.github/workflows/slow-checks.yml secrets: inherit permissions: @@ -40,10 +41,11 @@ jobs: permissions: contents: read # If you need additional jobs to be part of the merge gate, add them below - needs: [ fast-checks, slow-checks ] + needs: + - fast-checks + - slow-checks # Each job requires a step, so we added this dummy step. steps: - name: Approve - run: | - echo "Merge Approved" + run: echo "Merge Approved" diff --git a/exasol/toolbox/templates/github/workflows/pr-merge.yml b/exasol/toolbox/templates/github/workflows/pr-merge.yml index a95cee5c3..8cf903f02 100644 --- a/exasol/toolbox/templates/github/workflows/pr-merge.yml +++ b/exasol/toolbox/templates/github/workflows/pr-merge.yml @@ -26,7 +26,8 @@ jobs: id-token: write metrics: - needs: [ ci-job ] + needs: + - ci-job uses: ./.github/workflows/report.yml secrets: inherit permissions: diff --git a/exasol/toolbox/templates/github/workflows/slow-checks.yml b/exasol/toolbox/templates/github/workflows/slow-checks.yml index da1974bfc..c58d358f1 100644 --- a/exasol/toolbox/templates/github/workflows/slow-checks.yml +++ b/exasol/toolbox/templates/github/workflows/slow-checks.yml @@ -12,7 +12,8 @@ jobs: tests: name: Integration-Tests (Python-${{ matrix.python-version }}, Exasol-${{ matrix.exasol-version}}) - needs: [ build-matrix ] + needs: + - build-matrix runs-on: "(( os_version ))" permissions: contents: read diff --git a/exasol/toolbox/tools/template.py b/exasol/toolbox/tools/template.py index 14829ea1f..1fcc28568 100644 --- a/exasol/toolbox/tools/template.py +++ b/exasol/toolbox/tools/template.py @@ -2,7 +2,6 @@ import io from collections.abc import Mapping from contextlib import ExitStack -from inspect import cleandoc from pathlib import Path from typing import ( Any, @@ -10,12 +9,11 @@ import importlib_resources as resources import typer -import yaml -from jinja2 import Environment from rich.columns import Columns from rich.console import Console from rich.syntax import Syntax +from exasol.toolbox.util.workflows.workflow import Workflow from noxconfig import PROJECT_CONFIG stdout = Console() @@ -23,10 +21,6 @@ CLI = typer.Typer() -jinja_env = Environment( - variable_start_string="((", variable_end_string="))", autoescape=True -) - def _templates(pkg: str) -> Mapping[str, Any]: def _normalize(name: str) -> str: @@ -72,18 +66,13 @@ def show_templates( def _render_template( src: str | Path, - stack: ExitStack, ) -> str: - input_file = stack.enter_context(open(src, encoding="utf-8")) - - # dynamically render the template with Jinja2 - template = jinja_env.from_string(input_file.read()) - rendered_string = template.render(PROJECT_CONFIG.github_template_dict) - - # validate that the rendered content is a valid YAML. This is not - # written out as by default it does not give GitHub-safe output. - yaml.safe_load(rendered_string) - return cleandoc(rendered_string) + "\n" + src_path = Path(src) + github_template_dict = PROJECT_CONFIG.github_template_dict + workflow = Workflow.load_from_template( + file_path=src_path, github_template_dict=github_template_dict + ) + return workflow.content + "\n" def diff_template(template: str, dest: Path, pkg: str, template_type: str) -> None: @@ -107,7 +96,7 @@ def diff_template(template: str, dest: Path, pkg: str, template_type: str) -> No old = old.read().split("\n") new = new.read().split("\n") elif template_type == "workflow": - new = _render_template(src=new, stack=stack) + new = _render_template(src=new) old = old.read().split("\n") new = new.split("\n") @@ -134,7 +123,7 @@ def _install_template( return output_file = stack.enter_context(open(dest, "wb")) - rendered_string = _render_template(src=src, stack=stack) + rendered_string = _render_template(src=src) output_file.write(rendered_string.encode("utf-8")) diff --git a/exasol/toolbox/util/workflows/__init__.py b/exasol/toolbox/util/workflows/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/exasol/toolbox/util/workflows/template_processing.py b/exasol/toolbox/util/workflows/template_processing.py new file mode 100644 index 000000000..e78deba45 --- /dev/null +++ b/exasol/toolbox/util/workflows/template_processing.py @@ -0,0 +1,44 @@ +from dataclasses import dataclass +from typing import Any + +from jinja2 import Environment + +jinja_env = Environment( + variable_start_string="((", variable_end_string="))", autoescape=True +) + +import io +from inspect import cleandoc + +from ruamel.yaml import YAML + + +@dataclass(frozen=True) +class TemplateRenderer: + template_str: str + github_template_dict: dict[str, Any] + + def _render_with_jinja(self, input_str: str) -> str: + """ + Render the template with Jinja. + """ + jinja_template = jinja_env.from_string(input_str) + return jinja_template.render(self.github_template_dict) + + def render_to_workflow(self) -> str: + """ + Render the template to the contents of a valid GitHub workflow. + """ + yaml = YAML() + yaml.width = 200 + yaml.preserve_quotes = True + yaml.sort_base_mapping_type_on_output = False # type: ignore + yaml.indent(mapping=2, sequence=4, offset=2) + + workflow_string = self._render_with_jinja(self.template_str) + workflow_dict = yaml.load(workflow_string) + + stream = io.StringIO() + yaml.dump(workflow_dict, stream) + workflow_string = stream.getvalue() + return cleandoc(workflow_string) diff --git a/exasol/toolbox/util/workflows/workflow.py b/exasol/toolbox/util/workflows/workflow.py new file mode 100644 index 000000000..83964a2ea --- /dev/null +++ b/exasol/toolbox/util/workflows/workflow.py @@ -0,0 +1,30 @@ +from pathlib import Path +from typing import Any + +from pydantic import ( + BaseModel, + ConfigDict, +) + +from exasol.toolbox.util.workflows.template_processing import TemplateRenderer + + +class Workflow(BaseModel): + model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True) + + content: str + + @classmethod + def load_from_template(cls, file_path: Path, github_template_dict: dict[str, Any]): + if not file_path.exists(): + raise FileNotFoundError(file_path) + + try: + raw_content = file_path.read_text() + template_renderer = TemplateRenderer( + template_str=raw_content, github_template_dict=github_template_dict + ) + workflow = template_renderer.render_to_workflow() + return cls(content=workflow) + except Exception as e: + raise ValueError(f"Error rendering file: {file_path}") from e diff --git a/poetry.lock b/poetry.lock index 94fb0f9bd..9db334592 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2165,14 +2165,14 @@ tests = ["pytest (>=9)", "typing-extensions (>=4.15)"] [[package]] name = "pip" -version = "25.3" +version = "26.0" description = "The PyPA recommended tool for installing Python packages." optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pip-25.3-py3-none-any.whl", hash = "sha256:9655943313a94722b7774661c21049070f6bbb0a1516bf02f7c8d5d9201514cd"}, - {file = "pip-25.3.tar.gz", hash = "sha256:8d0538dbbd7babbd207f261ed969c65de439f6bc9e5dbd3b3b9a77f25d95f343"}, + {file = "pip-26.0-py3-none-any.whl", hash = "sha256:98436feffb9e31bc9339cf369fd55d3331b1580b6a6f1173bacacddcf9c34754"}, + {file = "pip-26.0.tar.gz", hash = "sha256:3ce220a0a17915972fbf1ab451baae1521c4539e778b28127efa79b974aff0fa"}, ] [[package]] @@ -3943,4 +3943,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "8a5d05c9cc28f8599edd5055881b350ebf0588dcaed75656aa1ad2cc30b4d7ce" +content-hash = "d1f9d88ca70834f069d89f24b82e2109852867a1a58396250c9c4ad89119f9af" diff --git a/pyproject.toml b/pyproject.toml index 65d080cc3..edf00a9ee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ dependencies = [ "pysonar>=1.0.1.1548,<2", "pytest>=7.2.2,<10", "pyupgrade>=2.38.2,<4.0.0", - "pyyaml (>=6.0.3,<7.0.0)", + "ruamel-yaml (>=0.18.0,<=0.18.16)", "ruff>=0.14.5,<0.15", "shibuya>=2024.5.14", "sphinx>=5.3,<8", diff --git a/test/integration/tools/workflow_test.py b/test/integration/tools/workflow_integration_test.py similarity index 100% rename from test/integration/tools/workflow_test.py rename to test/integration/tools/workflow_integration_test.py diff --git a/test/unit/template_test.py b/test/unit/tool_template_test.py similarity index 100% rename from test/unit/template_test.py rename to test/unit/tool_template_test.py diff --git a/test/unit/tools/test_template.py b/test/unit/tools/test_template.py deleted file mode 100644 index 354695805..000000000 --- a/test/unit/tools/test_template.py +++ /dev/null @@ -1,149 +0,0 @@ -from contextlib import ExitStack -from inspect import cleandoc - -import pytest -from yaml.parser import ParserError - -from exasol.toolbox.tools.template import ( - _render_template, -) - -TEMPLATE = """ -name: Publish Documentation - -on: - workflow_call: - workflow_dispatch: - -jobs: - - build-documentation: - runs-on: "(( os_version ))" - permissions: - contents: read - steps: - - name: SCM Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Setup Python & Poetry Environment - uses: exasol/python-toolbox/.github/actions/python-environment@v5 - with: - python-version: "(( minimum_python_version ))" - poetry-version: "(( dependency_manager_version ))" - - - name: Build Documentation - run: | - poetry run -- nox -s docs:multiversion - mv .html-documentation html-documentation - - - name: Upload artifact - uses: actions/upload-pages-artifact@v4 - with: - path: html-documentation - - deploy-documentation: - needs: [ build-documentation ] - permissions: - contents: read - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: "(( os_version ))" - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 - -""" - -RENDERED_TEMPLATE = """ -name: Publish Documentation - -on: - workflow_call: - workflow_dispatch: - -jobs: - - build-documentation: - runs-on: "ubuntu-24.04" - permissions: - contents: read - steps: - - name: SCM Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Setup Python & Poetry Environment - uses: exasol/python-toolbox/.github/actions/python-environment@v5 - with: - python-version: "3.10" - poetry-version: "2.3.0" - - - name: Build Documentation - run: | - poetry run -- nox -s docs:multiversion - mv .html-documentation html-documentation - - - name: Upload artifact - uses: actions/upload-pages-artifact@v4 - with: - path: html-documentation - - deploy-documentation: - needs: [ build-documentation ] - permissions: - contents: read - pages: write - id-token: write - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - runs-on: "ubuntu-24.04" - steps: - - name: Deploy to GitHub Pages - id: deployment - uses: actions/deploy-pages@v4 - -""" - -BAD_TEMPLATE = """ -name: Publish Documentation - -on: - workflow_call: - workflow_dispatch: - -jobs: - - build-documentation: - runs-on: "ubuntu-24.04" - permissions: - contents: read - steps: - - name: SCM Checkout - uses: actions/checkout@v5 -""" - - -class TestRenderTemplate: - @staticmethod - def test_works_as_expected(tmp_path): - file_path = tmp_path / "test.yml" - file_path.write_text(TEMPLATE) - with ExitStack() as stack: - rendered_str = _render_template(src=file_path, stack=stack) - assert rendered_str == cleandoc(RENDERED_TEMPLATE) + "\n" - - @staticmethod - def test_fails_when_yaml_malformed(tmp_path): - file_path = tmp_path / "test.yaml" - file_path.write_text(BAD_TEMPLATE) - with pytest.raises(ParserError, match="while parsing a block collection"): - with ExitStack() as stack: - _render_template(src=file_path, stack=stack) diff --git a/test/unit/util/workflows/template_processing_test.py b/test/unit/util/workflows/template_processing_test.py new file mode 100644 index 000000000..93b6b2dfe --- /dev/null +++ b/test/unit/util/workflows/template_processing_test.py @@ -0,0 +1,178 @@ +from inspect import cleandoc + +from exasol.toolbox.util.workflows.template_processing import TemplateRenderer +from noxconfig import PROJECT_CONFIG + + +class TestTemplateRenderer: + @staticmethod + def test_works_for_general_case(): + input_yaml = """ + name: Build & Publish + + on: + workflow_call: + secrets: + PYPI_TOKEN: + required: true + + jobs: + cd-job: + name: Continuous Delivery + permissions: + contents: write + """ + + template_renderer = TemplateRenderer( + template_str=cleandoc(input_yaml), + github_template_dict=PROJECT_CONFIG.github_template_dict, + ) + assert template_renderer.render_to_workflow() == cleandoc(input_yaml) + + @staticmethod + def test_fixes_extra_horizontal_whitespace(): + # required has 2 extra spaces + input_yaml = """ + name: Build & Publish + + on: + workflow_call: + secrets: + PYPI_TOKEN: + required: true + """ + + expected_yaml = """ + name: Build & Publish + + on: + workflow_call: + secrets: + PYPI_TOKEN: + required: true + """ + + template_renderer = TemplateRenderer( + template_str=cleandoc(input_yaml), + github_template_dict=PROJECT_CONFIG.github_template_dict, + ) + assert template_renderer.render_to_workflow() == cleandoc(expected_yaml) + + @staticmethod + def test_keeps_comments(): + input_yaml = """ + steps: + # Comment in nested area + - name: SCM Checkout # Comment inline + uses: actions/checkout@v6 + # Comment in step + """ + + expected_yaml = """ + steps: + # Comment in nested area + - name: SCM Checkout # Comment inline + uses: actions/checkout@v6 + # Comment in step + """ + + template_renderer = TemplateRenderer( + template_str=cleandoc(input_yaml), + github_template_dict=PROJECT_CONFIG.github_template_dict, + ) + + assert template_renderer.render_to_workflow() == cleandoc(expected_yaml) + + @staticmethod + def test_keeps_quotes_for_variables_as_is(): + input_yaml = """ + - name: Build Artifacts + run: poetry build + - name: PyPi Release + env: + POETRY_HTTP_BASIC_PYPI_USERNAME: "__token__" + POETRY_HTTP_BASIC_PYPI_PASSWORD: "${{ secrets.PYPI_TOKEN }}" + run: poetry publish + - name: GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: > + gh release create ${GITHUB_REF_NAME} + --title ${GITHUB_REF_NAME} + --notes-file ./doc/changes/changes_${GITHUB_REF_NAME}.md + dist/* + """ + + expected_yaml = """ + - name: Build Artifacts + run: poetry build + - name: PyPi Release + env: + POETRY_HTTP_BASIC_PYPI_USERNAME: "__token__" + POETRY_HTTP_BASIC_PYPI_PASSWORD: "${{ secrets.PYPI_TOKEN }}" + run: poetry publish + - name: GitHub Release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: >- + gh release create ${GITHUB_REF_NAME} + --title ${GITHUB_REF_NAME} + --notes-file ./doc/changes/changes_${GITHUB_REF_NAME}.md + dist/* + """ + + template_renderer = TemplateRenderer( + template_str=cleandoc(input_yaml), + github_template_dict=PROJECT_CONFIG.github_template_dict, + ) + + assert template_renderer.render_to_workflow() == cleandoc(expected_yaml) + + @staticmethod + def test_updates_jinja_variables(): + input_yaml = """ + - name: Setup Python & Poetry Environment + uses: exasol/python-toolbox/.github/actions/python-environment@v5 + with: + python-version: "(( minimum_python_version ))" + poetry-version: "(( dependency_manager_version ))" + """ + expected_yaml = """ + - name: Setup Python & Poetry Environment + uses: exasol/python-toolbox/.github/actions/python-environment@v5 + with: + python-version: "3.10" + poetry-version: "2.3.0" + """ + + template_renderer = TemplateRenderer( + template_str=cleandoc(input_yaml), + github_template_dict=PROJECT_CONFIG.github_template_dict, + ) + + assert template_renderer.render_to_workflow() == cleandoc(expected_yaml) + + @staticmethod + def test_preserves_list_format(): + input_yaml = """ + on: + pull_request: + types: [opened, synchronize, reopened] + + Type-Check: + name: Type Checking (Python-${{ matrix.python-versions }}) + runs-on: "ubuntu-24.04" + permissions: + contents: read + strategy: + fail-fast: false + matrix: + python-versions: ["3.10", "3.11", "3.12", "3.13", "3.14"] + """ + + template_renderer = TemplateRenderer( + template_str=cleandoc(input_yaml), + github_template_dict=PROJECT_CONFIG.github_template_dict, + ) + + assert template_renderer.render_to_workflow() == cleandoc(input_yaml) diff --git a/test/unit/util/workflows/workflow_test.py b/test/unit/util/workflows/workflow_test.py new file mode 100644 index 000000000..dafb0665e --- /dev/null +++ b/test/unit/util/workflows/workflow_test.py @@ -0,0 +1,58 @@ +import pytest +from ruamel.yaml.parser import ParserError + +from exasol.toolbox.util.workflows.workflow import Workflow +from noxconfig import PROJECT_CONFIG + +BAD_TEMPLATE = """ +name: Publish Documentation + +on: + workflow_call: + workflow_dispatch: + +jobs: + + build-documentation: + runs-on: "ubuntu-24.04" + permissions: + contents: read + steps: + - name: SCM Checkout + uses: actions/checkout@v5 +""" + +TEMPLATE_DIR = PROJECT_CONFIG.source_code_path / "templates" / "github" / "workflows" + + +class TestWorkflow: + @staticmethod + @pytest.mark.parametrize("template_path", list(TEMPLATE_DIR.glob("*.yml"))) + def test_works_for_all_templates(template_path): + Workflow.load_from_template( + file_path=template_path, + github_template_dict=PROJECT_CONFIG.github_template_dict, + ) + + @staticmethod + def test_fails_when_yaml_does_not_exist(tmp_path): + file_path = tmp_path / "test.yaml" + with pytest.raises(FileNotFoundError, match="test.yaml"): + Workflow.load_from_template( + file_path=file_path, + github_template_dict=PROJECT_CONFIG.github_template_dict, + ) + + @staticmethod + def test_fails_when_yaml_malformed(tmp_path): + file_path = tmp_path / "test.yaml" + file_path.write_text(BAD_TEMPLATE) + + with pytest.raises(ValueError, match="Error rendering file") as excinfo: + Workflow.load_from_template( + file_path=file_path, + github_template_dict=PROJECT_CONFIG.github_template_dict, + ) + + assert isinstance(excinfo.value.__cause__, ParserError) + assert "while parsing a block collection" in str(excinfo.value.__cause__)