version notes: Fallback auf den Release-Feed, wenn die Instanz keinen lokalen Eintrag hat (#107)
Befund 2 aus dem getraceten 5.0.0-auf-6.0.0-Upgrade-Lauf. Eine ausgelieferte Instanz bekommt CHANGES.md als Stub und dist upgrade ueberschreibt sie nie, der Befehl konnte dort also nie antworten - an genau der Stelle, an der Breaking Change und Migration gelesen werden muessen. Fehlt der Eintrag lokal, wird der Feed aus update_url gefragt. Nur mit Release-Stamp, damit Ursprungs-Repo und CI den Pfad nicht betreten koennen; stdout traegt nur die Notes, Herkunft nach stderr; --offline verweigert den Aufruf und nennt die release_url, so wie jeder Feed-Fehlerfall auch. Dazu zwei seit ihrer Umsetzung falsche Eintraege aus tools/CONTRACT.md "Future considerations" entfernt: MCP-Server-Wrapper und dist upgrade. Files changed: - CHANGES.md - INSTALL.md - VERSION - instructions/upgrade-instance.md - tools/CONTRACT.md - tools/chemenu/commands/version_cmd.py - tools/chemenu/tests/test_version_cmd.py - tools/chemenu/version.py
This commit is contained in:
1 parent
72d01beef8
commit
0c98080964
8 files changed
+360
-52
No files matched your search
+67
-10
@@ -349,8 +349,12 @@ def fetch_latest(
|
||||
|
||||
The network call sits behind `fetcher` so every caller above this line -
|
||||
and every test - can run without a network. This is the one place in
|
||||
`wikitool` that talks to a remote host, and it is reached only from
|
||||
`version check`, never implicitly from another command.
|
||||
`wikitool` that talks to a remote host, and only two commands reach it:
|
||||
`version check`, whose whole job it is, and `version notes` on a
|
||||
*distributed* instance, whose local `CHANGES.md` is a stub with no entry to
|
||||
print (see `fetch_latest_notes`). Neither is implicit - `check` exists for
|
||||
the call, and `notes` announces the URL it is asking before it asks, on
|
||||
stderr, and takes `--offline` for a caller that wants none of it.
|
||||
"""
|
||||
fetch = fetcher or _urlopen_fetch
|
||||
try:
|
||||
@@ -378,14 +382,18 @@ def fetch_latest(
|
||||
return Version.parse(tag)
|
||||
|
||||
|
||||
def fetch_latest_release(
|
||||
def _fetch_latest_object(
|
||||
url: str,
|
||||
token: Optional[str] = None,
|
||||
timeout: float = 10.0,
|
||||
fetcher: Optional[Fetcher] = None,
|
||||
) -> tuple[Version, Optional[str], Optional[str]]:
|
||||
"""`fetch_latest` plus the two display fields a report wants: the release's
|
||||
own page and its publication date."""
|
||||
token: Optional[str],
|
||||
timeout: float,
|
||||
fetcher: Optional[Fetcher],
|
||||
) -> tuple[Version, dict]:
|
||||
"""`fetch_latest`'s version plus the whole release object it came out of.
|
||||
|
||||
`fetch_latest` deliberately answers one question and validates only the
|
||||
field that answers it. The two callers below want further fields off the
|
||||
same response, and neither may make a second request for them - so the
|
||||
payload is captured on the way through rather than re-fetched."""
|
||||
fetch = fetcher or _urlopen_fetch
|
||||
captured: dict = {}
|
||||
|
||||
@@ -399,12 +407,61 @@ def fetch_latest_release(
|
||||
pass
|
||||
return payload
|
||||
|
||||
version = fetch_latest(url, token, timeout, capturing)
|
||||
return fetch_latest(url, token, timeout, capturing), captured
|
||||
|
||||
|
||||
def fetch_latest_release(
|
||||
url: str,
|
||||
token: Optional[str] = None,
|
||||
timeout: float = 10.0,
|
||||
fetcher: Optional[Fetcher] = None,
|
||||
) -> tuple[Version, Optional[str], Optional[str]]:
|
||||
"""`fetch_latest` plus the two display fields a report wants: the release's
|
||||
own page and its publication date."""
|
||||
version, captured = _fetch_latest_object(url, token, timeout, fetcher)
|
||||
html_url = captured.get("html_url") or captured.get("url")
|
||||
published = captured.get("published_at") or captured.get("created_at")
|
||||
return version, (str(html_url) if html_url else None), (str(published) if published else None)
|
||||
|
||||
|
||||
def fetch_latest_notes(
|
||||
url: str,
|
||||
token: Optional[str] = None,
|
||||
timeout: float = 10.0,
|
||||
fetcher: Optional[Fetcher] = None,
|
||||
) -> tuple[Version, str, Optional[str]]:
|
||||
"""The latest release's notes text, its version, and its own page.
|
||||
|
||||
This is what makes `version notes` answer on a distributed instance at
|
||||
all. Such an instance receives `CHANGES.md` as a nine-line stub with no
|
||||
version entries, and `dist upgrade` never overwrites it
|
||||
(`ownership.is_upgrade_preserved`), so the local file it would read can
|
||||
never carry the entry - not today and not after any future release. The
|
||||
release the feed publishes carries the same text in its `body`, because
|
||||
`release.yml` builds that body out of `version notes` in the origin repo.
|
||||
|
||||
Only the feed's *latest* release can be asked for: `update_url` is the one
|
||||
URL a release stamp records, and composing a `/releases/tags/<tag>` URL out
|
||||
of it would be guessing at an API shape rather than reading a recorded one
|
||||
(AGENTS.md invariant 7). The caller therefore compares the returned version
|
||||
against what it asked for and says so - which is not the edge case but the
|
||||
main one: an operator reads the notes *before* the swap, while `VERSION`
|
||||
still names the release being left.
|
||||
|
||||
Raises `VersionError` for an unreachable feed, a non-release answer, or a
|
||||
release with an empty body - an empty answer must never read as "this
|
||||
release has no breaking change to report"."""
|
||||
version, captured = _fetch_latest_object(url, token, timeout, fetcher)
|
||||
body = str(captured.get("body") or "").strip()
|
||||
if not body:
|
||||
raise VersionError(
|
||||
f"{url} answered with release {version} but no notes text (`body` is empty) - "
|
||||
"nothing to print, and an empty answer must not read as 'nothing to report'"
|
||||
)
|
||||
html_url = captured.get("html_url") or captured.get("url")
|
||||
return version, body, (str(html_url) if html_url else None)
|
||||
|
||||
|
||||
# --- CHANGES.md ------------------------------------------------------------
|
||||
#
|
||||
# The changelog is prose and stays the author's job. What is mechanical is the
|
||||
|
||||
Reference in new issue
Block a user