From 98966411d05ea10a4b696c860eacf198971ce3ac Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Mon, 26 Jan 2026 17:18:29 +0100 Subject: [PATCH 01/15] Move workflows to be in features --- .../{workflows.rst => features/github_workflows/index.rst} | 2 +- doc/user_guide/features/index.rst | 1 + doc/user_guide/user_guide.rst | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) rename doc/user_guide/{workflows.rst => features/github_workflows/index.rst} (97%) diff --git a/doc/user_guide/workflows.rst b/doc/user_guide/features/github_workflows/index.rst similarity index 97% rename from doc/user_guide/workflows.rst rename to doc/user_guide/features/github_workflows/index.rst index ea4752e48..4f5d0d8cb 100644 --- a/doc/user_guide/workflows.rst +++ b/doc/user_guide/features/github_workflows/index.rst @@ -3,7 +3,7 @@ GitHub Workflows ================ -.. figure:: ../_static/github-workflows.png +.. figure:: ../../../_static/github-workflows.png :alt: GitHub Workflow Example The exasol-toolbox ships with various GitHub workflow templates. To leverage the full feature set of the toolbox, you should use them. diff --git a/doc/user_guide/features/index.rst b/doc/user_guide/features/index.rst index 6a57544fd..bc0e7555b 100644 --- a/doc/user_guide/features/index.rst +++ b/doc/user_guide/features/index.rst @@ -10,6 +10,7 @@ Features creating_a_release documentation/index git_hooks/index + github_workflows/index formatting_code/index managing_dependencies diff --git a/doc/user_guide/user_guide.rst b/doc/user_guide/user_guide.rst index 1e2957b3e..2effc68e9 100644 --- a/doc/user_guide/user_guide.rst +++ b/doc/user_guide/user_guide.rst @@ -10,6 +10,5 @@ getting_started configuration features/index - workflows customization migrating From 362906dd42012e1fb335fc8336d5a72a66e035d1 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Mon, 26 Jan 2026 17:24:50 +0100 Subject: [PATCH 02/15] Move section to configuration --- .../github_workflows/configuration.rst | 32 +++++++++++++++++ .../features/github_workflows/index.rst | 35 ++++--------------- 2 files changed, 38 insertions(+), 29 deletions(-) create mode 100644 doc/user_guide/features/github_workflows/configuration.rst diff --git a/doc/user_guide/features/github_workflows/configuration.rst b/doc/user_guide/features/github_workflows/configuration.rst new file mode 100644 index 000000000..df24b91d9 --- /dev/null +++ b/doc/user_guide/features/github_workflows/configuration.rst @@ -0,0 +1,32 @@ +.. _github_workflows_configuration: + +Configuration +============= + +1. Configure the GitHub project ++++++++++++++++++++++++++++++++ + +* Make sure your GitHub project has access to a deployment token for PyPi with the following name: **PYPI_TOKEN**. It should be available to the repository either as an Organization-, Repository-, or Environment-secret. + +* If your CI workflow involves slow or expensive steps you can guard these to be executed only after manual approval. The CI workflow will automaticallu create a GitHub environment named :code:`manual-approval`. You only need to add reviewers in (:code:`Settings/Environments/manual-approval`) and move the steps to be guarded into the related section in job :code:`slow-checks` in file :code:`.github/workflows/merge-gate.yml`. + +2. Add all workflows to your project +++++++++++++++++++++++++++++++++++++ + +.. code-block:: shell + + tbx workflow install all + +.. warning:: + + #. If you already have various workflows, you may want to run the :code:`update` command instead of the :code:`install` command. + + #. Some workflows depend on other workflows. Please ensure you have all the required workflows if you do not install all of them. + +3. Update Branch Protection +++++++++++++++++++++++++++++ + +The best and most maintainable way to have solid branch protection (:code:`Settings/Branches/main`) is to require the workflow :code:`CI / Allow Merge` to pass successfully. + +.. note:: + Setting the required status checks to pass before merging is only possible after running a CI build at **least once** on the affected branch. diff --git a/doc/user_guide/features/github_workflows/index.rst b/doc/user_guide/features/github_workflows/index.rst index 4f5d0d8cb..7d5da07fa 100644 --- a/doc/user_guide/features/github_workflows/index.rst +++ b/doc/user_guide/features/github_workflows/index.rst @@ -3,6 +3,12 @@ GitHub Workflows ================ +.. toctree:: + :maxdepth: 2 + :hidden: + + configuration + .. figure:: ../../../_static/github-workflows.png :alt: GitHub Workflow Example @@ -18,32 +24,3 @@ The exasol-toolbox ships with various GitHub workflow templates. To leverage the The toolbox command itself, :code:`tbx`, provides various CLI functions to help you maintain those workflows. For further help, run the command :code:`tbx workflow --help`. - - -1. Configure the GitHub project -+++++++++++++++++++++++++++++++ - -* Make sure your GitHub project has access to a deployment token for PyPi with the following name: **PYPI_TOKEN**. It should be available to the repository either as an Organization-, Repository-, or Environment-secret. - -* If your CI workflow involves slow or expensive steps you can guard these to be executed only after manual approval. The CI workflow will automaticall create a GitHub environment named :code:`manual-approval`. You only need to add reviewers in (:code:`Settings/Environments/manual-approval`) and move the steps to be guarded into the related section in job :code:`slow-checks` in file :code:`.github/workflows/merge-gate.yml`. - -2. Add all workflows to your project -++++++++++++++++++++++++++++++++++++ - -.. code-block:: shell - - tbx workflow install all - -.. warning:: - - #. If you already have various workflows, you may want to run the :code:`update` command instead of the :code:`install` command. - - #. Some workflows depend on other workflows. Please ensure you have all the required workflows if you do not install all of them. - -3. Update Branch Protection -++++++++++++++++++++++++++++ - -The best and most maintainable way to have solid branch protection (:code:`Settings/Branches/main`) is to require the workflow :code:`CI / Allow Merge` to pass successfully. - -.. note:: - Setting the required status checks to pass before merging is only possible after running a CI build at **least once** on the affected branch. From 1fadf2b1a024b65ad0da59d9b246310fed7d4175 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 27 Jan 2026 09:20:51 +0100 Subject: [PATCH 03/15] Add dependency for mermaid diagrams in sphinx --- doc/conf.py | 1 + poetry.lock | 22 +++++++++++++++++++++- pyproject.toml | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index b1d64b738..2a2aa812a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -33,6 +33,7 @@ "sphinx.ext.napoleon", "sphinx.ext.intersphinx", "sphinx.ext.autosummary", + "sphinxcontrib.mermaid", "sphinx_toolbox.collapse", "sphinx_copybutton", "myst_parser", diff --git a/poetry.lock b/poetry.lock index 029cfc7cf..94fb0f9bd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3563,6 +3563,26 @@ files = [ [package.extras] test = ["flake8", "mypy", "pytest"] +[[package]] +name = "sphinxcontrib-mermaid" +version = "2.0.0" +description = "Mermaid diagrams in your Sphinx-powered docs" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "sphinxcontrib_mermaid-2.0.0-py3-none-any.whl", hash = "sha256:59a73249bbee2c74b1a4db036f8e8899ade65982bdda6712cf22b4f4e9874bb5"}, + {file = "sphinxcontrib_mermaid-2.0.0.tar.gz", hash = "sha256:cf4f7d453d001132eaba5d1fdf53d42049f02e913213cf8337427483bfca26f4"}, +] + +[package.dependencies] +jinja2 = "*" +pyyaml = "*" +sphinx = "*" + +[package.extras] +test = ["defusedxml", "myst-parser", "pytest", "ruff", "sphinx"] + [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" @@ -3923,4 +3943,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4.0" -content-hash = "27476fa095d516f571fec7473aa0018edf057a53bf5a2934182915d937997ef0" +content-hash = "8a5d05c9cc28f8599edd5055881b350ebf0588dcaed75656aa1ad2cc30b4d7ce" diff --git a/pyproject.toml b/pyproject.toml index 9c449a64f..dd37f49d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,7 @@ dependencies = [ "sphinx-toolbox>=4.0.0,<5", "typer[all]>=0.7.0", "twine>=6.1.0,<7", + "sphinxcontrib-mermaid (>=2.0.0,<3.0.0)", ] [project.scripts] From da100a86910b1792e203f50e1dbdf33fef5b8a6a Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 27 Jan 2026 09:46:09 +0100 Subject: [PATCH 04/15] Update with a description & flowcharts --- doc/_static/github-workflows.png | Bin 61535 -> 0 bytes .../features/github_workflows/index.rst | 166 +++++++++++++++++- 2 files changed, 158 insertions(+), 8 deletions(-) delete mode 100644 doc/_static/github-workflows.png diff --git a/doc/_static/github-workflows.png b/doc/_static/github-workflows.png deleted file mode 100644 index 885249ed1a190ca51493a92a31d9f1a0e896170a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61535 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 YoISF~vX!c;y+980boFyt=akR{0IO4j9{>OV diff --git a/doc/user_guide/features/github_workflows/index.rst b/doc/user_guide/features/github_workflows/index.rst index 7d5da07fa..985e9da87 100644 --- a/doc/user_guide/features/github_workflows/index.rst +++ b/doc/user_guide/features/github_workflows/index.rst @@ -9,18 +9,168 @@ GitHub Workflows configuration -.. figure:: ../../../_static/github-workflows.png - :alt: GitHub Workflow Example +The PTB ships with partially configurable GitHub workflow templates that cover the most common Python +CI/CD setup. The templates are defined in: +`exasol/toolbox/templates/github/workflows `__. -The exasol-toolbox ships with various GitHub workflow templates. To leverage the full feature set of the toolbox, you should use them. +To use, update, and otherwise interact with these templates, a CLI has been provided: + +.. code-block:: bash + + poetry run -- tbx workflow --help .. attention:: - Generally, it is advised to install/use all workflows provided by the toolbox as a whole due to their interdependencies. + Generally, it is advised to use all workflows provided by the PTB due to their + interdependencies. For more details of how the workflows work together, see + :ref:`ci_actions`. + +Underlying the CLI, the PTB uses Jinja to dynamically generate project-specific +workflows. The rendering process is supported by the ``github_template_dict`` found in +your ``noxconfig.py::PROJECT_CONFIG``. This dictionary is inherited by default from +``BaseConfig.py``, ensuring a standardized baseline that can be easily overridden, if +necessary. + +.. literalinclude:: ../../../../exasol/toolbox/config.py + :language: python + :start-at: github_template_dict + + +Workflows +--------- + +.. list-table:: + :widths: 25 25 50 + :header-rows: 1 + + * - Filename + - Run on + - Description + * - ``build-and-publish.yml`` + - Workflow call + - Packages the distribution and releases it to PyPi and GitHub. + * - ``cd.yml`` + - Push with new tag + - Manages continuous delivery by calling ``check-release-tag.yml``, + ``build-and-publish.yml``, and ``gh-pages.yml``. See :ref:`cd_yml` + for a graph of called workflows. + * - ``check-release-tag.yml`` + - Workflow call + - Verifies that the release tag matches the project's internal versioning. + * - ``checks.yml`` + - Workflow call + - Executes many small & fast checks: checks links of and builds documentation, + runs various linters (security, type checks, etc.), and unit tests. + * - ``ci.yml`` + - Pull request and monthly + - Executes the continuous integration suite by calling ``merge-gate.yml`` and + ``report.yml``. See :ref:`ci_yml` for a graph of called workflows. + * - ``gh-pages.yml`` + - Workflow call + - Builds the documentation and deploys it to GitHub Pages. + * - ``matrix-all.yml`` + - Workflow call + - Calls on a nox session ``matrix:all``, which typically has ``exasol_versions`` + and ``python_versions`` from the ``PROJECT_CONFIG``. + * - ``matrix-exasol.yml`` + - Workflow call + - Calls on a nox session ``matrix:exasol`` to get the ``exasol_versions`` from the + ``PROJECT_CONFIG``. + * - ``matrix-python.yml`` + - Workflow call + - Calls on a nox session ``matrix:python`` to get the ``python_versions`` from the + ``PROJECT_CONFIG``. + * - ``merge-gate.yml`` + - Workflow call + - Acts as a final status check (gatekeeper) to ensure all required CI steps have + passed before allowing a merge. + * - ``pr-merge.yml`` + - Push to main + - Runs ``checks.yml``, ``gh-pages.yml``, and ``report.yml``. See + :ref:`pr_merge_yml` for a graph of called workflows. + * - ``report.yml`` + - Workflow call + - Downloads coverage and linting information, creates a summary for GitHub action, + and uploads the raw results to Sonar for evaluation. + * - ``slow-checks.yml`` + - Workflow call + - Runs long-running checks, which typically involve an Exasol database. + +.. _ci_actions: + +CI Actions +---------- + +.. _ci_yml: + +Pull Request +^^^^^^^^^^^^ + +When any pull request is opened, synchronized, or reopened, then the ``ci.yml`` will be +triggered. + +When configured as described on :ref:`github_workflows_configuration`, the +``run-slow-tests`` job requires a developer to manually approve running the slower +workflows, like ``slow-checks.yml``. This allows developers to update their pull +request more often and to only periodically run the more time-expensive tests. + +If one of the jobs in the chain fails (or is ``run-slow-tests`` is not approved), +then the subsequent jobs will not be started. + +.. mermaid:: + + graph TD + %% Workflow Triggers (Solid Lines) + ci[ci.yml] --> merge-gate[merge-gate.yml] + ci --> metrics[report.yml] + + merge-gate --> fast-checks[checks.yml] + merge-gate --> run-slow-tests[run-slow-tests] + run-slow-tests -.->|needs| slow-checks[slow-checks.yml] + + %% Dependencies / Waiting (Dotted Lines) + fast-checks -.->|needs| approve-merge[approve-merge] + slow-checks -.->|needs| approve-merge + + %% Final Dependency + approve-merge -.->|needs| metrics + + %% Visual Styling to distinguish jobs + style approve-merge fill:#fff,stroke:#333,stroke-dasharray: 5 5 + style run-slow-tests fill:#fff,stroke:#333,stroke-dasharray: 5 5 + + +.. _pr_merge_yml: + +Merge +^^^^^ + +When a pull request is merged to main, then the ``pr-merge.yml`` workflow is activated. + +.. mermaid:: + + graph TD + %% Workflow Triggers (Solid Lines) + pr-merge[pr-merge.yml] --> checks[checks.yml] + pr-merge --> publish-docs[publish-docs.yml] + + %% Dependencies / Waiting (Dotted Lines) + checks -.->|needs| report[report.yml] + +.. _cd_yml: + +Release +^^^^^^^ + +When the nox session ``release:trigger`` is used, a new tag is created & pushed +to main. This starts the release process by activating the ``cd.yml`` workflow. - However, if you know what you are doing and are well-versed in GitHub workflows and actions, you can use just select individual ones or use them as inspiration. Still, an individual approach is likely to be more error-prone. +.. mermaid:: -.. note:: + graph TD + %% Workflow Triggers (Solid Lines) + cd[cd.yml] --> check-release-tag[check-release-tag.yml] - The toolbox command itself, :code:`tbx`, provides various CLI functions to help you maintain those workflows. - For further help, run the command :code:`tbx workflow --help`. + %% Dependencies / Waiting (Dotted Lines) + check-release-tag -.->|needs| build-and-publish[build-and-publish.yml] + build-and-publish -.->|needs| gh-pages[gh-pages.yml] From b02eb60ba425b0f7e3cbcf2a377dc69324a58984 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 27 Jan 2026 09:51:30 +0100 Subject: [PATCH 05/15] Add changelog entry --- doc/changes/unreleased.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/doc/changes/unreleased.md b/doc/changes/unreleased.md index 549f78b52..11163776b 100644 --- a/doc/changes/unreleased.md +++ b/doc/changes/unreleased.md @@ -5,3 +5,7 @@ ## Feature * #673: Switched `checks.yml` to get Python versions for matrix from `BaseConfig` + +## Documentation + +* #676: Move GitHub Workflows to be inside features & updated From a5d7d3af552bfb5b4b9d51f65b4bfe117d58374d Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 27 Jan 2026 09:53:33 +0100 Subject: [PATCH 06/15] Fix typo --- doc/user_guide/features/github_workflows/configuration.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user_guide/features/github_workflows/configuration.rst b/doc/user_guide/features/github_workflows/configuration.rst index df24b91d9..aa654fcaa 100644 --- a/doc/user_guide/features/github_workflows/configuration.rst +++ b/doc/user_guide/features/github_workflows/configuration.rst @@ -8,7 +8,7 @@ Configuration * Make sure your GitHub project has access to a deployment token for PyPi with the following name: **PYPI_TOKEN**. It should be available to the repository either as an Organization-, Repository-, or Environment-secret. -* If your CI workflow involves slow or expensive steps you can guard these to be executed only after manual approval. The CI workflow will automaticallu create a GitHub environment named :code:`manual-approval`. You only need to add reviewers in (:code:`Settings/Environments/manual-approval`) and move the steps to be guarded into the related section in job :code:`slow-checks` in file :code:`.github/workflows/merge-gate.yml`. +* If your CI workflow involves slow or expensive steps you can guard these to be executed only after manual approval. The CI workflow will automatically create a GitHub environment named :code:`manual-approval`. You only need to add reviewers in (:code:`Settings/Environments/manual-approval`) and move the steps to be guarded into the related section in job :code:`slow-checks` in file :code:`.github/workflows/merge-gate.yml`. 2. Add all workflows to your project ++++++++++++++++++++++++++++++++++++ From 495d9275ebe0c6a971ce93a3f673012657f2eb14 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 27 Jan 2026 10:55:00 +0100 Subject: [PATCH 07/15] Switch to gerund --- doc/user_guide/features/github_workflows/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/user_guide/features/github_workflows/index.rst b/doc/user_guide/features/github_workflows/index.rst index 985e9da87..f33d1e7ff 100644 --- a/doc/user_guide/features/github_workflows/index.rst +++ b/doc/user_guide/features/github_workflows/index.rst @@ -1,7 +1,7 @@ .. _GitHub Workflows: -GitHub Workflows -================ +Enabling GitHub Workflows +========================= .. toctree:: :maxdepth: 2 From 0817e5e31d6976e4f091b46c83473051ec1d5a10 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 27 Jan 2026 13:12:24 +0100 Subject: [PATCH 08/15] Part 1: Apply reviewer comments --- .../features/github_workflows/index.rst | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/doc/user_guide/features/github_workflows/index.rst b/doc/user_guide/features/github_workflows/index.rst index f33d1e7ff..db10eb860 100644 --- a/doc/user_guide/features/github_workflows/index.rst +++ b/doc/user_guide/features/github_workflows/index.rst @@ -9,11 +9,12 @@ Enabling GitHub Workflows configuration -The PTB ships with partially configurable GitHub workflow templates that cover the most common Python -CI/CD setup. The templates are defined in: +The PTB ships with configurable GitHub workflow templates covering the most common +CI/CD setup variants for Python projects. The templates are defined in: `exasol/toolbox/templates/github/workflows `__. -To use, update, and otherwise interact with these templates, a CLI has been provided: +The PTB provides a command line interface (CLI) for generating and updating actual +workflows from the templates. .. code-block:: bash @@ -21,9 +22,8 @@ To use, update, and otherwise interact with these templates, a CLI has been prov .. attention:: - Generally, it is advised to use all workflows provided by the PTB due to their - interdependencies. For more details of how the workflows work together, see - :ref:`ci_actions`. + In most cases, we recommend using _all_ workflows without change to ensure + consistent interdependencies. For more details, see :ref:`ci_actions`. Underlying the CLI, the PTB uses Jinja to dynamically generate project-specific workflows. The rendering process is supported by the ``github_template_dict`` found in @@ -48,37 +48,38 @@ Workflows - Description * - ``build-and-publish.yml`` - Workflow call - - Packages the distribution and releases it to PyPi and GitHub. + - Packages the distribution and publishes it to PyPi and GitHub. * - ``cd.yml`` - Push with new tag - Manages continuous delivery by calling ``check-release-tag.yml``, ``build-and-publish.yml``, and ``gh-pages.yml``. See :ref:`cd_yml` - for a graph of called workflows. + for a graph of workflow calls. * - ``check-release-tag.yml`` - Workflow call - Verifies that the release tag matches the project's internal versioning. * - ``checks.yml`` - Workflow call - - Executes many small & fast checks: checks links of and builds documentation, - runs various linters (security, type checks, etc.), and unit tests. + - Executes many small & fast checks: builds documentation and validates + cross-references (AKA. "links") to be valid,runs various linters + (security, type checks, etc.), and unit tests. * - ``ci.yml`` - Pull request and monthly - Executes the continuous integration suite by calling ``merge-gate.yml`` and - ``report.yml``. See :ref:`ci_yml` for a graph of called workflows. + ``report.yml``. See :ref:`ci_yml` for a graph of workflow calls. * - ``gh-pages.yml`` - Workflow call - Builds the documentation and deploys it to GitHub Pages. * - ``matrix-all.yml`` - Workflow call - - Calls on a nox session ``matrix:all``, which typically has ``exasol_versions`` + - Calls Nox session ``matrix:all``, which typically evaluates ``exasol_versions`` and ``python_versions`` from the ``PROJECT_CONFIG``. * - ``matrix-exasol.yml`` - Workflow call - - Calls on a nox session ``matrix:exasol`` to get the ``exasol_versions`` from the + - Calls Nox session ``matrix:exasol`` to get the ``exasol_versions`` from the ``PROJECT_CONFIG``. * - ``matrix-python.yml`` - Workflow call - - Calls on a nox session ``matrix:python`` to get the ``python_versions`` from the + - Calls Nox session ``matrix:python`` to get the ``python_versions`` from the ``PROJECT_CONFIG``. * - ``merge-gate.yml`` - Workflow call From 8fa498eb9ce47a697f8e24f544e845904dbe4d50 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 27 Jan 2026 13:15:53 +0100 Subject: [PATCH 09/15] Part 2: Apply reviewer comments --- doc/user_guide/features/github_workflows/index.rst | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/doc/user_guide/features/github_workflows/index.rst b/doc/user_guide/features/github_workflows/index.rst index db10eb860..7a899caa0 100644 --- a/doc/user_guide/features/github_workflows/index.rst +++ b/doc/user_guide/features/github_workflows/index.rst @@ -84,18 +84,20 @@ Workflows * - ``merge-gate.yml`` - Workflow call - Acts as a final status check (gatekeeper) to ensure all required CI steps have - passed before allowing a merge. + passed before allowing to merge the branch of your pull request to the + default branch of the repository. e.g. ``main``. * - ``pr-merge.yml`` - Push to main - Runs ``checks.yml``, ``gh-pages.yml``, and ``report.yml``. See :ref:`pr_merge_yml` for a graph of called workflows. * - ``report.yml`` - Workflow call - - Downloads coverage and linting information, creates a summary for GitHub action, - and uploads the raw results to Sonar for evaluation. + - Downloads results from code coverage analysis and linting, + creates a summary displayed by GitHub as result of running + the action, and uploads the results to Sonar. * - ``slow-checks.yml`` - Workflow call - - Runs long-running checks, which typically involve an Exasol database. + - Runs long-running checks, which typically involve an Exasol database instance. .. _ci_actions: From f4814ffb73ac319cafa7d0174d2e2504845f9540 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 27 Jan 2026 13:17:58 +0100 Subject: [PATCH 10/15] Part 3: Apply reviewer comments --- doc/user_guide/features/github_workflows/index.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/user_guide/features/github_workflows/index.rst b/doc/user_guide/features/github_workflows/index.rst index 7a899caa0..ac2ed348f 100644 --- a/doc/user_guide/features/github_workflows/index.rst +++ b/doc/user_guide/features/github_workflows/index.rst @@ -113,11 +113,11 @@ When any pull request is opened, synchronized, or reopened, then the ``ci.yml`` triggered. When configured as described on :ref:`github_workflows_configuration`, the -``run-slow-tests`` job requires a developer to manually approve running the slower +``run-slow-tests`` job requires a developer to manually approve executing the slower workflows, like ``slow-checks.yml``. This allows developers to update their pull request more often and to only periodically run the more time-expensive tests. -If one of the jobs in the chain fails (or is ``run-slow-tests`` is not approved), +If one of the jobs in the chain fails (or if ``run-slow-tests`` is not approved), then the subsequent jobs will not be started. .. mermaid:: From 44fb79e69345b94b12aa336ac66ee88c9a0ef9b7 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 27 Jan 2026 14:48:06 +0100 Subject: [PATCH 11/15] Switch to using yaml to output string and prevent 'on' from being converted to 'True' --- exasol/toolbox/tools/template.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/exasol/toolbox/tools/template.py b/exasol/toolbox/tools/template.py index 14829ea1f..33030d309 100644 --- a/exasol/toolbox/tools/template.py +++ b/exasol/toolbox/tools/template.py @@ -15,6 +15,7 @@ from rich.columns import Columns from rich.console import Console from rich.syntax import Syntax +from yaml.resolver import Resolver from noxconfig import PROJECT_CONFIG @@ -70,6 +71,16 @@ def show_templates( ) # type: ignore +# yaml uses a shorthand to identify "on" and "off" tags. +# for GitHub workflows, we do NOT want "on" replaced with "True". +for character in ["O", "o"]: + Resolver.yaml_implicit_resolvers[character] = [ + x + for x in Resolver.yaml_implicit_resolvers[character] + if x[0] != "tag:yaml.org,2002:bool" + ] + + def _render_template( src: str | Path, stack: ExitStack, @@ -80,10 +91,14 @@ def _render_template( 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" + # this line also checks that the rendered content is a valid YAML + data = yaml.safe_load(rendered_string) + output = yaml.safe_dump( + data, + sort_keys=False, # if True, then re-orders the jobs alphabetically + ) + + return cleandoc(output) + "\n" def diff_template(template: str, dest: Path, pkg: str, template_type: str) -> None: From 93cb0bf649db3b0c05c292289a07b55c75c6cb57 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 27 Jan 2026 14:54:10 +0100 Subject: [PATCH 12/15] Format so empty fields left without null --- exasol/toolbox/tools/template.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/exasol/toolbox/tools/template.py b/exasol/toolbox/tools/template.py index 33030d309..51d99b410 100644 --- a/exasol/toolbox/tools/template.py +++ b/exasol/toolbox/tools/template.py @@ -81,6 +81,24 @@ def show_templates( ] +class GitHubDumper(yaml.SafeDumper): + pass + + +def empty_representer(dumper, data): + """ + Leave empty fields without 'null' + + on: + workflow_call: + """ + return dumper.represent_scalar("tag:yaml.org,2002:null", "") + + +# Register it to the dumper +GitHubDumper.add_representer(type(None), empty_representer) + + def _render_template( src: str | Path, stack: ExitStack, @@ -93,8 +111,9 @@ def _render_template( # this line also checks that the rendered content is a valid YAML data = yaml.safe_load(rendered_string) - output = yaml.safe_dump( + output = yaml.dump( data, + Dumper=GitHubDumper, sort_keys=False, # if True, then re-orders the jobs alphabetically ) From f14d9c1ffed6a91430aa5533cb066bf47d60d1d9 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 27 Jan 2026 14:58:03 +0100 Subject: [PATCH 13/15] Keep | lines instead of quoting instead --- exasol/toolbox/tools/template.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/exasol/toolbox/tools/template.py b/exasol/toolbox/tools/template.py index 51d99b410..5afe88aa1 100644 --- a/exasol/toolbox/tools/template.py +++ b/exasol/toolbox/tools/template.py @@ -95,7 +95,15 @@ def empty_representer(dumper, data): return dumper.represent_scalar("tag:yaml.org,2002:null", "") +def str_presenter(dumper, data): + # Use literal style '|' for strings with newlines + if "\n" in data: + return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|") + return dumper.represent_scalar("tag:yaml.org,2002:str", data) + + # Register it to the dumper +GitHubDumper.add_representer(str, str_presenter) GitHubDumper.add_representer(type(None), empty_representer) From 13155dacc3632cc9da829f5466d04656c0cd6e34 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 27 Jan 2026 15:08:36 +0100 Subject: [PATCH 14/15] Keep quoting versioned strings --- exasol/toolbox/tools/template.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/exasol/toolbox/tools/template.py b/exasol/toolbox/tools/template.py index 5afe88aa1..73fa4b857 100644 --- a/exasol/toolbox/tools/template.py +++ b/exasol/toolbox/tools/template.py @@ -1,5 +1,6 @@ import difflib import io +import re from collections.abc import Mapping from contextlib import ExitStack from inspect import cleandoc @@ -95,10 +96,19 @@ def empty_representer(dumper, data): return dumper.represent_scalar("tag:yaml.org,2002:null", "") +# Regex for common strings that lose quotes: +# 1. Version numbers (e.g., 2.3.0, 3.10) +# 2. OS/image names (e.g., ubuntu-24.04) +# 3. Numeric strings that look like octals or floats (e.g., 045, 1.2) +QUOTE_REGEX = re.compile(r"^(\d+\.\d+(\.\d+)?|[a-zA-Z]+-\d+\.\d+|0\d+)$") + + def str_presenter(dumper, data): # Use literal style '|' for strings with newlines if "\n" in data: return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|") + if QUOTE_REGEX.match(data): + return dumper.represent_scalar("tag:yaml.org,2002:str", data, style='"') return dumper.represent_scalar("tag:yaml.org,2002:str", data) From 8fe80f2179da261b50d452f231ce30d56d2f1817 Mon Sep 17 00:00:00 2001 From: Ariel Schulz Date: Tue, 27 Jan 2026 15:14:36 +0100 Subject: [PATCH 15/15] Try with new workflow --- .github/workflows/checks.yml | 310 +++++++++++++++++------------------ 1 file changed, 151 insertions(+), 159 deletions(-) diff --git a/.github/workflows/checks.yml b/.github/workflows/checks.yml index 37a7c4983..98a6415cd 100644 --- a/.github/workflows/checks.yml +++ b/.github/workflows/checks.yml @@ -1,8 +1,6 @@ name: Checks - on: workflow_call: - jobs: Version-Check: name: Version @@ -10,44 +8,38 @@ jobs: permissions: contents: read steps: - - name: SCM Checkout - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Setup Python & Poetry Environment - uses: ./.github/actions/python-environment - with: - python-version: "3.10" - poetry-version: "2.3.0" - - - name: Check Version(s) - run: poetry run -- nox -s version:check - + - name: SCM Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + - name: Setup Python & Poetry Environment + uses: ./.github/actions/python-environment + with: + python-version: "3.10" + poetry-version: "2.3.0" + - name: Check Version(s) + run: poetry run -- nox -s version:check Documentation: name: Docs - needs: [ Version-Check ] + needs: + - Version-Check runs-on: "ubuntu-24.04" permissions: contents: read steps: - - name: SCM Checkout - uses: actions/checkout@v6 - - - name: Setup Python & Poetry Environment - uses: ./.github/actions/python-environment - with: - python-version: "3.10" - poetry-version: "2.3.0" - - - name: Build Documentation - run: | - poetry run -- nox -s docs:build - - - name: Link Check - run: | - poetry run -- nox -s links:check - + - name: SCM Checkout + uses: actions/checkout@v6 + - name: Setup Python & Poetry Environment + uses: ./.github/actions/python-environment + with: + python-version: "3.10" + poetry-version: "2.3.0" + - name: Build Documentation + run: | + poetry run -- nox -s docs:build + - name: Link Check + run: | + poetry run -- nox -s links:check Changelog: name: Changelog Update Check runs-on: "ubuntu-24.04" @@ -55,143 +47,143 @@ jobs: contents: read if: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/master' }} steps: - - name: SCM Checkout - uses: actions/checkout@v6 - - - name: Setup Python & Poetry Environment - uses: ./.github/actions/python-environment - with: - python-version: "3.10" - poetry-version: "2.3.0" - - - name: Run changelog update check - run: poetry run -- nox -s changelog:updated - + - name: SCM Checkout + uses: actions/checkout@v6 + - name: Setup Python & Poetry Environment + uses: ./.github/actions/python-environment + with: + python-version: "3.10" + poetry-version: "2.3.0" + - name: Run changelog update check + run: poetry run -- nox -s changelog:updated Lint: name: Linting (Python-${{ matrix.python-versions }}) - needs: [ Version-Check ] + needs: + - Version-Check 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"] + python-versions: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" steps: - - name: SCM Checkout - uses: actions/checkout@v6 - - - name: Setup Python & Poetry Environment - uses: ./.github/actions/python-environment - with: - python-version: ${{ matrix.python-versions }} - poetry-version: "2.3.0" - - - name: Run lint - run: poetry run -- nox -s lint:code - - - name: Upload Artifacts - uses: actions/upload-artifact@v6 - with: - name: lint-python${{ matrix.python-versions }} - path: | - .lint.txt - .lint.json - include-hidden-files: true - + - name: SCM Checkout + uses: actions/checkout@v6 + - name: Setup Python & Poetry Environment + uses: ./.github/actions/python-environment + with: + python-version: ${{ matrix.python-versions }} + poetry-version: "2.3.0" + - name: Run lint + run: poetry run -- nox -s lint:code + - name: Upload Artifacts + uses: actions/upload-artifact@v6 + with: + name: lint-python${{ matrix.python-versions }} + path: | + .lint.txt + .lint.json + include-hidden-files: true Type-Check: name: Type Checking (Python-${{ matrix.python-versions }}) - needs: [ Version-Check ] + needs: + - Version-Check 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"] - + python-versions: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" steps: - - name: SCM Checkout - uses: actions/checkout@v6 - - - name: Setup Python & Poetry Environment - uses: ./.github/actions/python-environment - with: - python-version: ${{ matrix.python-versions }} - poetry-version: "2.3.0" - - - name: Run type-check - run: poetry run -- nox -s lint:typing - + - name: SCM Checkout + uses: actions/checkout@v6 + - name: Setup Python & Poetry Environment + uses: ./.github/actions/python-environment + with: + python-version: ${{ matrix.python-versions }} + poetry-version: "2.3.0" + - name: Run type-check + run: poetry run -- nox -s lint:typing Security: name: Security Checks (Python-${{ matrix.python-versions }}) - needs: [ Version-Check ] + needs: + - Version-Check 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"] - + python-versions: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" steps: - - name: SCM Checkout - uses: actions/checkout@v6 - - - name: Setup Python & Poetry Environment - uses: ./.github/actions/python-environment - with: - python-version: ${{ matrix.python-versions }} - poetry-version: "2.3.0" - - - name: Run security linter - run: poetry run -- nox -s lint:security - - - name: Upload Artifacts - uses: actions/upload-artifact@v6 - with: - name: security-python${{ matrix.python-versions }} - path: .security.json - include-hidden-files: true - + - name: SCM Checkout + uses: actions/checkout@v6 + - name: Setup Python & Poetry Environment + uses: ./.github/actions/python-environment + with: + python-version: ${{ matrix.python-versions }} + poetry-version: "2.3.0" + - name: Run security linter + run: poetry run -- nox -s lint:security + - name: Upload Artifacts + uses: actions/upload-artifact@v6 + with: + name: security-python${{ matrix.python-versions }} + path: .security.json + include-hidden-files: true Format: name: Format Check runs-on: "ubuntu-24.04" permissions: contents: read steps: - - name: SCM Checkout - uses: actions/checkout@v6 - - - name: Setup Python & Poetry Environment - uses: ./.github/actions/python-environment - with: - python-version: "3.10" - poetry-version: "2.3.0" - - - name: Run format check - run: poetry run -- nox -s format:check - - + - name: SCM Checkout + uses: actions/checkout@v6 + - name: Setup Python & Poetry Environment + uses: ./.github/actions/python-environment + with: + python-version: "3.10" + poetry-version: "2.3.0" + - name: Run format check + run: poetry run -- nox -s format:check Build-Packages: name: Build Package Check - needs: [ Documentation, Lint, Type-Check, Security, Format ] + needs: + - Documentation + - Lint + - Type-Check + - Security + - Format runs-on: "ubuntu-24.04" permissions: contents: read steps: - - name: SCM Checkout - uses: actions/checkout@v6 - - - name: Setup Python & Poetry Environment - uses: ./.github/actions/python-environment - with: - python-version: "3.10" - poetry-version: "2.3.0" - - - name: Run Distribution Check - run: poetry run -- nox -s package:check - + - name: SCM Checkout + uses: actions/checkout@v6 + - name: Setup Python & Poetry Environment + uses: ./.github/actions/python-environment + with: + python-version: "3.10" + poetry-version: "2.3.0" + - name: Run Distribution Check + run: poetry run -- nox -s package:check Lint-Imports: name: Lint Imports runs-on: ubuntu-24.04 @@ -200,46 +192,46 @@ jobs: steps: - name: SCM Checkout uses: actions/checkout@v6 - - name: Setup Python & Poetry Environment uses: ./.github/actions/python-environment with: python-version: "3.10" poetry-version: "2.3.0" - - name: Run import linter run: poetry run -- nox -s lint:import - Tests: name: Unit-Tests (Python-${{ matrix.python-versions }}) - needs: [ Build-Packages, Lint-Imports ] + needs: + - Build-Packages + - Lint-Imports 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"] - + python-versions: + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" steps: - - name: SCM Checkout - uses: actions/checkout@v6 - # The PTB has unit tests which require the fetch-depth to be 0. - with: - fetch-depth: 0 - - - name: Setup Python & Poetry Environment - uses: ./.github/actions/python-environment - with: - python-version: ${{ matrix.python-versions }} - poetry-version: "2.3.0" - - - name: Run Tests and Collect Coverage - run: poetry run -- nox -s test:unit -- --coverage - - - name: Upload Artifacts - uses: actions/upload-artifact@v6 - with: - name: coverage-python${{ matrix.python-versions }}-fast - path: .coverage - include-hidden-files: true + - name: SCM Checkout + uses: actions/checkout@v6 + # The PTB has unit tests which require the fetch-depth to be 0. + with: + fetch-depth: 0 + - name: Setup Python & Poetry Environment + uses: ./.github/actions/python-environment + with: + python-version: ${{ matrix.python-versions }} + poetry-version: "2.3.0" + - name: Run Tests and Collect Coverage + run: poetry run -- nox -s test:unit -- --coverage + - name: Upload Artifacts + uses: actions/upload-artifact@v6 + with: + name: coverage-python${{ matrix.python-versions }}-fast + path: .coverage + include-hidden-files: true