Prepare v1.0.0 with rebuilt runtimes and HD coverage note
This commit is contained in:
1 parent
ea6e937b91
commit
cd31949383
18 files changed
+357
-42
No files matched your search
@@ -97,5 +97,56 @@ class FrozenDraftUploadTests(unittest.TestCase):
|
||||
upload.DraftExporter(package, state, root / 'single')
|
||||
|
||||
|
||||
class ConfigurableReleaseTargetTests(unittest.TestCase):
|
||||
def test_stable_and_preview_guards_require_the_explicit_expected_state(self):
|
||||
stable = upload.ReleaseTarget('c' * 40, 'v1.0.0', 12345, False)
|
||||
release = {'id': 12345, 'tag_name': 'v1.0.0', 'target_commitish': 'c' * 40,
|
||||
'draft': True, 'prerelease': False}
|
||||
upload.checked_release(release, stable)
|
||||
with self.assertRaises(ValueError):
|
||||
upload.checked_release(dict(release, prerelease=True), stable)
|
||||
with self.assertRaises(ValueError):
|
||||
upload.checked_release(release) # Legacy preview defaults cannot match stable.
|
||||
reference = {'ref': 'refs/tags/v1.0.0', 'object': {'type': 'commit', 'sha': 'c' * 40}}
|
||||
upload.checked_tag(reference, target=stable)
|
||||
with self.assertRaises(ValueError):
|
||||
upload.checked_tag(dict(reference, object={'type': 'commit', 'sha': upload.COMMIT}), target=stable)
|
||||
|
||||
def test_invalid_exact_release_inputs_fail_before_any_directory_or_upload(self):
|
||||
for values in (("main", 'v1.0.0', 123, False), ('A' * 40, 'v1.0.0', 123, False),
|
||||
('c' * 40, '../v1', 123, False), ('c' * 40, 'v1/other', 123, False),
|
||||
('c' * 40, 'v1.0.0', 0, False), ('c' * 40, 'v1.0.0', True, False),
|
||||
('c' * 40, 'v1.0.0', 123, 'false')):
|
||||
with self.subTest(values=values), self.assertRaises(ValueError):
|
||||
upload.ReleaseTarget(*values)
|
||||
|
||||
def test_explicit_release_routes_api_and_upload_to_its_exact_id_and_tag(self):
|
||||
target = upload.ReleaseTarget('c' * 40, 'v1.0.0', 12345, False)
|
||||
with tempfile.TemporaryDirectory() as temp:
|
||||
root = Path(temp)
|
||||
package = root / 'frozen'
|
||||
package.mkdir()
|
||||
controller = upload.DraftExporter(package, root / 'state', root / 'single', target)
|
||||
reference = {'ref': 'refs/tags/v1.0.0', 'object': {'type': 'commit', 'sha': 'c' * 40}}
|
||||
release = {'id': 12345, 'tag_name': 'v1.0.0', 'target_commitish': 'c' * 40,
|
||||
'draft': True, 'prerelease': False, 'assets': []}
|
||||
with patch.object(controller, 'api', side_effect=[reference, release]) as api:
|
||||
self.assertEqual(controller.release(), release)
|
||||
self.assertEqual(api.call_args_list[0].args[0], 'repos/Quintine/RustRaptor/git/ref/tags/v1.0.0')
|
||||
self.assertEqual(api.call_args_list[1].args[0], 'repos/Quintine/RustRaptor/releases/12345')
|
||||
archive = root / 'single/one.zip'
|
||||
archive.write_bytes(b'fixture')
|
||||
wrong = {'name': 'one.zip', 'size': archive.stat().st_size, 'state': 'uploaded', 'digest': 'sha256:' + '0' * 64}
|
||||
with patch.object(controller, 'snapshot'), patch.object(controller, 'release', return_value=release), \
|
||||
patch.object(controller, 'log'), patch.object(upload.subprocess, 'run') as run:
|
||||
run.return_value.returncode = 0
|
||||
run.return_value.stdout = json.dumps(wrong)
|
||||
run.return_value.stderr = ''
|
||||
with self.assertRaises(ValueError):
|
||||
controller.upload(archive, 'one.zip', upload.digest(archive), archive.stat().st_size)
|
||||
self.assertIn('/releases/12345/assets?name=one.zip', run.call_args.args[0][4])
|
||||
self.assertTrue(archive.exists())
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Export the frozen preview to its existing empty draft; never publish it."""
|
||||
"""Export an exact frozen release to its existing empty draft; never publish it."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
@@ -9,16 +9,37 @@ import subprocess
|
||||
import sys
|
||||
import time
|
||||
from pathlib import Path
|
||||
from dataclasses import dataclass
|
||||
from urllib.parse import quote
|
||||
|
||||
REPO = 'Quintine/RustRaptor'
|
||||
TAG = 'v0.1.0-preview.20261020'
|
||||
COMMIT = '7ecaa42b29ff0db5d2f7cdfe6da0a3221dfea555'
|
||||
RELEASE_ID = 418286902
|
||||
UPLOAD_URL = f'https://uploads.github.com/repos/{REPO}/releases/{RELEASE_ID}/assets'
|
||||
FLOOR = 1_800_000_000
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReleaseTarget:
|
||||
commit: str = COMMIT
|
||||
tag: str = TAG
|
||||
release_id: int = RELEASE_ID
|
||||
prerelease: bool = True
|
||||
|
||||
def __post_init__(self):
|
||||
if not isinstance(self.commit, str) or not re.fullmatch('[0-9a-f]{40}', self.commit):
|
||||
raise ValueError('release commit must be an exact lowercase 40-character SHA')
|
||||
if not isinstance(self.tag, str) or not re.fullmatch(r'v[0-9A-Za-z._-]+', self.tag):
|
||||
raise ValueError('release tag must be a safe exact version tag')
|
||||
if type(self.release_id) is not int or self.release_id <= 0:
|
||||
raise ValueError('release ID must be a positive integer')
|
||||
if type(self.prerelease) is not bool:
|
||||
raise ValueError('prerelease expectation must be a boolean')
|
||||
|
||||
|
||||
PREVIEW_TARGET = ReleaseTarget()
|
||||
|
||||
|
||||
def digest(path):
|
||||
with Path(path).open('rb') as stream:
|
||||
return hashlib.file_digest(stream, 'sha256').hexdigest()
|
||||
@@ -30,24 +51,24 @@ def write_json(path, value):
|
||||
stream.write('\n')
|
||||
|
||||
|
||||
def checked_release(release):
|
||||
if (release.get('id') != RELEASE_ID or release.get('tag_name') != TAG
|
||||
or release.get('target_commitish') != COMMIT or release.get('draft') is not True
|
||||
or release.get('prerelease') is not True):
|
||||
def checked_release(release, target=PREVIEW_TARGET):
|
||||
if (release.get('id') != target.release_id or release.get('tag_name') != target.tag
|
||||
or release.get('target_commitish') != target.commit or release.get('draft') is not True
|
||||
or release.get('prerelease') is not target.prerelease):
|
||||
raise ValueError('existing release ID/tag/target/draft state differs')
|
||||
return release
|
||||
|
||||
|
||||
def checked_tag(reference, annotated=None):
|
||||
if reference.get('ref') != 'refs/tags/' + TAG:
|
||||
def checked_tag(reference, annotated=None, target=PREVIEW_TARGET):
|
||||
if reference.get('ref') != 'refs/tags/' + target.tag:
|
||||
raise ValueError('wrong frozen tag reference')
|
||||
obj = reference.get('object', {})
|
||||
if obj.get('type') == 'tag':
|
||||
if (not isinstance(annotated, dict) or annotated.get('sha') != obj.get('sha')
|
||||
or annotated.get('tag') != TAG):
|
||||
or annotated.get('tag') != target.tag):
|
||||
raise ValueError('annotated tag identity differs')
|
||||
obj = annotated.get('object', {})
|
||||
if obj.get('type') != 'commit' or obj.get('sha') != COMMIT:
|
||||
if obj.get('type') != 'commit' or obj.get('sha') != target.commit:
|
||||
raise ValueError('frozen tag no longer points to the tested commit')
|
||||
|
||||
|
||||
@@ -70,7 +91,8 @@ def checked_receipt(plan, plan_sha, asset, receipt):
|
||||
|
||||
|
||||
class DraftExporter:
|
||||
def __init__(self, package, state, temporary):
|
||||
def __init__(self, package, state, temporary, target=PREVIEW_TARGET):
|
||||
self.target = target
|
||||
self.package = package.resolve(strict=True)
|
||||
self.state = state.absolute()
|
||||
self.temporary = temporary.absolute()
|
||||
@@ -98,7 +120,7 @@ class DraftExporter:
|
||||
return json.loads(subprocess.check_output(['gh', 'api', endpoint], cwd=self.package))
|
||||
|
||||
def release(self):
|
||||
reference = self.api(f'repos/{REPO}/git/ref/tags/{TAG}')
|
||||
reference = self.api(f'repos/{REPO}/git/ref/tags/{self.target.tag}')
|
||||
obj = reference.get('object', {})
|
||||
annotated = None
|
||||
if obj.get('type') == 'tag':
|
||||
@@ -106,14 +128,14 @@ class DraftExporter:
|
||||
if not re.fullmatch('[0-9a-f]{40}', sha):
|
||||
raise ValueError('unsafe annotated tag SHA')
|
||||
annotated = self.api(f'repos/{REPO}/git/tags/{sha}')
|
||||
checked_tag(reference, annotated)
|
||||
return checked_release(self.api(f'repos/{REPO}/releases/{RELEASE_ID}'))
|
||||
checked_tag(reference, annotated, self.target)
|
||||
return checked_release(self.api(f'repos/{REPO}/releases/{self.target.release_id}'), self.target)
|
||||
|
||||
def snapshot(self):
|
||||
actual = {'commit': subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=self.package, text=True).strip(),
|
||||
'runtime_manifest_sha256': digest(self.package / 'runtime-manifest.json'),
|
||||
'windows_manifest_sha256': digest(self.package / 'runtime-windows-manifest.json')}
|
||||
if actual['commit'] != COMMIT or subprocess.check_output(
|
||||
if actual['commit'] != self.target.commit or subprocess.check_output(
|
||||
['git', 'status', '--porcelain', '--untracked-files=all'], cwd=self.package):
|
||||
raise ValueError('frozen source commit or cleanliness differs')
|
||||
if hasattr(self, 'plan') and actual != self.plan['snapshot']:
|
||||
@@ -138,7 +160,7 @@ class DraftExporter:
|
||||
or digest(path) != expected_sha):
|
||||
raise ValueError('local upload no longer matches authenticated receipt')
|
||||
self.log('uploading', asset=name, bytes=expected_size, sha256=expected_sha)
|
||||
result = subprocess.run(['gh', 'api', '--method', 'POST', UPLOAD_URL + '?name=' + quote(name, safe=''),
|
||||
result = subprocess.run(['gh', 'api', '--method', 'POST', f'https://uploads.github.com/repos/{REPO}/releases/{self.target.release_id}/assets?name=' + quote(name, safe=''),
|
||||
'--input', str(path), '-H', 'Content-Type: application/octet-stream'],
|
||||
cwd=self.package, capture_output=True, text=True)
|
||||
(self.state / (name + '.upload.stdout')).write_text(result.stdout)
|
||||
@@ -163,12 +185,12 @@ class DraftExporter:
|
||||
raise ValueError('draft must be completely empty before this one-pass export')
|
||||
write_json(self.state / 'initial-empty-draft.json', initial)
|
||||
subprocess.run([sys.executable, '-B', str(self.exporter), 'plan', '--package', str(self.package),
|
||||
'--expected-commit', COMMIT, '--output', str(self.plan_path)], check=True, cwd=self.package)
|
||||
'--expected-commit', self.target.commit, '--output', str(self.plan_path)], check=True, cwd=self.package)
|
||||
self.plan = json.loads(self.plan_path.read_text())
|
||||
plan_sha = digest(self.plan_path)
|
||||
if (len(self.plan['assets']) != 9 or self.plan['snapshot']['commit'] != COMMIT
|
||||
if (len(self.plan['assets']) != 9 or self.plan['snapshot']['commit'] != self.target.commit
|
||||
or sorted(asset['kind'] for asset in self.plan['assets']) != ['linux'] + ['shared-data'] * 7 + ['windows']):
|
||||
raise ValueError('frozen preview no longer has exactly the expected nine ZIPs')
|
||||
raise ValueError('frozen release no longer has exactly the expected nine ZIPs')
|
||||
for asset in self.plan['assets']:
|
||||
self.snapshot()
|
||||
self.remote_members(self.release())
|
||||
@@ -188,15 +210,15 @@ class DraftExporter:
|
||||
manifest = self.state / 'release-manifest.json'
|
||||
subprocess.run([sys.executable, '-B', str(self.exporter), 'checksums', '--plan', str(self.plan_path),
|
||||
'--receipts', str(self.receipts), '--output', str(sums),
|
||||
'--manifest-output', str(manifest), '--tag', TAG], check=True, cwd=self.package)
|
||||
'--manifest-output', str(manifest), '--tag', self.target.tag], check=True, cwd=self.package)
|
||||
for path in (sums, manifest, self.package / 'release/Install-Linux.sh', self.package / 'release/Install-Windows.ps1'):
|
||||
self.upload(path, path.name, digest(path), path.stat().st_size)
|
||||
final = self.release()
|
||||
self.remote_members(final)
|
||||
if len(final['assets']) != 13:
|
||||
raise ValueError('complete preview must contain nine ZIPs plus four companion assets')
|
||||
raise ValueError('complete release must contain nine ZIPs plus four companion assets')
|
||||
write_json(self.state / 'final-13-assets-draft-still-unpublished.json', final)
|
||||
self.log('COMPLETE_THIRTEEN_REMOTE_ASSETS_VERIFIED_DRAFT_NOT_PUBLISHED', checkpoint=COMMIT)
|
||||
self.log('COMPLETE_THIRTEEN_REMOTE_ASSETS_VERIFIED_DRAFT_NOT_PUBLISHED', checkpoint=self.target.commit)
|
||||
|
||||
|
||||
def main():
|
||||
@@ -204,8 +226,13 @@ def main():
|
||||
parser.add_argument('--package', type=Path, required=True)
|
||||
parser.add_argument('--state', type=Path, required=True)
|
||||
parser.add_argument('--temporary', type=Path, required=True)
|
||||
parser.add_argument('--commit', default=COMMIT)
|
||||
parser.add_argument('--tag', default=TAG)
|
||||
parser.add_argument('--release-id', type=int, default=RELEASE_ID)
|
||||
parser.add_argument('--prerelease', choices=('true', 'false'), default='true')
|
||||
args = parser.parse_args()
|
||||
DraftExporter(args.package, args.state, args.temporary).run()
|
||||
target = ReleaseTarget(args.commit, args.tag, args.release_id, args.prerelease == 'true')
|
||||
DraftExporter(args.package, args.state, args.temporary, target).run()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
Reference in new issue
Block a user