mirror of
https://github.com/SK8-ENGINE/skate-3-rust-engine.git
synced 2026-10-27 19:54:40 +00:00
116 lines
5.5 KiB
Python
116 lines
5.5 KiB
Python
"""Publish one rolling prerelease, with complete build-specific asset sets."""
|
|
import argparse
|
|
import hashlib
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import shutil
|
|
import subprocess
|
|
import zipfile
|
|
|
|
from updater import REPO, PACKAGE, PREFIX, identity, program_metadata, package_name
|
|
|
|
TAG = 'experimental'
|
|
|
|
|
|
def gh(*args, payload=None):
|
|
command = ['gh', *args]
|
|
if payload is not None:
|
|
command += ['--input', '-']
|
|
result = subprocess.run(command, input=json.dumps(payload) if payload is not None else None,
|
|
text=True, capture_output=True)
|
|
if result.returncode:
|
|
raise RuntimeError(result.stderr.strip() or 'GitHub command failed')
|
|
return result.stdout
|
|
|
|
|
|
def api(path, method='GET', payload=None):
|
|
return json.loads(gh('api', f'repos/{REPO}/{path}', '--method', method, payload=payload) or 'null')
|
|
|
|
|
|
def complete_builds(assets):
|
|
names = {a['name'] for a in assets if a.get('state') == 'uploaded'}
|
|
result = []
|
|
for name in names:
|
|
match = re.fullmatch(r'release-([1-9][0-9]*)\.json', name)
|
|
if match:
|
|
build = int(match[1])
|
|
package = f'skate3rust-windows-x64-build-{build}.zip'
|
|
if {package, package + '.sha256'} <= names:
|
|
result.append(build)
|
|
return sorted(result, reverse=True)
|
|
|
|
|
|
def validate(directory):
|
|
meta = json.loads((directory/'release.json').read_text(encoding='utf-8-sig'))
|
|
identity(meta)
|
|
program_metadata(meta)
|
|
if meta['tag'] != TAG:
|
|
raise ValueError('Expected experimental package')
|
|
archive = directory/PACKAGE
|
|
with archive.open('rb') as stream:
|
|
digest = hashlib.file_digest(stream, 'sha256').hexdigest()
|
|
checksum = (directory/(PACKAGE+'.sha256')).read_text().split()
|
|
if checksum != [digest, PACKAGE]:
|
|
raise ValueError('Package checksum mismatch')
|
|
with zipfile.ZipFile(archive) as z:
|
|
if json.loads(z.read(PREFIX+'release.json').decode('utf-8-sig')) != meta:
|
|
raise ValueError('Package identity mismatch')
|
|
for name, expected in meta['files'].items():
|
|
if hashlib.sha256(z.read(PREFIX+name)).hexdigest() != expected:
|
|
raise ValueError('Program checksum mismatch')
|
|
return meta, digest
|
|
|
|
|
|
def publish(directory):
|
|
meta, digest = validate(directory)
|
|
if api('git/ref/heads/main')['object']['sha'] != meta['revision']:
|
|
print('A newer main commit exists; leaving Experimental unchanged.')
|
|
return
|
|
# Include drafts so interrupted first publication can resume without another release.
|
|
releases = json.loads(gh('api', f'repos/{REPO}/releases?per_page=100', '--paginate', '--slurp'))
|
|
release = next((r for page in releases for r in page if r['tag_name'] == TAG), None)
|
|
if release and (release.get('immutable') or not release.get('prerelease')):
|
|
raise ValueError('Experimental must be a mutable prerelease')
|
|
if release and any(n >= meta['build'] for n in complete_builds(release.get('assets', []))) and not release['draft']:
|
|
print('This build or a newer build is already published.')
|
|
return
|
|
if release is None:
|
|
release = api('releases', 'POST', dict(tag_name=TAG, target_commitish=meta['revision'],
|
|
name='Experimental', draft=True, prerelease=True, make_latest='false'))
|
|
package = package_name(meta)
|
|
shutil.copyfile(directory/PACKAGE, directory/package)
|
|
(directory/(package+'.sha256')).write_text(digest+' '+package+'\n', encoding='ascii')
|
|
manifest = f"release-{meta['build']}.json"
|
|
shutil.copyfile(directory/'release.json', directory/manifest)
|
|
# Payload/checksum first, manifest last: clients ignore an incomplete upload.
|
|
for name in (package, package+'.sha256', manifest):
|
|
gh('release', 'upload', TAG, str(directory/name), '--repo', REPO, '--clobber')
|
|
current = api(f"releases/{release['id']}")
|
|
if meta['build'] not in complete_builds(current['assets']):
|
|
raise ValueError('Uploaded build is incomplete')
|
|
changes = gh('api', f'repos/{REPO}/commits?sha={meta["revision"]}&per_page=10')
|
|
commits = json.loads(changes)
|
|
notes = (f"Rolling build **{meta['build']}**, commit `{meta['revision'][:12]}`.\n\n"
|
|
f"[Download Windows ZIP](https://github.com/{REPO}/releases/download/{TAG}/{package})\n\n"
|
|
"Experimental builds may contain regressions. Choose Latest in the in-game Updates window "
|
|
"to receive these builds. Stable remains the default. Your existing maps and assets are kept.\n\n"
|
|
"### Recent commits\n\n" + '\n'.join(
|
|
f"- {c['sha'][:8]} {c['commit']['message'].splitlines()[0]}" for c in commits))
|
|
api(f"releases/{release['id']}", 'PATCH', dict(name='Experimental', body=notes,
|
|
target_commitish=meta['revision'], draft=False, prerelease=True, make_latest='false'))
|
|
api('git/refs/tags/'+TAG, 'PATCH', dict(sha=meta['revision'], force=True))
|
|
# Keep the previous complete download for clients already fetching it.
|
|
keep = set(complete_builds(current['assets'])[:2])
|
|
for asset in current['assets']:
|
|
match = re.fullmatch(r'(?:release-([1-9][0-9]*)\.json|skate3rust-windows-x64-build-([1-9][0-9]*)\.zip(?:\.sha256)?)', asset['name'])
|
|
if match and int(match[1] or match[2]) not in keep:
|
|
api(f"releases/assets/{asset['id']}", 'DELETE')
|
|
print(f"Published Experimental build {meta['build']}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('--directory', type=Path, required=True)
|
|
publish(parser.parse_args().directory)
|