From e76b998009efc119835743922aacf9ca4b116c52 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 16 Jan 2026 12:27:57 -0600 Subject: [PATCH 1/4] Publish version metadata to an external repository --- .github/workflows/publish-versions.yml | 82 +++++++++++++++++++ .github/workflows/release.yml | 8 ++ generate-version-metadata.py | 105 +++++++++++++++++++++++++ 3 files changed, 195 insertions(+) create mode 100644 .github/workflows/publish-versions.yml create mode 100755 generate-version-metadata.py diff --git a/.github/workflows/publish-versions.yml b/.github/workflows/publish-versions.yml new file mode 100644 index 000000000..368d17d1f --- /dev/null +++ b/.github/workflows/publish-versions.yml @@ -0,0 +1,82 @@ +# Publish python-build-standalone version information to the versions repository. +name: publish-versions + +on: + workflow_call: + inputs: + tag: + required: true + type: string + +permissions: {} + +jobs: + publish-versions: + runs-on: ubuntu-latest + env: + TAG: ${{ inputs.tag }} + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + + - name: "Install uv" + uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 + + - name: "Download release artifacts" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + mkdir -p dist + gh release download "$TAG" --dir dist + + - name: "Generate versions payload" + env: + GITHUB_EVENT_INPUTS_TAG: ${{ inputs.tag }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: uv run generate-version-metadata.py + + - name: "Set branch name" + run: echo "BRANCH_NAME=update-versions-$TAG-$(date +%s)" >> $GITHUB_ENV + + - name: "Clone versions repo" + run: git clone https://${{ secrets.ASTRAL_VERSIONS_PAT }}@github.com/astral-sh/versions.git astral-versions + + - name: "Update versions" + run: cat dist/python-build-standalone.json | uv run astral-versions/scripts/publish-version.py --format json --name python-build-standalone --output astral-versions/v1 + + - name: "Commit versions" + working-directory: astral-versions + run: | + git config user.name "astral-versions-bot" + git config user.email "176161322+astral-versions-bot@users.noreply.github.com" + + git checkout -b "$BRANCH_NAME" + git add -A + git commit -m "Update python-build-standalone to $TAG" + + - name: "Create Pull Request" + working-directory: astral-versions + env: + GITHUB_TOKEN: ${{ secrets.ASTRAL_VERSIONS_PAT }} + run: | + pull_request_title="Update python-build-standalone versions for $TAG" + + gh pr list --state open --json title --jq ".[] | select(.title == \"$pull_request_title\") | .number" | \ + xargs -I {} gh pr close {} + + git push origin "$BRANCH_NAME" + + gh pr create --base main --head "$BRANCH_NAME" \ + --title "$pull_request_title" \ + --body "Automated versions update for $TAG" \ + --label "automation" + + - name: "Merge Pull Request" + working-directory: astral-versions + env: + GITHUB_TOKEN: ${{ secrets.ASTRAL_VERSIONS_PAT }} + run: | + # Wait for PR to be created before merging + sleep 10 + gh pr merge --squash "$BRANCH_NAME" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3ec62442e..01a7c3d7d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -97,3 +97,11 @@ jobs: subject-path: | dist/*.tar.gz dist/*.tar.zst + + publish-versions: + needs: release + if: ${{ github.event.inputs.dry-run == 'false' }} + uses: ./.github/workflows/publish-versions.yml + with: + tag: ${{ github.event.inputs.tag }} + secrets: inherit diff --git a/generate-version-metadata.py b/generate-version-metadata.py new file mode 100755 index 000000000..a19011135 --- /dev/null +++ b/generate-version-metadata.py @@ -0,0 +1,105 @@ +# /// script +# requires-python = ">=3.11" +# /// +"""Generate versions payload for python-build-standalone releases.""" + +from __future__ import annotations + +import json +import os +import re +from collections import defaultdict +from datetime import datetime, timezone +from pathlib import Path +from urllib.parse import quote + +FILENAME_RE = re.compile( + r"""(?x) + ^ + cpython- + (?P\d+\.\d+\.\d+(?:(?:a|b|rc)\d+)?)(?:\+\d+)?\+ + (?P\d+)- + (?P[a-z\d_]+-[a-z\d]+(?:-[a-z\d]+)?-[a-z\d_]+)- + (?:(?P.+)-)? + (?P[a-z_]+)? + \.tar\.(?:gz|zst) + $ + """ +) + + +def main() -> None: + tag = os.environ["GITHUB_EVENT_INPUTS_TAG"] + repo = os.environ["GITHUB_REPOSITORY"] + dist = Path("dist") + checksums = dist / "SHA256SUMS" + + if not checksums.exists(): + raise SystemExit("SHA256SUMS not found in dist/") + + checksum_map: dict[str, str] = {} + for line in checksums.read_text().splitlines(): + line = line.strip() + if not line: + continue + checksum, filename = line.split(maxsplit=1) + checksum_map[filename.lstrip("*")] = checksum + + versions: dict[str, list[dict[str, str]]] = defaultdict(list) + for path in sorted(dist.glob("cpython-*.tar.*")): + match = FILENAME_RE.match(path.name) + if match is None: + continue + python_version = match.group("py") + build_version = match.group("tag") + version = f"{python_version}+{build_version}" + build = match.group("build") + flavor = match.group("flavor") + variant_parts: list[str] = [] + if build: + variant_parts.extend(build.split("+")) + if flavor: + variant_parts.append(flavor) + variant = "+".join(variant_parts) if variant_parts else "" + + url_prefix = f"https://github.com/{repo}/releases/download/{tag}/" + url = url_prefix + quote(path.name, safe="") + archive_format = "tar.zst" if path.name.endswith(".tar.zst") else "tar.gz" + + artifact = { + "platform": match.group("triple"), + "variant": variant, + "url": url, + "archive_format": archive_format, + "sha256": checksum_map.get(path.name, ""), + } + if not artifact["sha256"]: + artifact.pop("sha256") + versions[version].append(artifact) + + payload_versions: list[dict[str, object]] = [] + now = datetime.now(timezone.utc).isoformat() + for version, artifacts in sorted(versions.items(), reverse=True): + artifacts.sort( + key=lambda artifact: (artifact["platform"], artifact.get("variant", "")) + ) + payload_versions.append( + { + "version": version, + "date": now, + "artifacts": artifacts, + } + ) + + payload = { + "name": "python-build-standalone", + "versions": payload_versions, + } + + output = dist / "python-build-standalone.json" + output.write_text(json.dumps(payload, separators=(",", ":"))) + print(f"Wrote {output}") + + +if __name__ == "__main__": + main() From 4b43c4f4fde1e31f1316e44440909d0718885fb2 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 28 Jan 2026 10:06:55 -0600 Subject: [PATCH 2/4] Review --- .github/workflows/publish-versions.yml | 4 ++-- generate-version-metadata.py | 19 ++++++++++--------- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/publish-versions.yml b/.github/workflows/publish-versions.yml index 368d17d1f..9ec8562f3 100644 --- a/.github/workflows/publish-versions.yml +++ b/.github/workflows/publish-versions.yml @@ -23,12 +23,12 @@ jobs: - name: "Install uv" uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 - - name: "Download release artifacts" + - name: "Download SHA256SUMS" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | mkdir -p dist - gh release download "$TAG" --dir dist + gh release download "$TAG" --dir dist --pattern "SHA256SUMS" - name: "Generate versions payload" env: diff --git a/generate-version-metadata.py b/generate-version-metadata.py index a19011135..179ba4386 100755 --- a/generate-version-metadata.py +++ b/generate-version-metadata.py @@ -37,17 +37,20 @@ def main() -> None: if not checksums.exists(): raise SystemExit("SHA256SUMS not found in dist/") - checksum_map: dict[str, str] = {} + # Parse filenames and checksums directly from SHA256SUMS to avoid downloading + # all release artifacts (tens of GB). + entries: list[tuple[str, str]] = [] for line in checksums.read_text().splitlines(): line = line.strip() if not line: continue checksum, filename = line.split(maxsplit=1) - checksum_map[filename.lstrip("*")] = checksum + filename = filename.lstrip("*") + entries.append((filename, checksum)) versions: dict[str, list[dict[str, str]]] = defaultdict(list) - for path in sorted(dist.glob("cpython-*.tar.*")): - match = FILENAME_RE.match(path.name) + for filename, checksum in sorted(entries): + match = FILENAME_RE.match(filename) if match is None: continue python_version = match.group("py") @@ -63,18 +66,16 @@ def main() -> None: variant = "+".join(variant_parts) if variant_parts else "" url_prefix = f"https://github.com/{repo}/releases/download/{tag}/" - url = url_prefix + quote(path.name, safe="") - archive_format = "tar.zst" if path.name.endswith(".tar.zst") else "tar.gz" + url = url_prefix + quote(filename, safe="") + archive_format = "tar.zst" if filename.endswith(".tar.zst") else "tar.gz" artifact = { "platform": match.group("triple"), "variant": variant, "url": url, "archive_format": archive_format, - "sha256": checksum_map.get(path.name, ""), + "sha256": checksum, } - if not artifact["sha256"]: - artifact.pop("sha256") versions[version].append(artifact) payload_versions: list[dict[str, object]] = [] From 827514bb48cc852d939009b99eca5958e44297e6 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 30 Jan 2026 06:59:11 -0600 Subject: [PATCH 3/4] Update to new version --- .github/workflows/publish-versions.yml | 11 ++++------- generate-version-metadata.py | 10 ++-------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/.github/workflows/publish-versions.yml b/.github/workflows/publish-versions.yml index 9ec8562f3..d8cf5f219 100644 --- a/.github/workflows/publish-versions.yml +++ b/.github/workflows/publish-versions.yml @@ -30,12 +30,6 @@ jobs: mkdir -p dist gh release download "$TAG" --dir dist --pattern "SHA256SUMS" - - name: "Generate versions payload" - env: - GITHUB_EVENT_INPUTS_TAG: ${{ inputs.tag }} - GITHUB_REPOSITORY: ${{ github.repository }} - run: uv run generate-version-metadata.py - - name: "Set branch name" run: echo "BRANCH_NAME=update-versions-$TAG-$(date +%s)" >> $GITHUB_ENV @@ -43,7 +37,10 @@ jobs: run: git clone https://${{ secrets.ASTRAL_VERSIONS_PAT }}@github.com/astral-sh/versions.git astral-versions - name: "Update versions" - run: cat dist/python-build-standalone.json | uv run astral-versions/scripts/publish-version.py --format json --name python-build-standalone --output astral-versions/v1 + env: + GITHUB_EVENT_INPUTS_TAG: ${{ inputs.tag }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: uv run generate-version-metadata.py | uv run astral-versions/scripts/insert-versions.py --name python-build-standalone - name: "Commit versions" working-directory: astral-versions diff --git a/generate-version-metadata.py b/generate-version-metadata.py index 179ba4386..48582917e 100755 --- a/generate-version-metadata.py +++ b/generate-version-metadata.py @@ -92,14 +92,8 @@ def main() -> None: } ) - payload = { - "name": "python-build-standalone", - "versions": payload_versions, - } - - output = dist / "python-build-standalone.json" - output.write_text(json.dumps(payload, separators=(",", ":"))) - print(f"Wrote {output}") + for version in payload_versions: + print(json.dumps(version, separators=(",", ":"))) if __name__ == "__main__": From b5eda2d66d3503e187551d3f7054c790d9e75199 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 30 Jan 2026 08:36:17 -0600 Subject: [PATCH 4/4] Add workflow dispatch and dry-run --- .github/workflows/publish-versions.yml | 33 ++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-versions.yml b/.github/workflows/publish-versions.yml index d8cf5f219..be6d29449 100644 --- a/.github/workflows/publish-versions.yml +++ b/.github/workflows/publish-versions.yml @@ -7,6 +7,17 @@ on: tag: required: true type: string + workflow_dispatch: + inputs: + tag: + description: "Release tag to publish (e.g. 20260127)" + required: true + type: string + dry-run: + description: "Only generate metadata, skip PR creation" + required: false + type: boolean + default: true permissions: {} @@ -30,19 +41,31 @@ jobs: mkdir -p dist gh release download "$TAG" --dir dist --pattern "SHA256SUMS" + - name: "Generate versions metadata" + env: + GITHUB_EVENT_INPUTS_TAG: ${{ inputs.tag }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: uv run generate-version-metadata.py > dist/versions.ndjson + + - name: "Validate metadata" + run: | + echo "Generated $(wc -l < dist/versions.ndjson) version entries" + head -c 1000 dist/versions.ndjson + - name: "Set branch name" + if: inputs.dry-run != true run: echo "BRANCH_NAME=update-versions-$TAG-$(date +%s)" >> $GITHUB_ENV - name: "Clone versions repo" + if: inputs.dry-run != true run: git clone https://${{ secrets.ASTRAL_VERSIONS_PAT }}@github.com/astral-sh/versions.git astral-versions - name: "Update versions" - env: - GITHUB_EVENT_INPUTS_TAG: ${{ inputs.tag }} - GITHUB_REPOSITORY: ${{ github.repository }} - run: uv run generate-version-metadata.py | uv run astral-versions/scripts/insert-versions.py --name python-build-standalone + if: inputs.dry-run != true + run: cat dist/versions.ndjson | uv run astral-versions/scripts/insert-versions.py --name python-build-standalone - name: "Commit versions" + if: inputs.dry-run != true working-directory: astral-versions run: | git config user.name "astral-versions-bot" @@ -53,6 +76,7 @@ jobs: git commit -m "Update python-build-standalone to $TAG" - name: "Create Pull Request" + if: inputs.dry-run != true working-directory: astral-versions env: GITHUB_TOKEN: ${{ secrets.ASTRAL_VERSIONS_PAT }} @@ -70,6 +94,7 @@ jobs: --label "automation" - name: "Merge Pull Request" + if: inputs.dry-run != true working-directory: astral-versions env: GITHUB_TOKEN: ${{ secrets.ASTRAL_VERSIONS_PAT }}