Skip to content

Release v4.8.5

Release v4.8.5 #5047

Workflow file for this run

---
name: Build & release
# Read https://github.com/actions/runner/issues/491 for insights on complex workflow execution logic.
"on":
workflow_call:
secrets:
PYPI_TOKEN:
required: false
inputs:
binaries-test-plan:
description: Test plan for binaries
required: false
type: string
timeout:
description: Timeout in seconds for each binary test
required: false
type: number
outputs:
nuitka_matrix:
description: Nuitka build matrix
value: ${{ jobs.project-metadata.outputs.nuitka_matrix }}
# Target are chosen so that all commits get a chance to have their build tested.
push:
branches:
- main
pull_request:
jobs:
project-metadata:
name: Project metadata
runs-on: ubuntu-24.04
outputs:
# There's a design issue with GitHub actions: matrix outputs are not cumulative. The last job wins
# (see: https://git.luolix.topmunity/t/bug-jobs-output-should-return-a-list-for-a-matrix-job/128626).
# This means in a graph of jobs, a matrix-based one is terminal, and cannot be depended on. Same goes for
# (reusable) workflows. We use this preliminary job to produce all matrix we need to trigger depending jobs
# over the dimensions.
new_commits_matrix: ${{ steps.project-metadata.outputs.new_commits_matrix }}
release_commits_matrix: ${{ steps.project-metadata.outputs.release_commits_matrix }}
# Export Python project metadata.
nuitka_matrix: ${{ steps.project-metadata.outputs.nuitka_matrix }}
is_python_project: ${{ steps.project-metadata.outputs.is_python_project }}
package_name: ${{ steps.project-metadata.outputs.package_name }}
release_notes: ${{ steps.project-metadata.outputs.release_notes }}
steps:
- uses: actions/checkout@v4.2.2
with:
# Checkout pull request HEAD commit to ignore actions/checkout's merge commit. Fallback to push SHA.
ref: ${{ github.event.pull_request.head.sha || github.sha }}
# We're going to browse all new commits.
fetch-depth: 0
- name: List all branches
run: |
git branch --all
- name: List all commits
run: |
git log --decorate=full --oneline
- uses: actions/setup-python@v5.3.0
with:
python-version: "3.12"
cache: "pip"
cache-dependency-path: |
**/pyproject.toml
*requirements.txt
requirements/*.txt
- name: Install uv
run: |
python -m pip install -r https://raw.githubusercontent.com/kdeldycke/workflows/main/requirements/uv.txt
- name: Install gha-utils
run: >
uv tool install --with-requirements
https://raw.githubusercontent.com/kdeldycke/workflows/main/requirements/gha-utils.txt gha-utils
- name: Project metadata
id: project-metadata
env:
GITHUB_CONTEXT: ${{ toJSON(github) }}
run: |
gha-utils --verbosity DEBUG metadata --overwrite "$GITHUB_OUTPUT"
package-build:
name: "Build & check package"
needs:
- project-metadata
if: fromJSON(needs.project-metadata.outputs.is_python_project)
strategy:
matrix: ${{ fromJSON(needs.project-metadata.outputs.new_commits_matrix) }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4.2.2
with:
ref: ${{ matrix.commit }}
- uses: actions/setup-python@v5.3.0
with:
python-version: "3.12"
cache: "pip"
cache-dependency-path: |
**/pyproject.toml
*requirements.txt
requirements/*.txt
- name: Install uv
run: |
python -m pip install -r https://raw.githubusercontent.com/kdeldycke/workflows/main/requirements/uv.txt
- name: Install build dependencies
run: |
uv --no-progress venv
uv --no-progress pip install \
--requirement https://raw.githubusercontent.com/kdeldycke/workflows/main/requirements/build.txt
- name: Build package
run: |
uv --no-progress build
- name: Upload artifacts
uses: actions/upload-artifact@v4.4.3
with:
name: ${{ github.event.repository.name }}-build-${{ matrix.short_sha }}
path: ./dist/*
- name: Validates package metadata
# XXX These checks might be replaced by uv one day:
# https://github.com/astral-sh/uv/issues/8641
# https://github.com/astral-sh/uv/issues/8774
run: |
uv --no-progress run --frozen -- twine check ./dist/*
uv --no-progress run --frozen -- check-wheel-contents ./dist/*.whl
compile-binaries:
name: "Nuitka: generate binaries"
needs:
- project-metadata
if: needs.project-metadata.outputs.nuitka_matrix
strategy:
matrix: ${{ fromJSON(needs.project-metadata.outputs.nuitka_matrix) }}
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4.2.2
with:
ref: ${{ matrix.commit }}
- uses: actions/setup-python@v5.3.0
with:
python-version: "3.13"
cache: "pip"
cache-dependency-path: |
**/pyproject.toml
*requirements.txt
requirements/*.txt
- name: Install uv
run: |
python -m pip install -r https://raw.githubusercontent.com/kdeldycke/workflows/main/requirements/uv.txt
- name: Install Nuitka
# XXX We cannot break the long "pip install" line below with a class "\" because it will not be able to run on
# Windows' shell:
# ParserError: D:\a\_temp\330d7ec7-c0bf-4856-b2d8-407b69be9ee2.ps1:4
# Line |
# 4 | --requirement https://raw.githubusercontent.com/kdeldycke/workflows/m …
# | ~
# | Missing expression after unary operator '--'.
# yamllint disable rule:line-length
run: |
uv --no-progress venv
uv --no-progress pip install --requirement https://raw.githubusercontent.com/kdeldycke/workflows/main/requirements/nuitka.txt
# yamllint enable
- name: Nuitka + compilers versions
# XXX Nuitka needs the ".cmd" extension on Windows:
# https://github.com/Nuitka/Nuitka/issues/3173
# https://github.com/astral-sh/uv/issues/8770
# https://github.com/astral-sh/uv/pull/9099
run: >
uv --no-progress run --frozen -- nuitka${{ runner.os == 'Windows' && '.cmd' || '' }}
--version
- name: Build binary
run: >
uv --no-progress run --frozen -- nuitka${{ runner.os == 'Windows' && '.cmd' || '' }}
--onefile --assume-yes-for-downloads --output-filename=${{ matrix.bin_name }}
${{ matrix.module_path }}
- name: Upload binaries
uses: actions/upload-artifact@v4.4.3
with:
name: ${{ matrix.bin_name }}
if-no-files-found: error
path: ${{ matrix.bin_name }}
test-binaries:
name: Test binaries
needs:
- project-metadata
- compile-binaries
if: needs.project-metadata.outputs.nuitka_matrix
strategy:
matrix: ${{ fromJSON(needs.project-metadata.outputs.nuitka_matrix) }}
runs-on: ${{ matrix.os }}
steps:
- name: Download artifact
uses: actions/download-artifact@v4.1.8
id: artifacts
with:
name: ${{ matrix.bin_name }}
- name: Set binary permissions
if: runner.os != 'Windows'
run: |
chmod +x ${{ steps.artifacts.outputs.download-path }}/${{ matrix.bin_name }}
- name: Run tests
shell: python
run: |
import shlex
import sys
from subprocess import run, SubprocessError
from textwrap import dedent
from pathlib import Path
# Load test plan from workflow input, or use a default one.
test_plan = r"""${{ inputs.binaries-test-plan }}""".strip()
if not test_plan:
test_plan = dedent("""\
# Example of a test plan for a binary.
# Each line is a test, composed of CLI parameters.
# Empty lines and lines starting with "#" are ignored.
# Output the version of the CLI.
--version
# Test combination of version and verbosity.
--verbosity DEBUG --version
# Test help output.
--help
""")
# Load timeout from workflow input, or use a default one.
timeout = r"${{ inputs.timeout }}".strip()
if timeout:
assert timeout.isdigit()
timeout = int(timeout)
if not timeout:
timeout = 60
else:
assert timeout > 0
# Resolve binary path.
bin_name = r"${{ matrix.bin_name }}"
bin_path = (Path(r"${{ steps.artifacts.outputs.download-path }}") / bin_name).resolve(strict=True)
assert bin_path.is_file()
# Run tests.
for index, params in enumerate(test_plan.splitlines()):
params = params.strip()
if not params or params.startswith("#"):
continue
print(f"\n======== Test from line #{index + 1} ========\n$ {bin_name} {params}")
cli = [str(bin_path)]
if sys.platform == "win32":
cli.extend(params.split())
else:
cli.extend(shlex.split(params))
try:
result = run(
cli,
capture_output=True,
shell=True,
timeout=timeout,
check=True,
# XXX Do not force encoding to let CLIs figure out by themselves the contextual encoding to use.
# This avoid UnicodeDecodeError on output in Window's console which still defaults to legacy
# encoding (e.g. cp1252, cp932, etc...):
#
# Traceback (most recent call last):
# File "C:\...\__main__.py", line 49, in <module>
# File "C:\...\__main__.py", line 45, in main
# File "C:\...\click\core.py", line 1157, in __call__
# File "C:\...\click_extra\commands.py", line 347, in main
# File "C:\...\click\core.py", line 1078, in main
# File "C:\...\click_extra\commands.py", line 377, in invoke
# File "C:\...\click\core.py", line 1688, in invoke
# File "C:\...\click_extra\commands.py", line 377, in invoke
# File "C:\...\click\core.py", line 1434, in invoke
# File "C:\...\click\core.py", line 783, in invoke
# File "C:\...\cloup\_context.py", line 47, in new_func
# File "C:\...\meta_package_manager\cli.py", line 570, in managers
# File "C:\...\meta_package_manager\output.py", line 187, in print_table
# File "C:\...\click_extra\tabulate.py", line 97, in render_csv
# File "encodings\cp1252.py", line 19, in encode
# UnicodeEncodeError: 'charmap' codec can't encode character '\u2713' in position 128:
# character maps to <undefined>
#
# encoding="utf-8",
text=True,
)
except SubprocessError as ex:
print(f"\n=== stdout ===\n{ex.stdout}")
print(f"\n=== stderr ===\n{ex.stderr}")
raise ex
print(result.stdout)
git-tag:
name: Tag release
needs:
- project-metadata
# Only consider pushes to main branch as triggers for releases.
if: github.ref == 'refs/heads/main' && needs.project-metadata.outputs.release_commits_matrix
strategy:
matrix: ${{ fromJSON(needs.project-metadata.outputs.release_commits_matrix) }}
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4.2.2
with:
ref: ${{ matrix.commit }}
# XXX We need custom PAT with workflows permissions because tag generation will work but it will not trigger
# any other workflows that use `on.push.tags` triggers. See:
# https://stackoverflow.com/questions/60963759/use-github-actions-to-create-a-tag-but-not-a-release#comment135891921_64479344
# https://github.com/orgs/community/discussions/27028
token: ${{ secrets.WORKFLOW_UPDATE_GITHUB_PAT || secrets.GITHUB_TOKEN }}
- name: Check if tag exists
id: tag_exists
run: |
echo "tag_exists=$(git show-ref --tags "v${{ matrix.current_version }}" --quiet )" >> "$GITHUB_OUTPUT"
- name: Tag search results
run: |
echo "Does tag exist? ${{ steps.tag_exists.outputs.tag_exists && true || false }}"
- name: Push tag
# Skip the tag creation if it already exists instead of failing flat. This allows us to re-run the workflow if
# it was interrupted the first time. Which is really useful if the tagging fails during a release: we can
# simply push the new tag by hand and re-launch the workflow run.
if: ${{ ! steps.tag_exists.outputs.tag_exists }}
run: |
git tag "v${{ matrix.current_version }}" "${{ matrix.commit }}"
git push origin "v${{ matrix.current_version }}"
pypi-publish:
name: Publish to PyPi
needs:
- project-metadata
- package-build
- git-tag
if: needs.project-metadata.outputs.package_name
strategy:
matrix: ${{ fromJSON(needs.project-metadata.outputs.release_commits_matrix) }}
runs-on: ubuntu-24.04
steps:
- name: Install uv
run: |
python -m pip install -r https://raw.githubusercontent.com/kdeldycke/workflows/main/requirements/uv.txt
- name: Download build artifacts
uses: actions/download-artifact@v4.1.8
id: download
with:
name: ${{ github.event.repository.name }}-build-${{ matrix.short_sha }}
- name: Push to PyPi
run: |
uv --no-progress publish --token "${{ secrets.PYPI_TOKEN }}" "${{ steps.download.outputs.download-path }}/*"
github-release:
name: Publish GitHub release
needs:
- project-metadata
- compile-binaries
- git-tag
- pypi-publish
# Make sure this job always starts if git-tag ran and succeeded.
if: always() && needs.git-tag.result == 'success'
strategy:
matrix: ${{ fromJSON(needs.project-metadata.outputs.release_commits_matrix) }}
runs-on: ubuntu-24.04
steps:
- name: Download all artifacts
# Do not try to fetch build artifacts if any of the job producing them was skipped.
if: needs.pypi-publish.result != 'skipped' || needs.compile-binaries.result != 'skipped'
uses: actions/download-artifact@v4.1.8
id: artifacts
with:
path: release_artifact
# Only consider artifacts produced by the release commit.
pattern: "*-build-${{ matrix.short_sha }}*"
merge-multiple: true
- name: Rename binary artifacts, collect all others
# Do not try to rename artifacts if the job producing them was skipped.
if: needs.compile-binaries.result != 'skipped'
id: rename_artifacts
shell: python
run: |
import json
import os
from pathlib import Path
from random import randint
download_folder = Path("""${{ steps.artifacts.outputs.download-path }}""")
nuitka_matrix = json.loads("""${{ needs.project-metadata.outputs.nuitka_matrix }}""")
binaries = {entry["bin_name"] for entry in nuitka_matrix["include"] if "bin_name" in entry}
artifacts_path = []
for artifact in download_folder.glob("*"):
print(f"Processing {artifact} ...")
assert artifact.is_file()
# Rename binary artifacts to remove the build ID.
if artifact.name in binaries:
new_name = f'{artifact.stem.split("""-build-${{ matrix.short_sha }}""", 1)[0]}{artifact.suffix}'
new_path = artifact.with_name(new_name)
print(f"Renaming {artifact} to {new_path} ...")
assert not new_path.exists()
artifact.rename(new_path)
artifacts_path.append(new_path)
# Collect other artifacts as-is.
else:
print(f"Collecting {artifact} ...")
artifacts_path.append(artifact)
# Produce a unique delimiter to feed multiline content to GITHUB_OUTPUT:
# https://github.com/orgs/community/discussions/26288#discussioncomment-3876281
delimiter = f"ghadelimiter_{randint(10**8, (10**9) - 1)}"
output = f"artifacts_path<<{delimiter}\n"
output += "\n".join(str(p) for p in artifacts_path)
output += f"\n{delimiter}"
env_file = Path(os.getenv("GITHUB_OUTPUT"))
env_file.write_text(output)
- name: Create GitHub release
uses: softprops/action-gh-release@v2.1.0
# XXX We need custom PAT with workflows permissions because tag generation will work but it will not trigger
# any other workflows that use `on.push.tags` triggers. See:
# https://stackoverflow.com/questions/60963759/use-github-actions-to-create-a-tag-but-not-a-release#comment135891921_64479344
# https://github.com/orgs/community/discussions/27028
env:
GITHUB_TOKEN: ${{ secrets.WORKFLOW_UPDATE_GITHUB_PAT || secrets.GITHUB_TOKEN }}
with:
tag_name: v${{ matrix.current_version }}
target_commitish: ${{ matrix.commit }}
files: ${{ steps.rename_artifacts.outputs.artifacts_path }}
body: ${{ needs.project-metadata.outputs.release_notes }}