# ==================================================================================================================== # # Authors: # # Patrick Lehmann # # Unai Martinez-Corral # # # # ==================================================================================================================== # # Copyright 2020-2026 The pyTooling Authors # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # name: Parameters on: workflow_call: inputs: ubuntu_image_version: description: 'Ubuntu image version.' required: false default: '24.04' type: string name: description: 'Name of the tool.' required: false default: '' type: string package_namespace: description: 'Name of the tool''s namespace.' required: false default: '' type: string package_name: description: 'Name of the tool''s package.' required: false default: '' type: string version_file: description: "Path to module containing the version ('__version__' variable)." required: false default: '__init__.py' type: string python_version: description: 'Python version.' required: false default: '3.14' type: string python_version_list: description: 'Space separated list of Python versions to run tests with.' required: false default: '3.10 3.11 3.12 3.13 3.14' type: string system_list: description: 'Space separated list of systems to run tests on.' required: false default: 'ubuntu ubuntu-arm windows windows-arm macos macos-arm mingw64 ucrt64' type: string include_list: description: 'Space separated list of system:python items to be included into the list of test.' required: false default: '' type: string exclude_list: description: 'Space separated list of system:python items to be excluded from the list of test.' required: false default: 'windows-arm:3.9 windows-arm:3.10' type: string disable_list: description: 'Space separated list of system:python items to be disabled from the list of test.' required: false default: 'windows-arm:pypy-3.10 windows-arm:pypy-3.11' type: string ubuntu_image: description: 'The used GitHub Action image for Ubuntu (x86-64) based jobs.' required: false default: 'ubuntu-24.04' type: string ubuntu_arm_image: description: 'The used GitHub Action image for Ubuntu (aarch64) based jobs.' required: false default: 'ubuntu-24.04-arm' type: string windows_image: description: 'The used GitHub Action image for Windows Server (x86-64) based jobs.' required: false default: 'windows-2025' type: string windows_arm_image: description: 'The used GitHub Action image for Windows (aarch64) based jobs.' required: false default: 'windows-11-arm' type: string macos_intel_image: description: 'The used GitHub Action image for macOS (Intel x86-64) based jobs.' required: false default: 'macos-15-intel' type: string macos_arm_image: description: 'The used GitHub Action image for macOS (ARM aarch64) based jobs.' required: false default: 'macos-15' type: string pipeline-delay: description: 'Slow down this job, to delay the startup of the GitHub Action pipline.' required: false default: 0 type: number outputs: python_version: description: "Default Python version for other jobs." value: ${{ jobs.Parameters.outputs.python_version }} package_fullname: description: "The package's full name." value: ${{ jobs.Parameters.outputs.package_fullname }} package_directory: description: "The package's directory." value: ${{ jobs.Parameters.outputs.package_directory }} package_version_file: description: "Path to the package's module containing the version ('__version__' variable)." value: ${{ jobs.Parameters.outputs.package_version_file }} artifact_basename: description: "Artifact base name." value: ${{ jobs.Parameters.outputs.artifact_basename }} artifact_names: description: "Pre-defined artifact names for other jobs." value: ${{ jobs.Parameters.outputs.artifact_names }} python_jobs: description: "List of Python versions (and system combinations) to be used in the matrix of other jobs." value: ${{ jobs.Parameters.outputs.python_jobs }} jobs: Parameters: name: ✎ Generate pipeline parameters runs-on: "ubuntu-${{ inputs.ubuntu_image_version }}" outputs: python_version: ${{ steps.variables.outputs.python_version }} package_fullname: ${{ steps.variables.outputs.package_fullname }} package_directory: ${{ steps.variables.outputs.package_directory }} package_version_file: ${{ steps.variables.outputs.package_version_file }} artifact_basename: ${{ steps.variables.outputs.artifact_basename }} artifact_names: ${{ steps.artifacts.outputs.artifact_names }} python_jobs: ${{ steps.jobs.outputs.python_jobs }} steps: - name: ⏬ Checkout repository uses: actions/checkout@v6 with: # The command 'git describe' (used for version) needs the history. fetch-depth: 0 - name: Generate a startup delay of ${{ inputs.pipeline-delay }} seconds id: delay if: inputs.pipeline-delay >= 0 run: | sleep ${{ inputs.pipeline-delay }} - name: Generate 'python_version' id: variables shell: python run: | from os import getenv from pathlib import Path from sys import exit from textwrap import dedent python_version = "${{ inputs.python_version }}".strip() package_namespace = "${{ inputs.package_namespace }}".strip() package_name = "${{ inputs.package_name }}".strip() version_file = "${{ inputs.version_file }}".strip() name = "${{ inputs.name }}".strip() if package_namespace == "": package_fullname = package_name package_directory = package_name elif package_namespace[-2:] == ".*": package_fullname = package_namespace[:-2] package_directory = package_namespace[:-2] else: package_fullname = f"{package_namespace}.{package_name}" package_directory = f"{package_namespace}/{package_name}" packageDirectory = Path(package_directory) packageVersionFile = packageDirectory / version_file print(f"Check if package version file '{packageVersionFile}' exists ... ", end="") if packageVersionFile.exists(): print("✅") package_version_file = packageVersionFile.as_posix() else: print("❌") package_version_file = "" print(f"::warning title=Parameters::Version file '{packageVersionFile}' not found.") artifact_basename = package_fullname if name == "" else name if artifact_basename == "" or artifact_basename == ".": print("::error title=Parameters::artifact_basename is empty.") exit(1) print("Variables:") print(f" python_version: {python_version}") print(f" package_fullname: {package_fullname}") print(f" package_directory: {package_directory}") print(f" package_version_file: {package_directory}") print(f" artifact_basename: {artifact_basename}") # Write jobs to special file github_output = Path(getenv("GITHUB_OUTPUT")) print(f"GITHUB_OUTPUT: {github_output}") with github_output.open("a+", encoding="utf-8") as f: f.write(dedent(f"""\ python_version={python_version} package_fullname={package_fullname} package_directory={package_directory} package_version_file={package_version_file} artifact_basename={artifact_basename} """)) - name: Generate 'artifact_names' id: artifacts shell: python run: | from json import dumps as json_dumps from os import getenv from pathlib import Path from textwrap import dedent package_namespace = "${{ inputs.package_namespace }}".strip() package_name = "${{ inputs.package_name }}".strip() artifact_basename = "${{ steps.variables.outputs.artifact_basename }}" artifact_names = { "unittesting_xml": f"{artifact_basename}-UnitTestReportSummary-XML", "unittesting_html": f"{artifact_basename}-UnitTestReportSummary-HTML", "perftesting_xml": f"{artifact_basename}-PerformanceTestReportSummary-XML", "benchtesting_xml": f"{artifact_basename}-BenchmarkTestReportSummary-XML", "apptesting_xml": f"{artifact_basename}-ApplicationTestReportSummary-XML", "codecoverage_sqlite": f"{artifact_basename}-CodeCoverage-SQLite", "codecoverage_xml": f"{artifact_basename}-CodeCoverage-XML", "codecoverage_json": f"{artifact_basename}-CodeCoverage-JSON", "codecoverage_html": f"{artifact_basename}-CodeCoverage-HTML", "statictyping_cobertura": f"{artifact_basename}-StaticTyping-Cobertura-XML", "statictyping_junit": f"{artifact_basename}-StaticTyping-JUnit-XML", "statictyping_html": f"{artifact_basename}-StaticTyping-HTML", "package_all": f"{artifact_basename}-Packages", "documentation_html": f"{artifact_basename}-Documentation-HTML", "documentation_latex": f"{artifact_basename}-Documentation-LaTeX", "documentation_pdf": f"{artifact_basename}-Documentation-PDF", } print("Artifacts Names ({len(artifact_names)}):") for id, artifactName in artifact_names.items(): print(f" {id:>24}: {artifactName}") # Write jobs to special file github_output = Path(getenv("GITHUB_OUTPUT")) print(f"GITHUB_OUTPUT: {github_output}") with github_output.open("a+", encoding="utf-8") as f: f.write(dedent(f"""\ artifact_names={json_dumps(artifact_names)} """)) - name: Generate 'python_jobs' id: jobs shell: python run: | from json import dumps as json_dumps from os import getenv from pathlib import Path from textwrap import dedent from typing import Iterable python_version = "${{ steps.variables.outputs.python_version }}" name = "${{ steps.variables.outputs.artifact_basename }}" systems = "${{ inputs.system_list }}".strip() versions = "${{ inputs.python_version_list }}".strip() include_list = "${{ inputs.include_list }}".strip() exclude_list = "${{ inputs.exclude_list }}".strip() disable_list = "${{ inputs.disable_list }}".strip() currentMSYS2Version = "3.12" currentAlphaVersion = "3.15" currentAlphaRelease = "3.15.0-a.1" if systems == "": print("::error title=Parameters::system_list is empty.") else: systems = [sys.strip() for sys in systems.split(" ")] if versions == "": versions = [ python_version ] else: versions = [ver.strip() for ver in versions.split(" ")] if include_list == "": includes = [] else: includes = [tuple(include.strip().split(":")) for include in include_list.split(" ")] if exclude_list == "": excludes = [] else: excludes = [exclude.strip() for exclude in exclude_list.split(" ")] if disable_list == "": disabled = [] else: disabled = [disable.strip() for disable in disable_list.split(" ")] if "3.9" in versions: print("::warning title=Deprecated::Support for Python 3.9 ended in 2025.10.") if "msys2" in systems: print("::warning title=Deprecated::System 'msys2' will be replaced by 'mingw64'.") if currentAlphaVersion in versions: print(f"::notice title=Experimental::Python {currentAlphaVersion} ({currentAlphaRelease}) is a pre-release.") for disable in disabled: print(f"::warning title=Disabled Python Job::{name}: Job combination '{disable}' temporarily disabled.") # see https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json data = { # Python and PyPy versions supported by "setup-python" action "python": { "3.9": { "icon": "⚫", "until": "2025.10" }, "3.10": { "icon": "🔴", "until": "2026.10" }, "3.11": { "icon": "🟠", "until": "2027.10" }, "3.12": { "icon": "🟡", "until": "2028.10" }, "3.13": { "icon": "🟢", "until": "2029.10" }, "3.14": { "icon": "🟢", "until": "2030.10" }, "3.15": { "icon": "🟣", "until": "2031.10" }, "pypy-3.9": { "icon": "⟲🔴", "until": "????.??" }, "pypy-3.10": { "icon": "⟲🟠", "until": "????.??" }, "pypy-3.11": { "icon": "⟲🟡", "until": "????.??" }, }, # Runner systems (runner images) supported by GitHub Actions "sys": { "ubuntu": { "icon": "🐧", "runs-on": "${{ inputs.ubuntu_image }}", "shell": "bash", "name": "Linux (x86-64)" }, "ubuntu-arm": { "icon": "⛄", "runs-on": "${{ inputs.ubuntu_arm_image }}", "shell": "bash", "name": "Linux (aarch64)" }, "windows": { "icon": "🪟", "runs-on": "${{ inputs.windows_image }}", "shell": "pwsh", "name": "Windows (x86-64)" }, "windows-arm": { "icon": "🏢", "runs-on": "${{ inputs.windows_arm_image }}", "shell": "pwsh", "name": "Windows (aarch64)" }, "macos": { "icon": "🍎", "runs-on": "${{ inputs.macos_intel_image }}", "shell": "bash", "name": "macOS (x86-64)" }, "macos-arm": { "icon": "🍏", "runs-on": "${{ inputs.macos_arm_image }}", "shell": "bash", "name": "macOS (aarch64)" }, }, # Runtimes provided by MSYS2 "runtime": { "msys": { "icon": "🪟🟪", "name": "Windows+MSYS2 (x86-64) - MSYS" }, "mingw32": { "icon": "🪟⬛", "name": "Windows+MSYS2 (x86-64) - MinGW32" }, "mingw64": { "icon": "🪟🟦", "name": "Windows+MSYS2 (x86-64) - MinGW64" }, "clang32": { "icon": "🪟🟫", "name": "Windows+MSYS2 (x86-64) - Clang32" }, "clang64": { "icon": "🪟🟧", "name": "Windows+MSYS2 (x86-64) - Clang64" }, "ucrt64": { "icon": "🪟🟨", "name": "Windows+MSYS2 (x86-64) - UCRT64" }, } } print(f"includes ({len(includes)}):") for system,version in includes: print(f"- {system}:{version}") print(f"excludes ({len(excludes)}):") for exclude in excludes: print(f"- {exclude}") print(f"disabled ({len(disabled)}):") for disable in disabled: print(f"- {disable}") def match(combination: str, pattern: str) -> bool: system, version = combination.split(":") sys, ver = pattern.split(":") if sys == "*": return (ver == "*") or (version == ver) elif system == sys: return (ver == "*") or (version == ver) else: return False def notIn(combination: str, patterns: Iterable[str]) -> bool: for pattern in patterns: if match(combination, pattern): return False return True combinations = [ (system, version) for system in systems if system in data["sys"] for version in versions if version in data["python"] and notIn(f"{system}:{version}", excludes) and notIn(f"{system}:{version}", disabled) ] + [ (system, currentMSYS2Version) for system in systems if system in data["runtime"] and notIn(f"{system}:{currentMSYS2Version}", excludes) and notIn(f"{system}:{currentMSYS2Version}", disabled) ] + [ (system, version) for system, version in includes if system in data["sys"] and version in data["python"] and notIn(f"{system}:{version}", disabled) ] print(f"Combinations ({len(combinations)}):") for system, version in combinations: print(f"- {system}:{version}") jobs = [ { "sysicon": data["sys"][system]["icon"], "system": system, "runs-on": data["sys"][system]["runs-on"], "runtime": "native", "shell": data["sys"][system]["shell"], "pyicon": data["python"][version]["icon"], "python": currentAlphaRelease if version == currentAlphaVersion else version, "envname": data["sys"][system]["name"], } for system, version in combinations if system in data["sys"] ] + [ { "sysicon": data["runtime"][runtime]["icon"], "system": "msys2", "runs-on": "${{ inputs.windows_image }}", "runtime": runtime.upper(), "shell": "msys2 {0}", "pyicon": data["python"][currentMSYS2Version]["icon"], "python": version, "envname": data["runtime"][runtime]["name"], } for runtime, version in combinations if runtime not in data["sys"] ] print("Parameters:") print(f" python_version: {python_version}") print(f" python_jobs ({len(jobs)}):\n" + "".join([f" {{ " + ", ".join([f"\"{key}\": \"{value}\"" for key, value in job.items()]) + f" }},\n" for job in jobs]) ) # Write jobs to special file github_output = Path(getenv("GITHUB_OUTPUT")) print(f"GITHUB_OUTPUT: {github_output}") with github_output.open("a+", encoding="utf-8") as f: f.write(dedent(f"""\ python_version={python_version} python_jobs={json_dumps(jobs)} """)) - name: Verify out parameters id: verify run: | printf "python_version: %s\n" '${{ steps.variables.outputs.python_version }}' printf "package_fullname: %s\n" '${{ steps.variables.outputs.package_fullname }}' printf "package_directory: %s\n" '${{ steps.variables.outputs.package_directory }}' printf "package_version_file: %s\n" '${{ steps.variables.outputs.package_version_file }}' printf "artifact_basename: %s\n" '${{ steps.variables.outputs.artifact_basename }}' printf "================================================================================\n" printf "artifact_names: %s\n" '${{ steps.artifacts.outputs.artifact_names }}' printf "================================================================================\n" printf "python_jobs: %s\n" '${{ steps.jobs.outputs.python_jobs }}' printf "================================================================================\n"