diff --git a/README.md b/README.md index 89a9f343..a1e988b5 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,14 @@ Hashes document what emulator code loads and accepts, not dump provenance; that 3. **Build packs** - include baseline files plus what each platform's cores need 4. **Verify** - run platform-native checks and emulator-level validation +## Contributors + +PixNyb +Takiiiiiii +Takiiiiiiii +monster-penguin +zjl88858 + ## Community tools @@ -158,4 +166,4 @@ The scripts and tooling are released under the [MIT License](LICENSE). The BIOS and firmware files are not covered by that license: they are third-party system software, preserved and provided for personal backup, archival, and interoperability with emulation software. [NOTICE](NOTICE) sets out their status and how to ask for a file to be removed. The reasoning, and where it is weakest, is in the [FAQ](https://abdess.github.io/retrobios/wiki/faq/#is-this-legal). -*Auto-generated on 2026-08-23T07:25:37Z* +*Auto-generated on 2026-08-23T07:45:09Z* diff --git a/scripts/generate_readme.py b/scripts/generate_readme.py index 818ea9e2..90f2aa34 100644 --- a/scripts/generate_readme.py +++ b/scripts/generate_readme.py @@ -126,6 +126,34 @@ RELEASE_URL = "../../releases/latest" REPO = "Abdess/retrobios" +def _existing_contributor_block(readme_path: str = "README.md") -> list[str]: + """The contributors already published, read back from the file. + + The section is the one part of this document that comes from the network. + A refused or rate-limited request used to leave the list empty, which + silently deleted the section from a published README and put the local + result permanently at odds with what the freshness check regenerates. + Keeping what is already there makes an offline run additive-only. + """ + try: + with open(readme_path, encoding="utf-8") as handle: + text = handle.read() + except OSError: + return [] + start = text.find("## Contributors") + if start == -1: + return [] + rest = text[start:].splitlines() + block = [] + for line in rest[1:]: + if line.startswith("## "): + break + block.append(line) + while block and not block[-1].strip(): + block.pop() + return ["## Contributors"] + block if block else [] + + def fetch_contributors() -> list[dict]: """Fetch contributors from GitHub API, exclude bots.""" import urllib.error @@ -518,6 +546,13 @@ def generate_readme(db: dict, platforms_dir: str) -> str: ) contributors = fetch_contributors() + if not contributors: + # The request failed. Republish what is already there rather than + # dropping the section: losing it is a worse answer than a stale list. + kept = _existing_contributor_block("README.md") + if kept: + print("contributors: request failed, keeping the published list") + lines.extend(kept + [""]) if contributors: lines.extend( [ diff --git a/tests/test_audit_regressions.py b/tests/test_audit_regressions.py index d590c713..28529226 100644 --- a/tests/test_audit_regressions.py +++ b/tests/test_audit_regressions.py @@ -957,3 +957,61 @@ class ScriptsImportThreeWays(unittest.TestCase): def test_run_as_a_script(self): result = self._run("scripts/list_platforms.py") self.assertEqual(result.returncode, 0, result.stderr[-400:]) + + +class ContributorsSurviveAFailedRequest(unittest.TestCase): + """The one part of the README that comes from the network. + + A refused or rate-limited request left the list empty, which deleted the + section from a published README. It happened during a pipeline run and + the result was committed, after which the freshness check regenerated the + section and failed on the difference. Losing the list is a worse answer + than publishing a stale one. + """ + + def _block(self, text: str): + import tempfile + + sys.path.insert(0, str(ROOT / "scripts")) + from generate_readme import _existing_contributor_block + + with tempfile.NamedTemporaryFile("w", suffix=".md", delete=False) as handle: + handle.write(text) + name = handle.name + try: + return _existing_contributor_block(name) + finally: + os.unlink(name) + + def test_a_published_list_is_read_back(self): + text = ( + "# Title\n\n## Contributors\n\n" + 'a\n' + 'b\n\n' + "## Community tools\n\nsomething else\n" + ) + block = self._block(text) + self.assertEqual(block[0], "## Contributors") + self.assertIn('alt="a"', "\n".join(block)) + self.assertIn('alt="b"', "\n".join(block)) + self.assertNotIn("Community tools", "\n".join(block)) + self.assertNotIn("something else", "\n".join(block)) + + def test_a_readme_without_the_section_yields_nothing(self): + self.assertEqual(self._block("# Title\n\n## Other\n\ntext\n"), []) + + def test_an_unreadable_file_yields_nothing(self): + sys.path.insert(0, str(ROOT / "scripts")) + from generate_readme import _existing_contributor_block + + self.assertEqual(_existing_contributor_block("/nonexistent/README.md"), []) + + def test_the_committed_readme_still_carries_its_contributors(self): + """Regenerating offline must never be the reason this disappears.""" + text = (ROOT / "README.md").read_text() + if "## Contributors" not in text: + self.skipTest("README carries no contributors section") + self.assertGreater( + text.count('