diff --git a/.github/workflows/release-builtins.yml b/.github/workflows/release-builtins.yml index 408f9c7..538ef90 100644 --- a/.github/workflows/release-builtins.yml +++ b/.github/workflows/release-builtins.yml @@ -20,30 +20,56 @@ jobs: build: name: Build distribution runs-on: ubuntu-latest - + outputs: + release: ${{ steps.check.outputs.release }} steps: - uses: actions/checkout@v4 with: submodules: true fetch-depth: 0 persist-credentials: false - - name: update core - env: - TAG: ${{ inputs.tag || 'origin/master' }} - # needs to detach because we can update to a tag - run: git -C uap-core switch --detach "$TAG" - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.x" + - name: Check necessity of release + id: check + env: + PYPI: ${{ github.event.inputs.environment }} + REF: ${{ inputs.tag || 'HEAD' }} + run: | + case $PYPI in + pypi) + DOMAIN=pypi.org + ;; + testpypi) + DOMAIN=test.pypi.org + ;; + *) + exit 1 + esac + + RELREV=$(python scripts/relrev.py --domain "$DOMAIN") + VERSION=$(date +%Y%m) + CURREV=$(python scripts/tagcore.py --ref $REF --version $VERSION) + + if [ -n "$CURREV" -a "$RELREV" = "$CURREV" ] + then + echo "current rev matches latest release, skip new release" + else + echo release=true >> $GITHUB_OUTPUT + fi - name: Install pypa/build + if: ${{ steps.check.outputs.release == 'true' }} run: python3 -m pip install build --user - name: Build wheel + if: ${{ steps.check.outputs.release == 'true' }} run: | python3 -m build -w ua-parser-builtins mv ua-parser-builtins/dist . - name: Store the distribution packages + if: ${{ steps.check.outputs.release == 'true' }} uses: actions/upload-artifact@v4 with: name: python-package-distributions @@ -51,9 +77,8 @@ jobs: publish-to-testpypi: name: Publish to TestPyPI - if: ${{ github.event.inputs.environment == 'testpypi' }} - needs: - - build + needs: build + if: ${{ github.event.inputs.environment == 'testpypi' && needs.build.outputs.release == 'true' }} runs-on: ubuntu-latest environment: @@ -78,9 +103,8 @@ jobs: publish-to-pypi: name: publish - if: ${{ github.event_name == 'schedule' || github.event.inputs.environment == 'pypi' }} - needs: - - build + needs: build + if: ${{ (github.event_name == 'schedule' || github.event.inputs.environment == 'pypi') && needs.build.outputs.release == 'true' }} runs-on: ubuntu-latest environment: name: pypi diff --git a/scripts/relrev.py b/scripts/relrev.py new file mode 100644 index 0000000..0979a40 --- /dev/null +++ b/scripts/relrev.py @@ -0,0 +1,67 @@ +import argparse +import contextlib +import hashlib +import json +import re +import shutil +import sys +import tempfile +import zipfile +from urllib import parse, request + +parser = argparse.ArgumentParser( + description="Retrieves the revision for the latest release of ua-parser-builtins", +) +parser.add_argument( + "--domain", + default="pypi.org", +) +args = parser.parse_args() + +url = parse.urlunsplit(("https", args.domain, "simple/ua-parser-builtins", "", "")) + +print("checking", url, file=sys.stderr) +res = request.urlopen( + request.Request( + url, + headers={ + "Accept": "application/vnd.pypi.simple.v1+json", + }, + ) +) +if res.status != 200: + exit(f"Failed to retrieve project distributions: {res.status}") + +distributions = json.load(res) +version, distribution = next( + (v, d) + for v, d in zip( + reversed(distributions["versions"]), reversed(distributions["files"]) + ) + if not d["yanked"] + if re.fullmatch( + r"(\d+!)?\d+(\.\d+)*(\.post\d+)?", + v, + flags=re.ASCII, + ) +) +print("latest version:", version, file=sys.stderr) + +res = request.urlopen(distribution["url"]) +if res.status != 200: + exit(f"Failed to retrieve wheel: {res.status}") + +with tempfile.SpooledTemporaryFile(256 * 1024) as tf: + shutil.copyfileobj(res, tf) + for name, val in distribution["hashes"].items(): + tf.seek(0) + d = hashlib.file_digest(tf, name).hexdigest() + if d != val: + exit(f"{name} mismatch: expected {val!r} got {d!r}") + tf.seek(0) + with zipfile.ZipFile(tf) as z: + # if the REVISION file is not found then it's fine it's a + # pre-calver release (hopefully) and that means we should cut + # a calver one + with contextlib.suppress(KeyError): + print(z.read("REVISION").decode()) diff --git a/scripts/tagcore.py b/scripts/tagcore.py new file mode 100644 index 0000000..a5ef7f8 --- /dev/null +++ b/scripts/tagcore.py @@ -0,0 +1,72 @@ +import argparse +import datetime +import pathlib +import shutil +import subprocess + +CORE_REMOTE = "https://github.com/ua-parser/uap-core" + + +parser = argparse.ArgumentParser( + description="""Updates `uap-core` to `ref` and tags it with `version` + +If successful, writes the commit to `REVISION` and prints it to stdout. +""" +) +parser.add_argument( + "--ref", + default="HEAD", + help="uap-core ref to build, defaults to HEAD (the head of the default branch)", +) +parser.add_argument( + "--version", + help="version to tag the package as, defaults to an YMD calendar version matching the ref's commit date", +) +args = parser.parse_args() + + +if not shutil.which("git"): + exit("git required") + +r = subprocess.run( + ["git", "ls-remote", CORE_REMOTE, args.ref], + encoding="utf-8", + stdout=subprocess.PIPE, +) +if r.returncode: + exit("Unable to query uap-core repo") + +if r.stdout: + if r.stdout.count("\n") > 1: + exit(f"Found multiple matching refs for {args.ref}:\n{r.stdout}") + commit, _rest = r.stdout.split("\t", 1) +else: + try: + int(args.ref, 16) + commit = args.ref + except ValueError: + exit(f"Unknown or invalid ref {args.ref!r}") + +CORE_PATH = pathlib.Path(__file__).resolve().parent.parent / "uap-core" + +r = subprocess.run(["git", "-C", CORE_PATH, "fetch", CORE_REMOTE, commit]) +if r.returncode: + exit(f"Unable to retrieve commit {commit!r}") + +if args.version: + tagname = args.version +else: + r = subprocess.run( + ["git", "-C", CORE_PATH, "show", "-s", "--format=%cs", commit], + encoding="utf-8", + stdout=subprocess.PIPE, + ) + if r.returncode or not r.stdout: + exit(f"Unable to retrieve commit date from commit {commit!r}") + + tagname = datetime.date.fromisoformat(r.stdout.rstrip()).strftime("%Y%m%d") + +subprocess.run(["git", "-C", CORE_PATH, "switch", "-d", commit]) +subprocess.run(["git", "-C", CORE_PATH, "tag", tagname, commit]) +CORE_PATH.joinpath("REVISION").write_text(commit) +print(commit) diff --git a/ua-parser-builtins/hatch_build.py b/ua-parser-builtins/hatch_build.py index 9bbe23f..2a86012 100644 --- a/ua-parser-builtins/hatch_build.py +++ b/ua-parser-builtins/hatch_build.py @@ -41,6 +41,10 @@ def initialize( version: str, build_data: dict[str, Any], ) -> None: + rev = os.path.join(self.root, "uap-core/REVISION") + if os.path.exists(rev): + build_data["force_include"][rev] = "REVISION" + with open(os.path.join(self.root, "uap-core/regexes.yaml"), "rb") as f: data = yaml.safe_load(f)