Skip to content

Commit c58c8a4

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.
1 parent 0f2c13f commit c58c8a4

File tree

4 files changed

+183
-12
lines changed

4 files changed

+183
-12
lines changed

.github/workflows/release-builtins.yml

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,40 +20,62 @@ 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+
run: |
39+
case ${{ github.event.inputs.environment }} in
40+
pypi)
41+
DOMAIN=pypi.org
42+
;;
43+
testpypi)
44+
DOMAIN=test.pypi.org
45+
;;
46+
*)
47+
exit 1
48+
esac
49+
50+
RELREV=$(python scripts/relrev.py --domain "$DOMAIN")
51+
VERSION=$(date +%Y%m)
52+
CURREV=$(python scripts/tagcore.py --ref ${{ inputs.tag || 'HEAD' }} --version $VERSION)
53+
54+
if [ -n "$CURREV" -a "$RELREV" = "$CURREV" ]
55+
then
56+
echo "current rev matches latest release, skip new release"
57+
else
58+
echo release=true >> $GITHUB_OUTPUT
59+
fi
4060
- name: Install pypa/build
61+
if: ${{ steps.check.outputs.release == 'true' }}
4162
run: python3 -m pip install build --user
4263
- name: Build wheel
64+
if: ${{ steps.check.outputs.release == 'true' }}
4365
run: |
4466
python3 -m build -w ua-parser-builtins
4567
mv ua-parser-builtins/dist .
4668
- name: Store the distribution packages
69+
if: ${{ steps.check.outputs.release == 'true' }}
4770
uses: actions/upload-artifact@v4
4871
with:
4972
name: python-package-distributions
5073
path: dist/
5174

5275
publish-to-testpypi:
5376
name: Publish to TestPyPI
54-
if: ${{ github.event.inputs.environment == 'testpypi' }}
55-
needs:
56-
- build
77+
needs: build
78+
if: ${{ github.event.inputs.environment == 'testpypi' && needs.build.outputs.release == 'true' }}
5779
runs-on: ubuntu-latest
5880

5981
environment:
@@ -78,9 +100,8 @@ jobs:
78100

79101
publish-to-pypi:
80102
name: publish
81-
if: ${{ github.event_name == 'schedule' || github.event.inputs.environment == 'pypi' }}
82-
needs:
83-
- build
103+
needs: build
104+
if: ${{ (github.event_name == 'schedule' || github.event.inputs.environment == 'pypi') && needs.build.outputs.release == 'true' }}
84105
runs-on: ubuntu-latest
85106
environment:
86107
name: pypi

scripts/relrev.py

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

scripts/tagcore.py

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