mirror of
https://github.com/Abdess/retroarch_system.git
synced 2026-10-20 18:59:07 -05:00
feat: watch the release page of off-forge profiles
About forty profiles describe projects published outside any forge (Altirra, ePSXe, DraStic, mednafen, xroar). profile_sync skipped them as an unsupported host and check_freshness reported the same UNKNOWN line every run, so a new release adding a firmware went unseen. A profile can now name the page announcing its releases and a pattern for the version: --check-version reads it, and check_freshness --profiles calls the profile stale when the page announces another version than core_version.
This commit is contained in:
1 parent
d43aa5354c
commit
282f8d5d0a
7 files changed
+102
No files matched your search
@@ -809,6 +809,16 @@
|
||||
"type": "string",
|
||||
"description": "Why source_commit is held on purpose rather than followed. The profile is then judged against its own revision, like one pinned to a superseded tag: comparing it to HEAD says nothing and recaling would repoint its refs at code the build it documents never had."
|
||||
},
|
||||
"version_source": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": ["url", "pattern"],
|
||||
"properties": {
|
||||
"url": {"type": "string", "format": "uri"},
|
||||
"pattern": {"type": "string", "description": "Regular expression whose first group is the latest version the page announces."}
|
||||
},
|
||||
"description": "For a project published outside any forge, the page that announces its releases. profile_sync --check-version compares the version read there with core_version."
|
||||
},
|
||||
"upstream_gone": {
|
||||
"type": "string",
|
||||
"description": "Why the declared upstream can no longer be reached, when that is settled rather than transient. Records the death instead of leaving the profile failing every pass. profile_sync reports the contradiction if the forge answers again."
|
||||
|
||||
@@ -1097,6 +1097,16 @@ def summarize_profile_sync(report: object) -> list[Finding]:
|
||||
if not isinstance(row, dict):
|
||||
continue
|
||||
name = str(row.get("name") or "?")
|
||||
if row.get("skipped") and row.get("page_version"):
|
||||
# Off any forge, the release page is the feed: a version there
|
||||
# beyond the declared one is a release the profile does not cover.
|
||||
declared, announced = str(row.get("declared_version") or ""), str(row["page_version"])
|
||||
findings.append(Finding(
|
||||
"profiles", name, OK if announced == declared else STALE,
|
||||
local=declared, remote=announced,
|
||||
detail="" if announced == declared else "a newer release is announced",
|
||||
))
|
||||
continue
|
||||
if row.get("skipped"):
|
||||
findings.append(Finding("profiles", name, UNKNOWN, detail=str(row["skipped"])))
|
||||
continue
|
||||
|
||||
@@ -1114,6 +1114,10 @@ class ProfileReport:
|
||||
# Repositories whose pin is no ancestor of the branch the profile
|
||||
# tracks: history was rewritten, or the pin lies on another branch.
|
||||
orphaned: list[str] = field(default_factory=list)
|
||||
# A profile off any forge names the page announcing its releases
|
||||
# (version_source): the version read there, beside the one declared.
|
||||
declared_version: str | None = None
|
||||
page_version: str | None = None
|
||||
|
||||
# Skips that say the profile cannot be checked as written: a repository
|
||||
# that no longer answers, no pin to read, a death the forge contradicts.
|
||||
@@ -2032,6 +2036,11 @@ def format_report(report: ProfileReport, changed_only: bool = False) -> str:
|
||||
lines.append(f" not read (unsupported host): {url}")
|
||||
for line in report.orphaned:
|
||||
lines.append(f" orphaned pin: {line}")
|
||||
if report.declared_version or report.page_version:
|
||||
lines.append(
|
||||
f" version page: declared {report.declared_version or '-'}, "
|
||||
f"announced {report.page_version or 'unreadable'}"
|
||||
)
|
||||
if report.skipped:
|
||||
lines.append(f" skipped: {report.skipped}")
|
||||
return "\n".join(lines)
|
||||
@@ -2137,6 +2146,25 @@ def check_version(
|
||||
)
|
||||
|
||||
|
||||
def page_version(profile: dict, cache_dir: str, offline: bool) -> str | None:
|
||||
"""The latest version a profile's version_source page announces.
|
||||
|
||||
Forty-odd profiles live off any forge: profile_sync could only say
|
||||
"unsupported host", and a new Altirra or mednafen release went unseen.
|
||||
"""
|
||||
source = profile.get("version_source") or {}
|
||||
url, pattern = str(source.get("url") or ""), str(source.get("pattern") or "")
|
||||
if not url or not pattern:
|
||||
return None
|
||||
try:
|
||||
text = upstream.fetch_page(url, cache_dir, offline)
|
||||
except upstream.UpstreamError as exc:
|
||||
print(f"{profile.get('emulator', '?')}: {url}: {exc}", file=sys.stderr)
|
||||
return None
|
||||
match = re.search(pattern, text or "")
|
||||
return match.group(1) if match else None
|
||||
|
||||
|
||||
def version_warning(report: VersionReport | None, pin: str | None) -> str | None:
|
||||
"""Flag a declared version that names a tag away from the resolved pin."""
|
||||
if report is None or not report.tag_commit or not pin:
|
||||
@@ -3810,6 +3838,9 @@ def main() -> None:
|
||||
name=name, entries=[], counts={}, skipped=f"upstream error: {exc}"
|
||||
)
|
||||
failed.add(name)
|
||||
if args.check_version and profile.get("version_source"):
|
||||
report.declared_version = str(profile.get("core_version") or "") or None
|
||||
report.page_version = page_version(profile, args.cache_dir, args.offline)
|
||||
reports.append(report)
|
||||
if writes:
|
||||
try:
|
||||
|
||||
@@ -400,6 +400,26 @@ def _api(
|
||||
return payload
|
||||
|
||||
|
||||
def fetch_page(url: str, cache_dir: str, offline: bool) -> str | None:
|
||||
"""A web page's text, cached like a moving API answer (MOVING_TTL).
|
||||
|
||||
A project published outside a forge (Altirra, mednafen, ePSXe) says its
|
||||
version on a download page: that page is the only release feed there is.
|
||||
"""
|
||||
key = hashlib.sha256(url.encode()).hexdigest()
|
||||
target = Path(cache_dir) / "_pages" / f"{key}.txt"
|
||||
if target.is_file():
|
||||
fresh = time.time() - target.stat().st_mtime <= MOVING_TTL
|
||||
if fresh or offline:
|
||||
raw = target.read_text(encoding="utf-8")
|
||||
return None if raw == ABSENT else raw
|
||||
if offline:
|
||||
return None
|
||||
text = _http_text(url)
|
||||
write_cache(target, ABSENT if text is None else text)
|
||||
return text
|
||||
|
||||
|
||||
def _project(repo: Repo) -> str:
|
||||
return urllib.parse.quote(f"{repo.owner}/{repo.name}", safe="")
|
||||
|
||||
|
||||
@@ -422,5 +422,20 @@ class RowsSayAllTheyStandFor(unittest.TestCase):
|
||||
self.assertIn("--json lists all 50", row.detail)
|
||||
|
||||
|
||||
class AReleasePageMakesAProfileStale(unittest.TestCase):
|
||||
def test_a_newer_announced_version_is_stale(self):
|
||||
import check_freshness
|
||||
|
||||
rows = [
|
||||
{"name": "altirra", "skipped": "unsupported host: x",
|
||||
"declared_version": "4.40", "page_version": "4.50"},
|
||||
{"name": "mednafen", "skipped": "unsupported host: y",
|
||||
"declared_version": "1.32.1", "page_version": "1.32.1"},
|
||||
]
|
||||
found = {f.subject: f.status for f in check_freshness.summarize_profile_sync(rows)}
|
||||
self.assertEqual(found["altirra"], check_freshness.STALE)
|
||||
self.assertEqual(found["mednafen"], check_freshness.OK)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -4253,5 +4253,20 @@ class StrictRefusesAnUncheckableProfile(unittest.TestCase):
|
||||
self.assertFalse(report("no source_ref").uncheckable())
|
||||
|
||||
|
||||
class AnOffForgeProfileWatchesItsReleasePage(unittest.TestCase):
|
||||
"""Profiles of projects outside any forge were skipped as an unsupported
|
||||
host, and a new release of Altirra or mednafen went unseen."""
|
||||
|
||||
def test_the_page_version_is_read(self):
|
||||
from unittest import mock
|
||||
|
||||
profile = {"emulator": "Altirra", "core_version": "4.40", "version_source": {
|
||||
"url": "https://example.test/altirra", "pattern": r"Altirra (\d+\.\d+) released",
|
||||
}}
|
||||
with mock.patch.object(profile_sync.upstream, "fetch_page",
|
||||
return_value="<p>Altirra 4.50 released</p>"):
|
||||
self.assertEqual(profile_sync.page_version(profile, "cache", False), "4.50")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -384,6 +384,7 @@ partitions), and `game_count`.
|
||||
| `adler32_pairs_with`, `adler32_pairs` | the file hashed together with this one and the combinations the code accepts, this file's value first, in table order: Dolphin warns on a DSP ROM pair outside its `known_roms` even when each file passes alone |
|
||||
| `pin_frozen` | why `source_commit` is held on purpose rather than followed. The profile is then judged against its own revision, like one pinned to a superseded tag |
|
||||
| `prose_written_at` | the pin a reader confirmed the prose citations against. When the profile's history shows a text living under several pins, profile_sync takes this one as the revision the text describes, so a bump is no longer refused for a citation already reread. It never overrides a history with a single candidate |
|
||||
| `version_source` | for a project published outside any forge, `{url, pattern}`: the page that announces releases and a regular expression whose first group is the latest version. `profile_sync --check-version` compares it with `core_version`, and `check_freshness --profiles` calls the profile stale when they differ |
|
||||
| `upstream_gone` | why the declared upstream can no longer be reached, when that is settled rather than transient. Records the death instead of leaving the profile failing every pass. profile_sync reports it as declared, and reports the contradiction if the forge answers again |
|
||||
| `source_mirror` | a repository carrying the same tree, consulted after `source` and `upstream`. What keeps a profile checkable once its own forge stops answering |
|
||||
| `validation` | checks the code performs: `size`, `crc32`, `md5`, `sha1`, `adler32`, `signature`, `crypto`. Can be a list or dict `{core: [...], upstream: [...]}` for divergent checks |
|
||||
|
||||
Reference in new issue
Block a user