From 9816fe2d16327f3ca1f20c9998d5e233993ba2c9 Mon Sep 17 00:00:00 2001 From: Sergey Date: Mon, 31 Aug 2026 19:43:44 +0200 Subject: [PATCH] fix(nightly): stamp the version before building instead of renaming the wheel. (#762) * fix(nightly): stamp the version before building instead of renaming the wheel * test(nightly): require the known build jobs rather than an exact count --- .github/workflows/nightly.yml | 54 ++++++++-------- tests/test_nightly_versioning.py | 108 +++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 27 deletions(-) create mode 100644 tests/test_nightly_versioning.py diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 5dc262d..eacaca9 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -33,45 +33,35 @@ jobs: run: | python -m pip install --upgrade pip setuptools wheel build twine - - name: Build package - run: | - python -m build - ls -la dist/ - - - name: Generate nightly version + # Stamped before the build, not patched into the filename after it. + # pyproject.toml takes the version from `vocalinux.version.__version__` + # (tool.setuptools.dynamic), so renaming the wheel left its own metadata + # saying 0.16.1 while the filename claimed 0.16.1.dev+. pip + # tolerated that; uv rejects it as a malformed wheel, and the AppImage + # build has installed with uv since #743 — which is what broke nightly. + # __version_info__ is deliberately left alone: a dev/local suffix has no + # meaningful place in a numeric tuple, and nothing builds from it. + - name: Stamp the nightly version id: version run: | + set -euo pipefail BASE_VERSION=$(grep -oP '__version__\s*=\s*"\K[^"]+' src/vocalinux/version.py) DATE=$(date +%Y%m%d) SHORT_SHA=$(git rev-parse --short HEAD) NIGHTLY_VERSION="${BASE_VERSION}.dev${DATE}+${SHORT_SHA}" + + sed -i "s/^__version__ = \".*\"$/__version__ = \"${NIGHTLY_VERSION}\"/" src/vocalinux/version.py + grep -q "^__version__ = \"${NIGHTLY_VERSION}\"$" src/vocalinux/version.py + echo "version=${NIGHTLY_VERSION}" >> $GITHUB_OUTPUT echo "base_version=${BASE_VERSION}" >> $GITHUB_OUTPUT echo "date=${DATE}" >> $GITHUB_OUTPUT echo "sha=${SHORT_SHA}" >> $GITHUB_OUTPUT - echo "Built version: ${NIGHTLY_VERSION}" + echo "Building version: ${NIGHTLY_VERSION}" - - name: Rename artifacts for nightly + - name: Build package run: | - for file in dist/*; do - base=$(basename "$file") - if [[ "$base" == vocalinux-* ]]; then - # Extract the file extension - if [[ "$base" == *.whl ]]; then - # vocalinux-0.5.0b0-py3-none-any.whl -> vocalinux-0.5.0b0.dev20250211+g1234567-py3-none-any.whl - newname="vocalinux-${{ steps.version.outputs.version }}-py3-none-any.whl" - elif [[ "$base" == *.tar.gz ]]; then - # vocalinux-0.5.0b0.tar.gz -> vocalinux-0.5.0b0.dev20250211+g1234567.tar.gz - newname="vocalinux-${{ steps.version.outputs.version }}.tar.gz" - else - # Fallback: replace version in filename - newname=$(echo "$base" | sed 's/vocalinux-[^[:space:]]*/vocalinux-${{ steps.version.outputs.version }}/') - fi - if [[ "$base" != "$newname" ]]; then - mv "$file" "dist/$newname" - fi - fi - done + python -m build ls -la dist/ # glslc is built from the pinned shaderc commit inside the container - @@ -270,6 +260,16 @@ jobs: run: | python -m pip install --upgrade pip setuptools wheel build + # This job builds its own wheel, so it needs the same stamp: without it + # the aarch64 AppImage bundles a wheel calling itself 0.16.1 while the + # release it is attached to is 0.16.1.dev+. + - name: Stamp the nightly version + run: | + set -euo pipefail + VERSION="${{ needs.nightly.outputs.version }}" + sed -i "s/^__version__ = \".*\"$/__version__ = \"${VERSION}\"/" src/vocalinux/version.py + grep -q "^__version__ = \"${VERSION}\"$" src/vocalinux/version.py + - name: Build package run: | python -m build diff --git a/tests/test_nightly_versioning.py b/tests/test_nightly_versioning.py new file mode 100644 index 0000000..ac3458d --- /dev/null +++ b/tests/test_nightly_versioning.py @@ -0,0 +1,108 @@ +"""A nightly artifact's filename and its own metadata have to agree. + +`nightly.yml` used to build the wheel and then rename the file to carry the +nightly version, leaving `.dist-info` still saying 0.16.1 while the filename +claimed 0.16.1.dev+. pip installed that without complaint for as long +as the AppImage build used pip. #743 moved that install to uv, which rejects the +mismatch outright: + + Wheel version does not match filename (0.16.1 != 0.16.1.dev20260831+71929b7), + which indicates a malformed wheel. + +Nightly went red the first night it ran the new build.sh and stayed red. The fix +is to stamp `vocalinux.version.__version__` — where `tool.setuptools.dynamic` +reads the version from — before building, so the two cannot disagree. + +Parsed as text rather than YAML: the only YAML parser in the venv arrives +transitively through pre-commit. +""" + +import re +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +NIGHTLY = REPO_ROOT / ".github" / "workflows" / "nightly.yml" +PYPROJECT = REPO_ROOT / "pyproject.toml" + +STAMP = '__version__ = \\"${' + +#: The jobs that build a distribution, and so must stamp before they do. +BUILDING_JOBS = {"nightly", "nightly-appimage-arm64"} + + +def _text() -> str: + return NIGHTLY.read_text(encoding="utf-8") + + +def _jobs() -> dict: + """job name -> its block of the workflow, split on the 2-space indent.""" + text = _text() + start = text.index("\njobs:\n") + body = text[start:] + headers = list(re.finditer(r"^ ([a-z0-9][a-z0-9-]*):$", body, re.M)) + assert headers, "no jobs found; nightly.yml's layout moved" + + jobs = {} + for index, header in enumerate(headers): + end = headers[index + 1].start() if index + 1 < len(headers) else len(body) + jobs[header.group(1)] = body[header.start() : end] + return jobs + + +def _building_jobs() -> dict: + return {name: block for name, block in _jobs().items() if "python -m build" in block} + + +def test_the_version_still_comes_from_the_file_the_workflow_stamps(): + """If setuptools stopped reading version.py, stamping it would silently do + nothing and the mismatch would come back.""" + pyproject = PYPROJECT.read_text(encoding="utf-8") + assert 'version = {attr = "vocalinux.version.__version__"}' in pyproject + + +def test_nightly_never_renames_a_built_artifact(): + """Renaming a wheel does not change what is recorded inside it.""" + text = _text() + assert "Rename artifacts" not in text, "the rename step is back" + offenders = [ + line.strip() + for line in text.splitlines() + if re.search(r"\bmv\b.*dist/", line) or re.search(r"newname=", line) + ] + assert not offenders, "nightly renames build output:\n" + "\n".join(offenders) + + +def test_every_job_that_builds_stamps_the_version_first(): + jobs = _building_jobs() + # A floor, not a fixed count. The guard is worthless if the parser quietly + # stops finding a job, but a third build job should not fail CI merely for + # existing: the loop below covers whatever is actually there. + missing = BUILDING_JOBS - set(jobs) + assert not missing, f"{sorted(missing)} no longer builds, or the parser stopped seeing it" + for name, block in jobs.items(): + assert STAMP in block, f"{name} builds without stamping the version" + assert block.index(STAMP) < block.index( + "python -m build" + ), f"{name} stamps the version after building, which changes nothing" + + +def test_the_stamp_is_verified_rather_than_assumed(): + """A `sed` that matches nothing exits 0 and hands the build a stale version.""" + for name, block in _building_jobs().items(): + assert re.search( + r'grep -q "\^__version__ = ', block + ), f"{name} does not check that the stamp landed" + + +def test_both_jobs_stamp_the_same_version(): + """The aarch64 job builds its own wheel; if it stamps something else, the two + AppImages on one nightly release carry different versions. + + Anchored on the assignment that feeds `sed`, not on the value appearing + anywhere in the job: it also appears in the AppImage step, which made an + earlier version of this test pass while aarch64 stamped a date. + """ + arm = _jobs()["nightly-appimage-arm64"] + assert ( + 'VERSION="${{ needs.nightly.outputs.version }}"' in arm + ), "aarch64 stamps something other than the version the nightly job published"