Hand-installed attempt builds share the plain package version with the published release, so the updater never replaces them. The diagnostic page for an attempt ref now says so above the file table (decision 25).
159 lines
7.8 KiB
Python
159 lines
7.8 KiB
Python
"""Tag pages and release bodies publish the same independently named objects."""
|
|
import importlib.util
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from urllib.parse import unquote
|
|
from urllib.request import urlopen
|
|
|
|
import pytest
|
|
|
|
from tests.scripts.test_release_r2 import r2_server # noqa: F401
|
|
|
|
_SPEC = importlib.util.spec_from_file_location(
|
|
'render_builds_table', Path(__file__).resolve().parents[2] / 'scripts/render-builds-table.py')
|
|
assert _SPEC and _SPEC.loader
|
|
rbt = importlib.util.module_from_spec(_SPEC)
|
|
_SPEC.loader.exec_module(rbt)
|
|
|
|
|
|
@pytest.fixture
|
|
def release_body(monkeypatch):
|
|
state = {'body': '# Notes\n\n<!-- HERMES_BUILDS_TABLE -->\n\n## Changes\n- x\n', 'edits': []}
|
|
|
|
def gh(argv, **kwargs):
|
|
if argv[:3] == ['gh', 'release', 'view']:
|
|
return subprocess.CompletedProcess(argv, 0, json.dumps({'body': state['body']}), '')
|
|
assert argv[:3] == ['gh', 'release', 'edit']
|
|
state['body'] = kwargs['input']
|
|
state['edits'].append(state['body'])
|
|
return subprocess.CompletedProcess(argv, 0, '', '')
|
|
|
|
monkeypatch.setattr(rbt.subprocess, 'run', gh)
|
|
return state
|
|
|
|
|
|
@pytest.mark.parametrize('tag', ['v1.2.3', 'v1.2.3+canary.20260818T101010Z'])
|
|
def test_tag_publication_pending_rerun_stale_and_dry_run(monkeypatch, r2_server, release_body, tag):
|
|
base = f'http://127.0.0.1:{r2_server.server_port}/hermes-releases'
|
|
version = tag[1:]
|
|
prefix = f'releases/tag/{tag}/'
|
|
visible = [f'HermesBundled-{version}-mac-arm64.dmg', f'HermesBundled-{version}-win-x64.msix',
|
|
f'HermesBundled-{version}-win-arm64.msix', f'HermesBundled-{version}-linux-x64.AppImage',
|
|
f'HermesLight-{version}-win-x64.msix']
|
|
hidden = [f'HermesBundled-{version}-mac-arm64.zip', f'HermesBundled-{version}-win.msixbundle',
|
|
f'HermesBundled-{version}-win-x64.msix.blockmap', 'latest.yml',
|
|
'HermesBundled-9.9.9-win-x64.msix']
|
|
for name in visible + hidden:
|
|
r2_server.store[prefix + name] = (name.encode(), '"e"')
|
|
r2_server.store['releases/tag/v9.9.9/HermesBundled-9.9.9-win-x64.msix'] = (b'neighbor', '"e"')
|
|
monkeypatch.setenv('RELEASE_NEEDS', '{"build-win32":{"result":"success"}}')
|
|
args = ['render-builds-table.py', '--tag', tag, '--repo', 'o/r', '--r2-base-url', base + '/']
|
|
|
|
def render(*extra):
|
|
monkeypatch.setattr(sys, 'argv', [*args, *extra])
|
|
assert rbt.main() == 0
|
|
|
|
for run in ('1', '2'):
|
|
render('--pending-run-url', f'https://github.example/runs/{run}')
|
|
assert 'runs/1' not in release_body['body'] and release_body['body'].count('runs/2') == 1
|
|
assert not any(method == 'PUT' for method, _, _ in r2_server.requests)
|
|
render()
|
|
body = release_body['body']
|
|
assert 'runs/2' not in body and body.count('## Downloads') == 1
|
|
assert body.count(rbt.MARKER) == body.count(rbt.END_MARKER) == 1
|
|
expected = {rbt.r2.public_url_for(base, prefix + name) for name in visible}
|
|
assert set(re.findall(r'\]\((http[^)]+)\)', body)) == expected
|
|
with urlopen(f'{base}/{prefix}index.html', timeout=5) as response:
|
|
page = response.read().decode()
|
|
release_url = rbt.r2.public_url_for('https://github.com/o/r/releases/tag', tag)
|
|
assert set(re.findall(r'href="(http[^"]+)"', page)) == expected | {release_url}
|
|
assert rbt.recorded_build(page) == tag and 'Bundle environment' not in page
|
|
assert all(name not in page and name not in body for name in hidden)
|
|
assert 'linux-arm64' not in page
|
|
for url in expected:
|
|
with urlopen(url, timeout=5) as response:
|
|
assert response.read() == unquote(url.rsplit('/', 1)[1]).encode()
|
|
channel_key = 'releases/canary/index.html' if '+canary.' in tag else 'releases/stable/index.html'
|
|
assert r2_server.store[channel_key][0].decode() == page
|
|
render()
|
|
assert release_body['body'] == body
|
|
|
|
# Use the real page writer's marker format, independent of asset selection.
|
|
newer = rbt.render_page('v9.9.9', {}, base).encode()
|
|
r2_server.store[channel_key] = (newer, '"e"')
|
|
render()
|
|
assert r2_server.store[channel_key][0] == newer
|
|
assert r2_server.store[prefix + 'index.html'][0].decode() == page
|
|
before, edits = dict(r2_server.store), list(release_body['edits'])
|
|
render('--dry-run')
|
|
assert r2_server.store == before and release_body['edits'] == edits
|
|
release_body['body'] = 'no marker here'
|
|
render()
|
|
assert release_body['body'] == 'no marker here' and release_body['edits'] == edits
|
|
|
|
|
|
@pytest.mark.parametrize('asset_present', [False, True])
|
|
@pytest.mark.parametrize('result', ['failure', 'cancelled', 'skipped'])
|
|
def test_incomplete_tag_keeps_channel_and_links_diagnostics(monkeypatch, r2_server, release_body, asset_present, result):
|
|
tag = 'v1.2.3+canary.20260818T101010Z'
|
|
base = f'http://127.0.0.1:{r2_server.server_port}/hermes-releases'
|
|
key = f'releases/tag/{tag}/HermesBundled-{tag[1:]}-win-x64.msix'
|
|
r2_server.store['releases/canary/index.html'] = (b'previous good page', '"e"')
|
|
if asset_present:
|
|
r2_server.store[key] = (b'transport fixture', '"e"')
|
|
run = 'https://github.example/runs/123'
|
|
monkeypatch.setenv('RELEASE_NEEDS', json.dumps({'publish-win32-updater': {'result': result}}))
|
|
monkeypatch.setattr(sys, 'argv', ['renderer', '--tag', tag, '--r2-base-url', base, '--run-url', run])
|
|
assert rbt.main() == 0
|
|
with urlopen(f'{base}/releases/tag/{tag}/index.html', timeout=5) as response:
|
|
page = response.read().decode()
|
|
for output in (page, release_body['body']):
|
|
assert 'Build incomplete' in output and f'publish-win32-updater ({result})' in output
|
|
assert run in output
|
|
assert (rbt.r2.public_url_for(base, key) in output) == asset_present
|
|
assert ('No downloadable artifacts' in page) == (not asset_present)
|
|
assert r2_server.store['releases/canary/index.html'][0] == b'previous good page'
|
|
|
|
|
|
def test_attempt_page_warns_and_canary_page_does_not():
|
|
base = 'https://cdn.example'
|
|
sentence = ('Attempt builds are not upgrade-safe: every attempt of 1.2.3 has the same '
|
|
'package version, so an installed attempt is not replaced by the published 1.2.3.')
|
|
page = rbt.render_page('rc.1-v1.2.3', {}, base)
|
|
assert sentence in page
|
|
for tag in ('v1.2.3', 'v1.2.3+canary.20260818T101010Z', 'abandoned-rc.1-v1.2.3'):
|
|
assert sentence not in rbt.render_page(tag, {}, base)
|
|
|
|
|
|
@pytest.mark.parametrize('version,name', [
|
|
('1.2.3', 'HermesBundled-1.2.3-win-x64.msix'),
|
|
('1.2.3+canary.20260818T000000Z', 'HermesBundled-1.2.3+canary.20260818T000000Z-win-x64.msix'),
|
|
])
|
|
def test_exact_version_and_flat_name_boundaries(version, name):
|
|
names = [name, 'HermesBundled-1.2.3+canary.20260817T000000Z-win-x64.msix', 'HermesBundled-1.2.4-win-x64.msix', name + '.blockmap']
|
|
assert rbt.filter_names_for_version(names, version) == [name]
|
|
assert rbt.parse_assets([name])['HermesBundled'][('win', 'x64')] == (name, 'msix')
|
|
|
|
|
|
def test_attempt_archive_objects_are_listed_by_their_plain_version(monkeypatch):
|
|
keys = ['releases/tag/rc.2-v1.2.3/HermesBundled-1.2.3-win-x64.msix',
|
|
'releases/tag/rc.2-v1.2.3/HermesBundled-1.2.3-win-x64.msix.blockmap',
|
|
'releases/tag/rc.2-v1.2.3/latest.yml',
|
|
'releases/tag/rc.2-v1.2.3/HermesBundled-1.2.4-win-x64.msix']
|
|
monkeypatch.setattr(rbt, 'r2_object_names_under', lambda prefix: keys)
|
|
assert rbt.r2_object_names('rc.2-v1.2.3') == [keys[0]]
|
|
|
|
|
|
@pytest.mark.parametrize('current,tag,allowed', [
|
|
(None, 'v1.2.3', True), ('garbage', 'v1.2.3', True),
|
|
('v1.2.3', 'v1.2.3+canary.20260818T101010Z', False),
|
|
('v1.2.3+canary.20260818T101010Z', 'v1.2.3+canary.20260818T101009Z', False),
|
|
('v1.2.3+canary.20260818T101010Z', 'v1.2.3+canary.20260818T101011Z', True),
|
|
])
|
|
def test_channel_order_boundaries(current, tag, allowed):
|
|
page = rbt.render_page(current, {}, 'https://cdn.example') if current and current != 'garbage' else current
|
|
assert rbt.supersedes(page, tag) is allowed
|