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
@@ -19,10 +19,16 @@ number means, and `instructions/dev/version-parts.md` for the candidate model):
|
||||
rendered list - the correction path for the judgment `version bump
|
||||
--impact` made at the time, per Gitea #95's fix for an unreadably long,
|
||||
ungraded bump list.
|
||||
- `version check` is the one command in `wikitool` that makes a network call.
|
||||
It is deliberately its own command: nothing else reaches for it implicitly,
|
||||
it needs no key, it times out, and a feed that cannot be reached is reported
|
||||
as an error rather than silently answered as "up to date".
|
||||
- `version notes` prints one version's release notes. In a tree that writes
|
||||
its own `CHANGES.md` that is a mechanical extraction from it; on a
|
||||
*distributed* instance, whose `CHANGES.md` is a stub `dist upgrade` never
|
||||
overwrites, it falls back to the release feed, because otherwise the command
|
||||
can never answer there - not today and not after any future release.
|
||||
- `version check` and that fallback are the only two network calls in
|
||||
`wikitool`, and neither is implicit: `check` exists for the call, `notes`
|
||||
announces the URL on stderr before asking and takes `--offline`. Both need
|
||||
no key, both time out, and a feed that cannot be reached is reported as an
|
||||
error rather than silently answered as "up to date" or "no notes".
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -32,10 +38,18 @@ from typing import Optional
|
||||
|
||||
import typer
|
||||
|
||||
from rich.console import Console
|
||||
|
||||
from chemenu import config, version as version_mod
|
||||
from chemenu.commands._util import console, fail, rel_path, success, today_iso
|
||||
from chemenu.version import Version, VersionError
|
||||
|
||||
# `version notes` is the one command whose stdout is consumed by a machine -
|
||||
# `release.yml` redirects it into the file it posts as the release body - so
|
||||
# everything it says *about* the notes goes here instead of onto the same
|
||||
# stream as the notes themselves.
|
||||
err = Console(stderr=True)
|
||||
|
||||
app = typer.Typer(
|
||||
help="Report, bump, and check the stack version (see tools/CONTRACT.md).",
|
||||
invoke_without_command=True,
|
||||
@@ -169,13 +183,48 @@ def notes_command(
|
||||
version: Optional[str] = typer.Option(
|
||||
None, "--version", help="Which entry to print (default: this tree's VERSION)"
|
||||
),
|
||||
offline: bool = typer.Option(
|
||||
False, "--offline",
|
||||
help="Never ask the release feed: on a distributed instance, whose CHANGES.md carries no "
|
||||
"entry to print, fail with the release page instead of fetching the notes",
|
||||
),
|
||||
url: Optional[str] = typer.Option(
|
||||
None, "--url", help="Release feed to ask for the fallback (default: the stamp's, as `version check`)"
|
||||
),
|
||||
timeout: float = typer.Option(10.0, "--timeout", help="Seconds to wait for the feed"),
|
||||
):
|
||||
"""Print one version's `CHANGES.md` entry, for use as release notes.
|
||||
"""Print one version's release notes: the `CHANGES.md` entry where there is
|
||||
one, the installed release's notes from the feed on a distributed instance,
|
||||
where there never is.
|
||||
|
||||
Mechanical extraction, so the release workflow never has to parse markdown
|
||||
in shell."""
|
||||
in shell - which is also why **stdout carries nothing but the notes** and
|
||||
every line about where they came from goes to stderr. `release.yml` does
|
||||
`version notes > /tmp/release-notes.md`.
|
||||
|
||||
The fallback is reached only with a release stamp present, i.e. only from a
|
||||
tree that came out of `dist export`. A dev checkout keeps the plain error,
|
||||
so this command cannot make a network call in the origin repository or in
|
||||
CI. See `version_mod.fetch_latest_notes` for why only the feed's *latest*
|
||||
release can be asked for."""
|
||||
run_notes(version=version, offline=offline, url=url, timeout=timeout)
|
||||
|
||||
|
||||
def run_notes(
|
||||
version: Optional[str] = None,
|
||||
offline: bool = False,
|
||||
url: Optional[str] = None,
|
||||
timeout: float = 10.0,
|
||||
fetcher: Optional[version_mod.Fetcher] = None,
|
||||
) -> None:
|
||||
"""`version notes` itself, free of Typer's option objects - the same split
|
||||
`dist_cmd.run_export` makes, and for the same reason. `fetcher` is the
|
||||
network seam: a test passes one, nothing else does."""
|
||||
import os
|
||||
|
||||
try:
|
||||
wanted = Version.parse(version) if version else version_mod.read_version()
|
||||
stamp = version_mod.read_stamp()
|
||||
except VersionError as exc:
|
||||
fail(str(exc))
|
||||
return
|
||||
@@ -186,13 +235,65 @@ def notes_command(
|
||||
return
|
||||
|
||||
section = version_mod.changes_section(changes.read_text(encoding="utf-8"), wanted)
|
||||
if section is None:
|
||||
if section is not None:
|
||||
typer.echo(section, nl=False)
|
||||
return
|
||||
|
||||
release_url = str((stamp or {}).get("release_url") or "").strip()
|
||||
if stamp is None or offline:
|
||||
fail(_no_entry_message(wanted, stamp is not None, release_url))
|
||||
return
|
||||
|
||||
feed = url or version_mod.update_url(stamp)
|
||||
token = os.environ.get(version_mod.UPDATE_TOKEN_ENV, "").strip() or None
|
||||
err.print(
|
||||
f"[dim]{version_mod.CHANGES_FILENAME} has no entry for {wanted} - a distributed instance "
|
||||
f"receives it as a stub. Asking {feed}[/dim]"
|
||||
)
|
||||
try:
|
||||
latest, body, page = version_mod.fetch_latest_notes(feed, token, timeout, fetcher)
|
||||
except VersionError as exc:
|
||||
fail(
|
||||
f"{exc}. The notes for {wanted} are on the release page instead: "
|
||||
f"{release_url or '(no release_url in the release stamp)'}"
|
||||
)
|
||||
return
|
||||
|
||||
if latest == wanted:
|
||||
err.print(f"[dim]These are {latest}'s notes, from {page or feed}[/dim]")
|
||||
else:
|
||||
err.print(
|
||||
f"[yellow]These are {latest}'s notes, not {wanted}'s[/yellow] - the feed publishes only "
|
||||
f"its latest release, and this tree declares {wanted}. That is the expected shape "
|
||||
f"before an upgrade, where VERSION still names the release being left. From "
|
||||
f"{page or feed}"
|
||||
)
|
||||
typer.echo(body)
|
||||
|
||||
|
||||
def _no_entry_message(wanted: Version, has_stamp: bool, release_url: str) -> str:
|
||||
"""Why there is no entry, and where the notes are instead.
|
||||
|
||||
Two trees land here and they are not the same mistake: a dev checkout that
|
||||
has not written its entry yet, and an instance that was told not to go
|
||||
online (the only way an instance reaches this at all). Naming the wrong one
|
||||
sends the reader to the wrong fix."""
|
||||
if not has_stamp:
|
||||
return (
|
||||
f"{version_mod.CHANGES_FILENAME} has no entry for {wanted} - "
|
||||
f"run `wikitool version bump` before releasing, or write the entry"
|
||||
)
|
||||
return
|
||||
typer.echo(section, nl=False)
|
||||
where = (
|
||||
f"Read them on the release page instead: {release_url}"
|
||||
if release_url
|
||||
else f"The release stamp records no `release_url` to point at - `wikitool version check` "
|
||||
f"names the feed this instance asks."
|
||||
)
|
||||
return (
|
||||
f"{version_mod.CHANGES_FILENAME} has no entry for {wanted}, and a distributed instance "
|
||||
f"never has one: it receives the file as a stub and `dist upgrade` never overwrites it. "
|
||||
f"--offline was passed, so the feed was not asked. {where}"
|
||||
)
|
||||
|
||||
|
||||
@app.command("bump")
|
||||
|
||||
Reference in new issue
Block a user