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:
Abdessamad Derraz committed 2026-10-20 04:28:27 +02:00
1 parent d43aa5354c
commit 282f8d5d0a
7 files changed
+102

No files matched your search

+10
View File
@@ -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."
+10
View File
@@ -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
+31
View File
@@ -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:
+20
View File
@@ -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="")
+15
View File
@@ -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()
+15
View File
@@ -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()
+1
View File
@@ -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 |