chore: keep the launchers off the release

check_release reports a launcher in the checksum list as unrecorded and
one attached as extra, as it does for install.py. The release steps no
longer run build_port, and the checksum and upload globs name the packs
(*_BIOS_Pack*.zip) so a launcher left in dist/ is never caught.
This commit is contained in:
Abdessamad Derraz committed 2026-11-10 11:50:39 +01:00
1 parent d7bd89d42b
commit 962f18ac3f
6 files changed
+63 -43

No files matched your search

+6 -9
View File
@@ -3,13 +3,13 @@
Run on the draft before publishing it, and on the published release at any
time. Every pack asset release.json names must be on the release at the sha256
SHA256SUMS.txt lists for it, as must each handheld launcher (RetroBIOS.zip
and the others build_port.py writes) the list names, the parts of a pack must add up to the download
SHA256SUMS.txt lists for it, the parts of a pack must add up to the download
size the README prints, and nothing else may be attached. The record holds
one pack for each platform the registry marks release_pack, and no other
pack. The installer and the file lists are not release files: a package pins
them from the tag, so a reader of the release page sees the packs and nothing
to puzzle over. An upload that
pack. The installer, the file lists and the handheld launchers are not
release files: a package pins the first two from the tag, and the site
serves the launchers built from its own commit (sitehandheld.py), so a
reader of the release page sees the packs and nothing to puzzle over. An upload that
failed in the loop, or a part re-uploaded from an older build, otherwise
publishes a release that `sha256sum --check` rejects for every user.
@@ -31,7 +31,6 @@ from dataclasses import dataclass
from pathlib import Path
sys.path.insert(0, os.path.dirname(__file__))
from build_port import ARCHIVES
from check_release_assets import digests_of
from common import list_release_pack_platforms, require_directory
from largefiles import LARGE_FILES_REPO
@@ -112,9 +111,7 @@ def compare_release(
findings.append(Finding("extra", name, "attached but neither recorded nor listed"))
for name in sorted(set(recorded) - set(sums)):
findings.append(Finding("unlisted", name, f"absent from {SUMS}"))
# The handheld launchers are release files but no packs: release.json
# records packs, and the checksum list alone vouches for the launchers.
for name in sorted(set(sums) - set(recorded) - set(ARCHIVES)):
for name in sorted(set(sums) - set(recorded)):
findings.append(Finding("unrecorded", name, f"listed in {SUMS}, absent from release.json"))
wanted = {**sums, **(local_files or {})}
+16 -14
View File
@@ -15,6 +15,7 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
import build_port # noqa: E402
from check_release import SIGNATURE, SUMS, Asset, compare_release, parse_sums
A = "a" * 64
@@ -97,20 +98,21 @@ class ReleaseMatchesWhatWasBuilt(unittest.TestCase):
self.assertIn(("unrecorded", "Other_BIOS_Pack.zip"), kinds)
self.assertIn(("missing", "Other_BIOS_Pack.zip"), kinds)
def test_the_handheld_port_is_vouched_for_by_the_checksum_list(self):
sums = {**SUMS_LIST, "RetroBIOS.zip": A}
assets = _assets()
self.assertEqual(
_kinds(compare_release(RECORD, sums, assets, "v2026.10.10")),
{("missing", "RetroBIOS.zip")},
)
assets["RetroBIOS.zip"] = Asset(70, A)
self.assertEqual(compare_release(RECORD, sums, assets, "v2026.10.10"), [])
assets["RetroBIOS.zip"] = Asset(70, B)
self.assertEqual(
_kinds(compare_release(RECORD, sums, assets, "v2026.10.10")),
{("content", "RetroBIOS.zip")},
)
def test_a_handheld_launcher_is_no_release_file(self):
"""The site serves the launchers built from its own commit: one in
the checksum list or among the assets is refused."""
for name in build_port.ARCHIVES:
with self.subTest(name=name):
sums = {**SUMS_LIST, name: A}
assets = _assets()
assets[name] = Asset(70, A)
self.assertEqual(_kinds(compare_release(RECORD, sums, assets, "v2026.10.10")),
{("unrecorded", name)})
assets = _assets()
assets[name] = Asset(70, A)
self.assertEqual(
_kinds(compare_release(RECORD, SUMS_LIST, assets, "v2026.10.10")),
{("extra", name)})
def test_parts_must_add_up_to_the_printed_download_size(self):
assets = _assets()
+8 -2
View File
@@ -374,10 +374,16 @@ class TheArchives(unittest.TestCase):
with self.subTest(archive=name), self.assertRaises(ValueError):
build_port.build_archive(name, self.out / name, root)
def test_the_release_process_attaches_each(self):
def test_the_site_serves_each_and_the_release_none(self):
"""Each archive carries install.py: the site serves it from its own
commit (sitehandheld.py), and the release steps never build one."""
text = (REPO_ROOT / "wiki" / "release-process.md").read_text(encoding="utf-8")
self.assertNotIn("scripts/build_port.py", text)
installer = (REPO_ROOT / "wiki" / "installer.md").read_text(encoding="utf-8")
for name in build_port.ARCHIVES:
self.assertIn(name, text)
with self.subTest(archive=name):
self.assertNotIn(name, text)
self.assertIn(f"](../get/handheld/{name})", installer)
FAKE_INSTALL = r'''
+21 -1
View File
@@ -33,6 +33,7 @@ from __future__ import annotations
import tests # noqa: F401 offline, scratch under tmp/, the suite's own cache
import fnmatch
import json
import re
import sys
@@ -48,6 +49,7 @@ sys.path.insert(0, str(REPO_ROOT))
import sitediagrams # noqa: E402
from siteguides import INSTALL_SH # noqa: E402
from tests import sitepages # noqa: E402
import build_port # noqa: E402
from check_release import SUMS, Asset, compare_release # noqa: E402
@@ -870,15 +872,33 @@ class PinnedInstaller(unittest.TestCase):
kinds = {(f.kind, f.name) for f in compare_release(record, sums, assets, "v1")}
self.assertEqual(kinds, {("unrecorded", "install.py"), ("unrecorded", "retroarch.json")})
def test_a_launcher_attached_to_the_release_is_refused(self):
"""The handheld launchers carry install.py: the site serves them
from its own commit, the release never does."""
record = {"tag": "v1", "packs": {"P.zip": {"download_size": 5, "assets": ["P.zip"]}}}
sums = _sums_with(["P.zip", *build_port.ARCHIVES])
assets = {name: Asset(5, digest) for name, digest in sums.items()}
assets[SUMS] = Asset(1, None)
kinds = {(f.kind, f.name) for f in compare_release(record, sums, assets, "v1")}
self.assertEqual(kinds, {("unrecorded", name) for name in build_port.ARCHIVES})
def test_the_release_steps_ship_none(self):
text = _wiki("release-process.md")
self.assertNotIn("cp install.py", text)
sums = re.search(r"sha256sum [^>]+> SHA256SUMS.txt", text, re.S).group(0)
upload = re.search(r"for f in dist/SHA256SUMS.txt.*?; do", text, re.S).group(0)
for name in ("install.py", "install.sh", "install.ps1", ".json"):
for name in ("install.py", "install.sh", "install.ps1", ".json", "RetroBIOS",
".muxapp", "build_port", " *.zip", "/*.zip"):
with self.subTest(name=name):
self.assertNotIn(name, sums)
self.assertNotIn(name, upload)
# A launcher built into dist/ is never caught by the glob either.
for archive in build_port.ARCHIVES:
with self.subTest(archive=archive):
self.assertFalse(fnmatch.fnmatch(archive, "*_BIOS_Pack*.zip"))
self.assertIn("*_BIOS_Pack*.zip", sums)
self.assertIn("*_BIOS_Pack*.zip", upload)
self.assertNotIn("scripts/build_port.py", text)
def test_the_installer_page_pins_one_commit(self):
# v2026.10.10 carries no install.py: until a release does, the
+1 -1
View File
@@ -278,7 +278,7 @@ class ReleaseProcessPublishesArchives(unittest.TestCase):
def test_the_checksums_are_those_of_the_published_files(self):
self.assertLess(
self.text.index("scripts/split_pack.py"),
self.text.index("sha256sum *.zip"),
self.text.index("sha256sum *_BIOS_Pack*.zip"),
)
+11 -16
View File
@@ -248,25 +248,18 @@ python scripts/pipeline.py
# which install.py --check reads to name a part missing from a folder.
python scripts/split_pack.py dist/
# 2b. The handheld launchers, each carrying the installer the bootstraps pin:
# RetroBIOS.zip (the PortMaster port), RetroBIOS-batocera.zip (Batocera
# and Knulli Ports), RetroBIOS.muxapp (muOS Applications) and
# RetroBIOS-tools.zip (the Tools menu of ArkOS, dArkOS, ROCKNIX, JELOS,
# AmberELEC). They are release files, listed in the checksums below, and
# check_release.py compares each asset with its line there.
python scripts/build_port.py
# 2c. The release carries the packs, the handheld launchers and the signed
# checksum list, and nothing else: a beginner reads that page to pick one
# file. A package pins install.py and its file list from the tag
# (installer page, "For packagers"), and check_release.py refuses an
# installer or a file list attached to the release.
# 2b. The release carries the packs and the signed checksum list, and
# nothing else: a beginner reads that page to pick one file. A package
# pins install.py and its file list from the tag (installer page, "For
# packagers"). The handheld launchers carry install.py too, so the site
# builds and serves them from its own commit. check_release.py refuses an
# installer, a file list or a launcher attached to the release.
# 3. Checksums of the files as published, then sign the list. The checksums
# answer corruption, and the signature answers a rewritten release, which is
# the one thing a checksum published beside its own artifacts cannot
# answer.
(cd dist && sha256sum *.zip RetroBIOS.muxapp > SHA256SUMS.txt)
(cd dist && sha256sum *_BIOS_Pack*.zip > SHA256SUMS.txt)
ssh-keygen -Y sign -f ~/.ssh/retrobios_signing -n file dist/SHA256SUMS.txt
# 4. Record what each pack holds, read from the archives just checked:
@@ -284,14 +277,16 @@ python scripts/generate_site.py
# maintainer's own text in tmp/whats-new.md, appended as written. The
# script refuses a text that judges another project ("forum list") or
# promises what the gap analysis contradicts ("never stops"): it says
# what changed and how it was checked.
# what changed and how it was checked. The notes end with everyone who
# helped since the start, read with gh (--thanks FILE passes a checked
# list), and the full changelog from the previous release's record.
python scripts/release_notes.py --whats-new tmp/whats-new.md --output tmp/notes.md
# Create the release as a DRAFT, upload every asset, and only then publish it.
# A public release with half its assets is a broken download for everyone
# during the whole upload.
gh release create "v${DATE}" --draft --title "BIOS Pack v${DATE}" --notes-file tmp/notes.md
for f in dist/SHA256SUMS.txt dist/SHA256SUMS.txt.sig dist/*.zip dist/RetroBIOS.muxapp; do
for f in dist/SHA256SUMS.txt dist/SHA256SUMS.txt.sig dist/*_BIOS_Pack*.zip; do
[ -f "$f" ] && gh release upload "v${DATE}" "$f#$(basename "$f")" --clobber
done
# A count does not see a failed upload beside a stray file, nor a part