Skip to content

Commit b1914b2

Browse files
committed
[WIP] Implement release file creation for pydotorg
1 parent 75c7c35 commit b1914b2

File tree

2 files changed

+127
-3
lines changed

2 files changed

+127
-3
lines changed

ci/release.yml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -357,13 +357,13 @@ stages:
357357
$hashes = $files | `
358358
Sort-Object Name | `
359359
Format-Table Name, @{
360-
Label="MD5";
361-
Expression={(Get-FileHash $_ -Algorithm MD5).Hash}
360+
Label="SHA256";
361+
Expression={(Get-FileHash $_ -Algorithm SHA256).Hash}
362362
}, Length -AutoSize | `
363363
Out-String -Width 4096
364364
$hashes
365365
workingDirectory: $(DIST_DIR)
366-
displayName: 'Generate hashes (MD5)'
366+
displayName: 'Generate hashes (SHA256)'
367367
368368
- ${{ if eq(parameters.Publish, 'true') }}:
369369
- ${{ if eq(parameters.Sign, 'true') }}:
@@ -404,6 +404,8 @@ stages:
404404
UPLOAD_HOST_KEY: $(PyDotOrgHostKey)
405405
UPLOAD_USER: $(PyDotOrgUsername)
406406
UPLOAD_KEYFILE: $(sshkey.secureFilePath)
407+
RELEASE_API_KEY: $(PyDotOrgApiKey)
408+
BUNDLED_RUNTIME_VERSION: 3.14.3
407409
${{ if ne(parameters.Sign, 'true') }}:
408410
NO_UPLOAD: 1
409411

ci/upload.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import hashlib
12
import os
23
import subprocess
34
import sys
@@ -14,6 +15,12 @@
1415
UPLOAD_KEYFILE = os.getenv("UPLOAD_KEYFILE", "")
1516
UPLOAD_USER = os.getenv("UPLOAD_USER", "")
1617
NO_UPLOAD = os.getenv("NO_UPLOAD", "no")[:1].lower() in "yt1"
18+
RELEASE_API_URL = os.getenv("RELEASE_API_URL", "https://www.python.org/api").rstrip("/")
19+
RELEASE_API_KEY = os.getenv("RELEASE_API_KEY")
20+
21+
RELEASE_API_HEADERS = {"Content-Type": "application/json; charset=utf-8"}
22+
if RELEASE_API_KEY:
23+
RELEASE_API_HEADERS["Authorization"] = f"ApiKey {RELEASE_API_KEY}"
1724

1825
# Set to 'true' when updating index.json, rather than the app
1926
UPLOADING_INDEX = os.getenv("UPLOADING_INDEX", "no")[:1].lower() in "yt1"
@@ -126,6 +133,14 @@ def url2path(url):
126133
return UPLOAD_PATH_PREFIX + url[len(UPLOAD_URL_PREFIX) :]
127134

128135

136+
def sha256_for(file):
137+
h = hashlib.sha256()
138+
with open(file, "rb") as f:
139+
while b := f.read(1024 * 1024):
140+
h.update(b)
141+
return h.hexdigest().upper()
142+
143+
129144
def appinstaller_uri_matches(file, name):
130145
NS = {}
131146
with open(file, "r", encoding="utf-8") as f:
@@ -186,6 +201,101 @@ def validate_appinstaller(file, uploads):
186201
print()
187202

188203

204+
_get_release_id_cache = {}
205+
def get_release_id(**params):
206+
if not RELEASE_API_URL:
207+
raise RuntimeError("Cannot query object when RELEASE_API_URL is not set")
208+
uri = f"{RELEASE_API_URL}/v1/downloads/release/"
209+
uri += "?" + "&".join(f"{k}={v}" for k, v in params.items())
210+
try:
211+
return _get_release_id_cache[uri]
212+
except KeyError:
213+
pass
214+
req = Request(uri, method="GET", headers=RELEASE_API_HEADERS)
215+
with urlopen(req) as resp:
216+
if resp.status != 200:
217+
raise RuntimeError(f"no release for {params!r}: Status {resp.status}")
218+
obj = json.loads(resp.read())["objects"][0]
219+
_get_release_id_cache[url] = obj["resource_url"]
220+
return obj["resource_uri"]
221+
222+
223+
def calculate_release_file(file, url, upload_path):
224+
if not file:
225+
return
226+
if not url:
227+
print("Skipping", file, "as no URL was provided")
228+
return
229+
m = re.match(r".*?(\d+(?:\.\d+)*(?:(?:a|b|rc)?\d+))$", file.stem)
230+
if not m:
231+
print("Skipping", file, "as no version was found in filename")
232+
return
233+
slug = m.group(1).replace(".", "")
234+
rel_pk = get_release_id(slug=f"pymanager-{slug}")
235+
if not rel_pk:
236+
print("Skipping", slug, "as no release was found")
237+
return
238+
data = {
239+
"os": "/api/v1/downloads/os/windows/",
240+
"is_source": False,
241+
"url": url,
242+
"release": rel_pk,
243+
"sha256_sum": sha256sum_for(filename),
244+
"filesize": file.stat().st_size,
245+
"download_button": False,
246+
}
247+
if file.match("*.msix"):
248+
return {
249+
**data,
250+
"name": "Installer (MSIX)",
251+
"slug": f"pymanager-{slug}-msix",
252+
"description": f"Bundles Python {BUNDLED_RUNTIME_VERSION}",
253+
"download_button": True,
254+
}
255+
if file.match("*.msi"):
256+
return {
257+
**data,
258+
"name": "MSI package",
259+
"slug": f"pymanager-{slug}-msi",
260+
"description": "See documentation before use",
261+
}
262+
263+
264+
def publish_release_files(file_data):
265+
if not file_data:
266+
return
267+
print("Publishing:")
268+
print(json.dumps(file_data, indent=2))
269+
if NO_UPLOAD:
270+
print("Skipping release files due to NO_UPLOAD")
271+
return
272+
if not RELEASE_API_URL:
273+
raise RuntimeError("Cannot publish object when RELEASE_API_URL is not set")
274+
if not RELEASE_API_KEY:
275+
raise RuntimeError("Cannot publish object when RELEASE_API_KEY is not set")
276+
rel_pk = int(file_data["release"].rstrip("/").rpartition("/")[2]
277+
print("Deleting files from release", rel)
278+
u = f"{RELEASE_API_URL}/v1/downloads/release_file/?release={rel}"
279+
req = Request(u, method="DELETE", headers=RELEASE_API_HEADERS)
280+
with urlopen(req) as r:
281+
if 200 <= r.status < 300:
282+
print(f"Deleted successfully (status={r.status}).")
283+
else:
284+
print(f"Failed to delete (status={r.status}). Attempting to publish anyway.")
285+
286+
print("Publishing release file")
287+
u = f"{RELEASE_API_URL}/v1/downloads/release_file/"
288+
data = json.dumps(file_data).encode("utf-8")
289+
req = Request(u, method="POST", data=data, headers=RELEASE_API_HEADERS)
290+
with urlopen(req) as r:
291+
if 200 <= r.status < 300:
292+
print(f"Created successfully (status={r.status}).")
293+
else:
294+
print(f"Failed to create (status={r.status}).")
295+
print("Publishing complete")
296+
297+
298+
189299
def purge(url):
190300
if not UPLOAD_HOST or NO_UPLOAD:
191301
print("Skipping purge of", url, "because UPLOAD_HOST is missing")
@@ -242,3 +352,15 @@ def purge(url):
242352

243353
# Purge the upload directory so that the FTP browser is up to date
244354
purge(UPLOAD_URL)
355+
356+
357+
if RELEASE_API_URL:
358+
files = []
359+
for f, u, p in UPLOADS:
360+
fd = calculate_release_file(f, u, p)
361+
if fd:
362+
files.append(fd)
363+
364+
print("Releasing", len(files), "files")
365+
for fd in files:
366+
publish_release_file(fd)

0 commit comments

Comments
 (0)