"""Native metadata and artifact publication use the same verified bytes."""
import hashlib
import io
import json
import zipfile
from pathlib import Path
import pytest
from scripts.bundles.release_artifacts import materialize, record, stamp_matches
from tests.scripts.test_release_r2 import r2_server # noqa: F401
def test_windows_metadata_is_read_from_package_and_stale_stamp_is_rejected(tmp_path):
tag, commit = 'v1.2.3', 'a' * 40
root = tmp_path / 'release'
root.mkdir()
package = root / 'Product-win-x64.msix'
manifest = ''
def write_package(sha):
with zipfile.ZipFile(package, 'w') as archive:
archive.writestr('AppxManifest.xml', manifest)
archive.writestr('app/resources/install-stamp.json', json.dumps({'tag': tag, 'commit': sha}))
write_package(commit)
out = root / 'metadata-windows-x64.json'
original = package.read_bytes()
record('windows', 'x64', root, tag, commit, out)
metadata = json.loads(out.read_text(encoding='utf-8'))
assert metadata['identity'] == 'Product'
assert metadata['version'] == '1.2.3.0'
assert metadata['publisher'] == 'CN=Test'
assert metadata['applicationId'] == 'App'
assert package.read_bytes() == original
write_package('b' * 40)
with pytest.raises(ValueError, match='provenance'):
record('windows', 'x64', root, tag, commit, tmp_path / 'bad.json')
with pytest.raises(ValueError, match='provenance'):
stamp_matches({}, tag, commit)
def test_assemble_uses_staged_receipts_and_only_publishes_the_manifest(tmp_path, monkeypatch, r2_server):
from scripts.bundles.release_artifacts import assemble
from scripts.releases import handoff
tag, commit, base = 'v1.2.3', 'a' * 40, 'https://releases.example'
built = tmp_path / 'built'
built.mkdir()
for platform, arches in [('windows', ('x64', 'arm64')), ('macos', ('x64', 'arm64')), ('termux', ('aarch64',))]:
for arch in arches:
row = {'platform': platform, 'arch': arch, 'tag': tag, 'commit': commit, 'identity': 'Product'}
if platform == 'windows':
row.update(version='1.2.3.0', publisher='CN=Test', applicationId='App')
package = f'Product-win-{arch}.msix'
handoff_name = f'win32-{arch}'
elif platform == 'macos':
package = f'Product-mac-{arch}.zip'
row.update(version='1.2.3', teamId='ABCDEFGHIJ', filename=package)
handoff_name = f'darwin-{arch}'
else:
package = 'deb/product.deb'
row.update(version='1.2.3-1', filename=package)
handoff_name = 'termux'
file = built / package
file.parent.mkdir(parents=True, exist_ok=True)
file.write_bytes(b'package transport fixture')
metadata = built / f'metadata-{platform}-{arch}.json'
metadata.write_text(json.dumps(row), encoding='utf-8')
handoff.stage(tag, commit, handoff_name, built, [package, metadata.name])
bundle = built / 'Product-win.msixbundle'
with zipfile.ZipFile(bundle, 'w') as archive:
archive.writestr('AppxMetadata/AppxBundleManifest.xml', '')
(built / 'Store-Product-win.msixbundle').write_bytes(b'Store bundle transport fixture')
handoff.stage(tag, commit, 'windows-universal', built, ['*.msixbundle'])
fetched = tmp_path / 'fetched'
names = ['win32-x64', 'win32-arm64', 'darwin-x64', 'darwin-arm64', 'termux', 'windows-universal']
handoff.fetch(tag, commit, names, fetched, ['metadata-*.json', '*.msixbundle'])
r2_server.requests.clear()
manifest = assemble(fetched, tag, commit, base, tmp_path / 'release-candidates.json')
assert {row['platform'] + '/' + row['arch'] for row in manifest['packages']} == {
'windows/x64', 'windows/arm64', 'macos/x64', 'macos/arm64', 'termux/aarch64'}
assert all(not file['path'].startswith(('handoff-', 'metadata-')) for file in manifest['files'])
puts = [path for method, path, _ in r2_server.requests if method == 'PUT']
assert puts == [f'/hermes-releases/releases/tag/{tag}/release-candidates.json']
assert all(key.startswith(f'releases/tag/{tag}/') for key in r2_server.store)
receipt = fetched / 'handoff-darwin-arm64.json'
original = receipt.read_bytes()
receipt.unlink()
with pytest.raises(ValueError, match='handoff'):
assemble(fetched, tag, commit, base, tmp_path / 'missing.json')
receipt.write_bytes(original)
(fetched / 'metadata-windows-x64.json').write_text('{}', encoding='utf-8')
with pytest.raises(ValueError, match='digest'):
assemble(fetched, tag, commit, base, tmp_path / 'changed.json')
def test_materialize_validates_the_published_file_receipt_before_using_bytes(tmp_path, monkeypatch):
base, tag, commit = 'https://releases.example', 'v1.2.3', 'a' * 40
data = b'package transport fixture, not native signing proof'
digest = hashlib.sha256(data).hexdigest()
files, packages = [], []
for platform in ('windows', 'macos'):
for arch in ('x64', 'arm64'):
filename = f'{platform}-{arch}.' + ('msixbundle' if platform == 'windows' else 'zip')
url = f'{base}/releases/tag/{tag}/{filename}'
files.append({'path': filename, 'url': url, 'sha256': digest})
packages.append({'platform': platform, 'arch': arch, 'identity': 'Product', 'tag': tag, 'commit': commit,
'version': '1.2.3.0' if platform == 'windows' else '1.2.3',
**({'publisher': 'CN=Test', 'applicationId': 'App'} if platform == 'windows' else {'teamId': 'ABCDEFGHIJ'}),
'artifact': {'url': url, 'sha256': digest}})
manifest = {'schema': 1, 'tag': tag, 'commit': commit, 'packages': packages, 'files': files}
class Response(io.BytesIO):
def geturl(self):
return base + "/package"
monkeypatch.setattr('urllib.request.urlopen', lambda *a, **kw: Response(data))
materialize(manifest, tmp_path / 'good', public_base=base)
assert all((tmp_path / 'good' / f['path']).read_bytes() == data for f in files)
with pytest.raises(ValueError, match='one Store candidate'):
materialize(manifest, tmp_path / 'store-missing', public_base=base, store_only=True)
store = {'path': 'Store-App.msixbundle', 'url': f'{base}/releases/tag/{tag}/Store-App.msixbundle', 'sha256': digest}
files.append(store)
materialize(manifest, tmp_path / 'store', public_base=base, store_only=True)
assert [p.name for p in (tmp_path / 'store').iterdir()] == [store['path']]
from scripts.bundles import release_artifacts
from scripts.releases import stable
raw_manifest = json.dumps(manifest).encode()
seen = []
def read_remote(url, expected_hash, *, expected_origin):
seen.append((url, expected_hash, expected_origin))
return stable.read_manifest(url, expected_hash, expected_origin=expected_origin,
opener=lambda *args, **kwargs: Response(raw_manifest))
monkeypatch.setattr(release_artifacts, 'read_manifest', read_remote)
monkeypatch.setenv('CANDIDATE_MANIFEST_SHA256', hashlib.sha256(raw_manifest).hexdigest())
release_artifacts.main(['materialize', '--tag', tag, '--commit', commit, '--public-base', base,
'--root', str(tmp_path / 'remote-store'), '--store-only'])
assert (tmp_path / 'remote-store' / store['path']).read_bytes() == data
assert seen[0][0] == f'{base}/releases/tag/{tag}/release-candidates.json'
assert seen[0][2] == base
monkeypatch.setenv('CANDIDATE_MANIFEST_SHA256', 'f' * 64)
with pytest.raises(ValueError, match='digest mismatch'):
release_artifacts.main(['materialize', '--tag', tag, '--commit', commit, '--public-base', base,
'--root', str(tmp_path / 'wrong-manifest'), '--store-only'])
assert not (tmp_path / 'wrong-manifest').exists()
files[0]['sha256'] = 'b' * 64
with pytest.raises(ValueError, match='receipts differ'):
materialize(manifest, tmp_path / 'bad', public_base=base)
files[0]['sha256'] = digest
monkeypatch.setattr('urllib.request.urlopen', lambda *a, **kw: Response(b'changed bytes'))
with pytest.raises(ValueError, match='digest mismatch'):
materialize(manifest, tmp_path / 'corrupt', public_base=base)