Skip to content

Commit fdcddf1

Browse files
committed
Calver release of builtins
The updated workflow relies on the two new scripts: - `relrev` retrieves the REVISION of the latest published package - `tagcore` updates uap-core to the specified revision and writes out the commit hash to REVISION, printing it out to stdout The workflow calls those two scripts and check if they differ, in which case it cuts a new release. If the two revisions match the release is skipped. Fixes #277
1 parent 0f2c13f commit fdcddf1

File tree

4 files changed

+179
-12
lines changed

4 files changed

+179
-12
lines changed

.github/workflows/release-builtins.yml

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,40 +20,65 @@ jobs:
2020
build:
2121
name: Build distribution
2222
runs-on: ubuntu-latest
23-
23+
outputs:
24+
release: ${{ steps.check.outputs.release }}
2425
steps:
2526
- uses: actions/checkout@v4
2627
with:
2728
submodules: true
2829
fetch-depth: 0
2930
persist-credentials: false
30-
- name: update core
31-
env:
32-
TAG: ${{ inputs.tag || 'origin/master' }}
33-
# needs to detach because we can update to a tag
34-
run: git -C uap-core switch --detach "$TAG"
3531
- name: Set up Python
3632
uses: actions/setup-python@v5
3733
with:
3834
python-version: "3.x"
3935

36+
- name: Check necessity of release
37+
id: check
38+
env:
39+
PYPI: ${{ github.event.inputs.environment }}
40+
REF: ${{ inputs.tag || 'HEAD' }}
41+
run: |
42+
case $PYPI in
43+
pypi)
44+
DOMAIN=pypi.org
45+
;;
46+
testpypi)
47+
DOMAIN=test.pypi.org
48+
;;
49+
*)
50+
exit 1
51+
esac
52+
53+
RELREV=$(python scripts/relrev.py --domain "$DOMAIN")
54+
VERSION=$(date +%Y%m)
55+
CURREV=$(python scripts/tagcore.py --ref $REF --version $VERSION)
56+
57+
if [ -n "$CURREV" -a "$RELREV" = "$CURREV" ]
58+
then
59+
echo "current rev matches latest release, skip new release"
60+
else
61+
echo release=true >> $GITHUB_OUTPUT
62+
fi
4063
- name: Install pypa/build
64+
if: ${{ steps.check.outputs.release == 'true' }}
4165
run: python3 -m pip install build --user
4266
- name: Build wheel
67+
if: ${{ steps.check.outputs.release == 'true' }}
4368
run: |
4469
python3 -m build -w ua-parser-builtins
4570
mv ua-parser-builtins/dist .
4671
- name: Store the distribution packages
72+
if: ${{ steps.check.outputs.release == 'true' }}
4773
uses: actions/upload-artifact@v4
4874
with:
4975
name: python-package-distributions
5076
path: dist/
5177

5278
publish-to-testpypi:
5379
name: Publish to TestPyPI
54-
if: ${{ github.event.inputs.environment == 'testpypi' }}
55-
needs:
56-
- build
80+
needs: build
81+
if: ${{ github.event.inputs.environment == 'testpypi' && needs.build.outputs.release == 'true' }}
5782
runs-on: ubuntu-latest
5883

5984
environment:
@@ -78,9 +103,8 @@ jobs:
78103

79104
publish-to-pypi:
80105
name: publish
81-
if: ${{ github.event_name == 'schedule' || github.event.inputs.environment == 'pypi' }}
82-
needs:
83-
- build
106+
needs: build
107+
if: ${{ (github.event_name == 'schedule' || github.event.inputs.environment == 'pypi') && needs.build.outputs.release == 'true' }}
84108
runs-on: ubuntu-latest
85109
environment:
86110
name: pypi

scripts/relrev.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import argparse
2+
import contextlib
3+
import hashlib
4+
import json
5+
import re
6+
import shutil
7+
import sys
8+
import tempfile
9+
import zipfile
10+
from urllib import parse, request
11+
12+
parser = argparse.ArgumentParser(
13+
description="Retrieves the revision for the latest release of ua-parser-builtins",
14+
)
15+
parser.add_argument(
16+
"--domain",
17+
default="pypi.org",
18+
)
19+
args = parser.parse_args()
20+
21+
url = parse.urlunsplit(("https", args.domain, "simple/ua-parser-builtins", "", ""))
22+
23+
print("checking", url, file=sys.stderr)
24+
res = request.urlopen(
25+
request.Request(
26+
url,
27+
headers={
28+
"Accept": "application/vnd.pypi.simple.v1+json",
29+
},
30+
)
31+
)
32+
if res.status != 200:
33+
exit(f"Failed to retrieve project distributions: {res.status}")
34+
35+
distributions = json.load(res)
36+
version, distribution = next(
37+
(v, d)
38+
for v, d in zip(
39+
reversed(distributions["versions"]), reversed(distributions["files"])
40+
)
41+
if not d["yanked"]
42+
if re.fullmatch(
43+
r"(\d+!)?\d+(\.\d+)*(\.post\d+)?",
44+
v,
45+
flags=re.ASCII,
46+
)
47+
)
48+
print("latest version:", version, file=sys.stderr)
49+
50+
res = request.urlopen(distribution["url"])
51+
if res.status != 200:
52+
exit(f"Failed to retrieve wheel: {res.status}")
53+
54+
with tempfile.SpooledTemporaryFile(256 * 1024) as tf:
55+
shutil.copyfileobj(res, tf)
56+
for name, val in distribution["hashes"].items():
57+
tf.seek(0)
58+
d = hashlib.file_digest(tf, name).hexdigest()
59+
if d != val:
60+
exit(f"{name} mismatch: expected {val!r} got {d!r}")
61+
tf.seek(0)
62+
with zipfile.ZipFile(tf) as z:
63+
# if the REVISION file is not found then it's fine it's a
64+
# pre-calver release (hopefully) and that means we should cut
65+
# a calver one
66+
with contextlib.suppress(KeyError):
67+
print(z.read("REVISION").decode())

scripts/tagcore.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import argparse
2+
import datetime
3+
import pathlib
4+
import shutil
5+
import subprocess
6+
7+
CORE_REMOTE = "https://github.com/ua-parser/uap-core"
8+
9+
10+
parser = argparse.ArgumentParser(
11+
description="""Updates `uap-core` to `ref` and tags it with `version`
12+
13+
If successful, writes the commit to `REVISION` and prints it to stdout.
14+
"""
15+
)
16+
parser.add_argument(
17+
"--ref",
18+
default="HEAD",
19+
help="uap-core ref to build, defaults to HEAD (the head of the default branch)",
20+
)
21+
parser.add_argument(
22+
"--version",
23+
help="version to tag the package as, defaults to an YMD calendar version matching the ref's commit date",
24+
)
25+
args = parser.parse_args()
26+
27+
28+
if not shutil.which("git"):
29+
exit("git required")
30+
31+
r = subprocess.run(
32+
["git", "ls-remote", CORE_REMOTE, args.ref],
33+
encoding="utf-8",
34+
stdout=subprocess.PIPE,
35+
)
36+
if r.returncode:
37+
exit("Unable to query uap-core repo")
38+
39+
if r.stdout:
40+
if r.stdout.count("\n") > 1:
41+
exit(f"Found multiple matching refs for {args.ref}:\n{r.stdout}")
42+
commit, _rest = r.stdout.split("\t", 1)
43+
else:
44+
try:
45+
int(args.ref, 16)
46+
commit = args.ref
47+
except ValueError:
48+
exit(f"Unknown or invalid ref {args.ref!r}")
49+
50+
CORE_PATH = pathlib.Path(__file__).resolve().parent.parent / "uap-core"
51+
52+
r = subprocess.run(["git", "-C", CORE_PATH, "fetch", CORE_REMOTE, commit])
53+
if r.returncode:
54+
exit(f"Unable to retrieve commit {commit!r}")
55+
56+
if args.version:
57+
tagname = args.version
58+
else:
59+
r = subprocess.run(
60+
["git", "-C", CORE_PATH, "show", "-s", "--format=%cs", commit],
61+
encoding="utf-8",
62+
stdout=subprocess.PIPE,
63+
)
64+
if r.returncode or not r.stdout:
65+
exit(f"Unable to retrieve commit date from commit {commit!r}")
66+
67+
tagname = datetime.date.fromisoformat(r.stdout.rstrip()).strftime("%Y%m%d")
68+
69+
subprocess.run(["git", "-C", CORE_PATH, "switch", "-d", commit])
70+
subprocess.run(["git", "-C", CORE_PATH, "tag", tagname, commit])
71+
CORE_PATH.joinpath("REVISION").write_text(commit)
72+
print(commit)

ua-parser-builtins/hatch_build.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ def initialize(
4141
version: str,
4242
build_data: dict[str, Any],
4343
) -> None:
44+
rev = os.path.join(self.root, "uap-core/REVISION")
45+
if os.path.exists(rev):
46+
build_data["force_include"][rev] = "REVISION"
47+
4448
with open(os.path.join(self.root, "uap-core/regexes.yaml"), "rb") as f:
4549
data = yaml.safe_load(f)
4650

0 commit comments

Comments
 (0)