Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 104 additions & 0 deletions .github/workflows/publish-versions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Publish python-build-standalone version information to the versions repository.
name: publish-versions

on:
workflow_call:
inputs:
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: {}

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 SHA256SUMS"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
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"
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"
git config user.email "176161322+astral-versions-bot@users.noreply.github.com"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the numerical prefix here has significance? Would a prefix that helped id the commit as coming from the pbs workflow help debug in the future?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently that's the GitHub user id of the bot account which makes it robust to account name changes 🤷‍♀️ {user_id}+{username}@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"
if: inputs.dry-run != true
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"
if: inputs.dry-run != true
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"
8 changes: 8 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,11 @@
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

Check warning

Code scanning / zizmor

secrets unconditionally inherited by called workflow Warning

secrets unconditionally inherited by called workflow
with:
tag: ${{ github.event.inputs.tag }}
secrets: inherit
100 changes: 100 additions & 0 deletions generate-version-metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# /// 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<py>\d+\.\d+\.\d+(?:(?:a|b|rc)\d+)?)(?:\+\d+)?\+
(?P<tag>\d+)-
(?P<triple>[a-z\d_]+-[a-z\d]+(?:-[a-z\d]+)?-[a-z\d_]+)-
(?:(?P<build>.+)-)?
(?P<flavor>[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/")

# 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)
filename = filename.lstrip("*")
entries.append((filename, checksum))

versions: dict[str, list[dict[str, str]]] = defaultdict(list)
for filename, checksum in sorted(entries):
match = FILENAME_RE.match(filename)
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(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,
}
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,
}
)

for version in payload_versions:
print(json.dumps(version, separators=(",", ":")))


if __name__ == "__main__":
main()