mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-20 10:23:16 -05:00
feat: write the release notes from the record
The pack table, its parts sentence and the install commands were typed by hand from figures release.json already held. A copied cell or an old collection total at the top is how issue 81 happened. release_notes.py renders them and appends the hand-written account of what changed.
This commit is contained in:
1 parent
8bd2d319b2
commit
810396941d
4 files changed
+190
-5
No files matched your search
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Write the notes of a release from its record.
|
||||
|
||||
The notes carried the pack table, typed by hand from figures release.json
|
||||
already held: a copied cell or an old collection total at the top is how
|
||||
issue 81 happened. Install commands, the parts sentence and the table come
|
||||
from the record and the registry. The maintainer writes what changed, and
|
||||
that text is appended as given.
|
||||
|
||||
Usage:
|
||||
python scripts/release_notes.py --whats-new tmp/whats-new.md --output notes.md
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import release_record
|
||||
from common import load_platform_registry, write_text_atomic
|
||||
|
||||
REPO = "Abdess/retrobios"
|
||||
RAW = f"https://raw.githubusercontent.com/{REPO}/main"
|
||||
|
||||
|
||||
def _size(count: int) -> str:
|
||||
if count >= 1024**3:
|
||||
return f"{count / 1024**3:.1f} GB"
|
||||
return f"{count / 1024**2:.0f} MB"
|
||||
|
||||
|
||||
def _display(pack: str) -> str:
|
||||
"""RetroArch_Lakka_v1.22.2_BIOS_Pack.zip reads as RetroArch Lakka v1.22.2."""
|
||||
return pack.split("_BIOS_Pack", 1)[0].replace("_", " ")
|
||||
|
||||
|
||||
def _extract_folders(record: dict, platforms_dir: str) -> dict[str, str]:
|
||||
"""Pack name to the folder the registry says it extracts into."""
|
||||
folders: dict[str, str] = {}
|
||||
for key, entry in sorted(load_platform_registry(platforms_dir).items()):
|
||||
if not entry.get("extract_to"):
|
||||
continue
|
||||
needle = release_record._match_key(key)
|
||||
for name in sorted(record.get("packs", {})):
|
||||
# The name a pack starts with: RetroArch_Lakka is RetroArch's.
|
||||
if needle and release_record._match_key(name).startswith(needle):
|
||||
folders.setdefault(name, entry["extract_to"])
|
||||
return folders
|
||||
|
||||
|
||||
def _links(tag: str, assets: list[str]) -> str:
|
||||
base = f"https://github.com/{REPO}/releases/download/{tag}"
|
||||
if len(assets) == 1:
|
||||
return f"[{assets[0]}]({base}/{assets[0]})"
|
||||
return " + ".join(f"[part {n}]({base}/{asset})" for n, asset in enumerate(assets, 1))
|
||||
|
||||
|
||||
def render(record: dict, platforms_dir: str, whats_new: str) -> str:
|
||||
tag = record["tag"]
|
||||
packs = record.get("packs", {})
|
||||
folders = _extract_folders(record, platforms_dir)
|
||||
lines = [
|
||||
"### Install in one command",
|
||||
"",
|
||||
("The installer detects the platform and its BIOS folder, downloads only"
|
||||
" what is missing and checks every file. Running it again is safe."),
|
||||
"",
|
||||
"**Linux, macOS, Steam Deck**",
|
||||
"```sh",
|
||||
f"curl -fsSL {RAW}/install.sh | sh",
|
||||
"```",
|
||||
"",
|
||||
"**Windows (PowerShell)**",
|
||||
"```powershell",
|
||||
f"irm {RAW}/install.ps1 | iex",
|
||||
"```",
|
||||
"",
|
||||
"### Download a pack",
|
||||
"",
|
||||
("One pack per platform, the complete one: the BIOS list the platform"
|
||||
" publishes plus every file its emulators open. Files is what the pack"
|
||||
" holds once extracted, the count a file manager shows. Extracted size"
|
||||
" is the space those files take, and the download is smaller."),
|
||||
"",
|
||||
]
|
||||
if any(len(pack["assets"]) > 1 for pack in packs.values()):
|
||||
lines += [
|
||||
("A pack over 2 GB comes as parts that are each an ordinary ZIP of whole"
|
||||
" files (`Pack.part1of2.zip`, `Pack.part2of2.zip`). Download every part"
|
||||
" and extract them all into the same folder. Nothing needs joining."),
|
||||
"",
|
||||
]
|
||||
lines += [
|
||||
"| Platform | Download | Extracted size | Files | Extract to |",
|
||||
"|----------|----------|---------------:|------:|-----------|",
|
||||
]
|
||||
for name, pack in sorted(packs.items(), key=lambda item: -item[1]["extracted_size"]):
|
||||
folder = folders.get(name)
|
||||
lines.append(
|
||||
f"| {_display(name)} | {_links(tag, pack['assets'])} |"
|
||||
f" {_size(pack['extracted_size'])} | {pack['files']:,} |"
|
||||
f" {f'`{folder}`' if folder else '-'} |"
|
||||
)
|
||||
lines += ["", whats_new.strip(), ""]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(allow_abbrev=False, description="Write a release's notes")
|
||||
parser.add_argument("--record", default=release_record.RECORD, type=Path,
|
||||
help="release record (default: %(default)s)")
|
||||
parser.add_argument("--whats-new", required=True, type=Path,
|
||||
help="what changed since the previous release, written by hand")
|
||||
parser.add_argument("--platforms-dir", default="platforms")
|
||||
parser.add_argument("--output", required=True, type=Path)
|
||||
args = parser.parse_args()
|
||||
|
||||
record = release_record.load_record(args.record)
|
||||
if not record.get("packs"):
|
||||
print(f"Error: {args.record} records no pack", file=sys.stderr)
|
||||
return 1
|
||||
whats_new = args.whats_new.read_text(encoding="utf-8")
|
||||
if not whats_new.strip():
|
||||
print(f"Error: {args.whats_new} is empty", file=sys.stderr)
|
||||
return 1
|
||||
write_text_atomic(str(args.output), render(record, args.platforms_dir, whats_new))
|
||||
print(f"Wrote {args.output} for {record['tag']}, {len(record['packs'])} packs")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from common import run_cli
|
||||
|
||||
run_cli(main)
|
||||
@@ -35,6 +35,7 @@ import generate_db # noqa: E402
|
||||
import generate_pack as builder # noqa: E402
|
||||
import generate_readme # noqa: E402
|
||||
import generate_site # noqa: E402
|
||||
import release_notes # noqa: E402
|
||||
import release_record # noqa: E402
|
||||
import split_pack # noqa: E402
|
||||
|
||||
@@ -474,5 +475,47 @@ class RefusedFilesAreNotPresent(unittest.TestCase):
|
||||
self.assertEqual((coverage["present"], coverage["total"]), (1, 2))
|
||||
|
||||
|
||||
class TheReleaseNotesReadTheRecord(unittest.TestCase):
|
||||
"""The notes' pack table was typed by hand from release.json: a copied
|
||||
cell or an old collection total at the top reproduced issue 81."""
|
||||
|
||||
RECORD = {"tag": "v2026.10.10", "packs": {
|
||||
"RetroArch_Lakka_v1.22.2_BIOS_Pack.zip": {
|
||||
"files": 8504, "extracted_size": 6_400_000_000, "download_size": 4_000_000_000,
|
||||
"assets": ["RetroArch_Lakka_v1.22.2_BIOS_Pack.part1of2.zip",
|
||||
"RetroArch_Lakka_v1.22.2_BIOS_Pack.part2of2.zip"]},
|
||||
"EmuDeck_2.7EA_BIOS_Pack.zip": {
|
||||
"files": 620, "extracted_size": 3_500_000_000, "download_size": 3_000_000_000,
|
||||
"assets": ["EmuDeck_2.7EA_BIOS_Pack.zip"]},
|
||||
}}
|
||||
|
||||
def render(self, record=None):
|
||||
return release_notes.render(record or self.RECORD, str(REPO_ROOT / "platforms"),
|
||||
"### What changed\n\n- x\n")
|
||||
|
||||
def test_each_pack_row_carries_the_record(self):
|
||||
text = self.render()
|
||||
row = next(line for line in text.splitlines() if line.startswith("| RetroArch"))
|
||||
self.assertIn("8,504", row)
|
||||
self.assertIn("part1of2.zip", row)
|
||||
self.assertIn("part2of2.zip", row)
|
||||
self.assertIn("`system/`", row)
|
||||
self.assertIn("| Platform | Download | Extracted size | Files |", text)
|
||||
|
||||
def test_the_parts_sentence_sits_above_the_table_only_when_needed(self):
|
||||
text = self.render()
|
||||
self.assertLess(text.index("ordinary ZIP"), text.index("| Platform"))
|
||||
whole = {"tag": "v1", "packs": {"EmuDeck_2.7EA_BIOS_Pack.zip":
|
||||
self.RECORD["packs"]["EmuDeck_2.7EA_BIOS_Pack.zip"]}}
|
||||
self.assertNotIn("ordinary ZIP", self.render(whole))
|
||||
|
||||
def test_the_notes_never_open_on_the_collection(self):
|
||||
self.assertNotIn("collection", self.render().split("### What changed")[0])
|
||||
|
||||
def test_the_release_process_runs_it(self):
|
||||
page = (REPO_ROOT / "wiki" / "release-process.md").read_text(encoding="utf-8")
|
||||
self.assertIn("scripts/release_notes.py", page)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+11
-5
@@ -218,10 +218,15 @@ python scripts/release_record.py dist/ --tag "v${DATE}"
|
||||
python scripts/generate_readme.py --db database.json --platforms-dir platforms
|
||||
python scripts/generate_site.py
|
||||
|
||||
# 6. Create the release as a DRAFT, upload every asset, and only then publish it.
|
||||
# 6. Write the notes: install commands, the parts sentence and the pack table
|
||||
# come from release.json, and what changed since the previous tag is the
|
||||
# maintainer's own text in tmp/whats-new.md, appended as written.
|
||||
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 notes.md
|
||||
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; do
|
||||
[ -f "$f" ] && gh release upload "v${DATE}" "$f#$(basename "$f")" --clobber
|
||||
done
|
||||
@@ -292,9 +297,10 @@ and keeping the retired line, so signatures on past releases keep verifying.
|
||||
One pack per platform, the full one: the platform's list plus everything its
|
||||
cores load. Platform-only and per-emulator packs are build options, not
|
||||
release assets, since a lighter pack means a core that fails with no message.
|
||||
The release notes follow the previous release: the quick install commands,
|
||||
the pack table, what changed since the previous tag, and the contributors of
|
||||
the closed issues. A pack has two sizes and they are far apart: Batocera
|
||||
The release notes are written by `release_notes.py`: the install commands,
|
||||
the pack table and its parts sentence come from `release.json`, then what
|
||||
changed since the previous tag and the contributors of the closed issues,
|
||||
written by hand. A pack has two sizes and they are far apart: Batocera
|
||||
downloads as 2.4 GB and extracts to 4.0 GB. Step 5 prints both, and whichever
|
||||
one the table carries, the header names it. Someone sizing a USB drive is
|
||||
reading that column. The README table is the extracted size of the released
|
||||
|
||||
@@ -611,6 +611,7 @@ Further options, by script:
|
||||
| `provenance_report.py` | `--provenance-dir`, `--write-derivable PATH` (with `--derive`, and the pipeline writes `derivable.json`) |
|
||||
| `refresh_stale.py` | `--freshness-arg FLAG` passes a flag through to `check_freshness.py`, repeatable |
|
||||
| `release_record.py` | `--install-dir` (manifests the archives are held against), `--output` |
|
||||
| `release_notes.py` | `--whats-new FILE` (required, appended as written), `--record`, `--platforms-dir`, `--output` |
|
||||
| `restore_large_files.py` | `--cache` (downloaded assets), `--gitignore`, `--db` |
|
||||
| `romset_recipes.py` | `--dat-recipes PATH`, a snapshot or a directory of them (default `recipes/`) |
|
||||
| `split_pack.py` | `--max-size 1900M`, the ceiling per part (K, M, G, default 2G) |
|
||||
|
||||
Reference in new issue
Block a user