From 0cc710a0636920312bf7c430b423852ffc7725e3 Mon Sep 17 00:00:00 2001 From: Lemmy Date: Wed, 29 Jul 2026 01:04:08 -0400 Subject: [PATCH] ci(catalog): only change updated_at when the plugin version changes --- .github/workflows/update-catalog.py | 78 +++++++++++++++++++++++------ 1 file changed, 64 insertions(+), 14 deletions(-) diff --git a/.github/workflows/update-catalog.py b/.github/workflows/update-catalog.py index d5b7711..6fdccac 100644 --- a/.github/workflows/update-catalog.py +++ b/.github/workflows/update-catalog.py @@ -41,7 +41,49 @@ def git_output(*args: str) -> str: ).stdout -def release_history(subdir: str, tip_api: int) -> list[dict]: +def plugin_history(subdir: str) -> list[tuple[str, int, dict]]: + """Every readable revision of `/plugin.toml`, newest first. + + One `git show` per revision, and both the release ladder and the per-version release + dates come out of this single walk. (A directory rename starts the history over, the + same way it resets `added_at`.) + """ + history = [] + revisions = git_output( + "log", "--format=%H %ct", "--", f"{subdir}/plugin.toml" + ).splitlines() + + for line in revisions: + revision, _, commit_time = line.partition(" ") + try: + manifest = tomllib.loads( + git_output("show", f"{revision}:{subdir}/plugin.toml") + ) + except (subprocess.CalledProcessError, tomllib.TOMLDecodeError): + continue # unreadable or pre-`plugin_api` history + history.append((revision, int(commit_time), manifest)) + + return history + + +def release_times(history: list[tuple[str, int, dict]]) -> dict[str, int]: + """When each version string was first committed, keyed by version. + + The earliest commit carrying a version is the bump that released it. A later commit + editing plugin.toml without bumping (tags, description, translations) must not move the + date, and a version that reappears after a revert keeps its original one. + """ + times: dict[str, int] = {} + for _, commit_time, manifest in reversed(history): # oldest first + version = manifest.get("version") + if isinstance(version, str) and version: + times.setdefault(version, commit_time) + return times + + +def release_history( + history: list[tuple[str, int, dict]], tip_api: int, released: dict[str, int] +) -> list[dict]: """Older revisions of a plugin, one per API level below the tip's, newest first. A Noctalia below the tip's `plugin_api` has nothing to install unless the catalog names @@ -51,19 +93,10 @@ def release_history(subdir: str, tip_api: int) -> list[dict]: """ releases = [] lowest_api = tip_api - revisions = git_output( - "log", "--format=%H", "--", f"{subdir}/plugin.toml" - ).split() - for revision in revisions: + for revision, _, manifest in history: if lowest_api <= OLDEST_SUPPORTED_PLUGIN_API: break - try: - manifest = tomllib.loads( - git_output("show", f"{revision}:{subdir}/plugin.toml") - ) - except (subprocess.CalledProcessError, tomllib.TOMLDecodeError): - continue # unreadable or pre-`plugin_api` history plugin_api = manifest.get("plugin_api") version = manifest.get("version") @@ -74,7 +107,16 @@ def release_history(subdir: str, tip_api: int) -> list[dict]: if plugin_api >= lowest_api or plugin_api < OLDEST_SUPPORTED_PLUGIN_API: continue - releases.append({"plugin_api": plugin_api, "version": version, "rev": revision}) + # `rev` is the newest revision still on this API level, not necessarily the bump + # commit, so the date comes from the version rather than from `rev` itself. + releases.append( + { + "plugin_api": plugin_api, + "version": version, + "rev": revision, + "updated_at": released[version], + } + ) lowest_api = plugin_api return releases @@ -116,6 +158,8 @@ def load_plugin_manifest(path: Path) -> dict: # falls back to the file's mtime: an uncommitted plugin still gets a sensible entry so the # catalog can be generated mid-development. (A rename also breaks the link to the commit # that first added the file, which is why added_at falls back too.) + # `updated_at` is only the last plugin.toml touch here; discover_plugins replaces it with + # the date `version` was actually bumped once the file's history has been walked. mtime = int(path.stat().st_mtime) out["updated_at"] = git_commit_time(path) or mtime out["added_at"] = git_commit_time(path, "--diff-filter=A") or out["updated_at"] @@ -139,9 +183,13 @@ def discover_plugins() -> list[dict]: for manifest_path in sorted(ROOT_DIR.glob("*/plugin.toml")): manifest = load_plugin_manifest(manifest_path) directory = manifest_path.parent.name + history = plugin_history(directory) + released = release_times(history) manifest["_directory"] = directory manifest["_order"] = order.get(manifest["id"], len(order)) - manifest["releases"] = release_history(directory, manifest["plugin_api"]) + # A bump that is not committed yet has no commit to date it, so the last touch stands. + manifest["updated_at"] = released.get(manifest["version"], manifest["updated_at"]) + manifest["releases"] = release_history(history, manifest["plugin_api"], released) plugins.append(manifest) plugins.sort(key=lambda plugin: (plugin["_order"], plugin["_directory"])) @@ -160,12 +208,13 @@ def render_catalog(plugins: list[dict]) -> str: lines = [ "# This file is auto-generated. Do not edit manually.", "# Do not include it in your commit.", - "# Community Noctalia plugins catalog.", + "# Noctalia plugins catalog.", "# Index of every plugin this source ships: the minimum needed to render, search,", "# and compat-check the list. The per-plugin plugin.toml stays authoritative; the", "# host re-reads it on enable. Keep one [[plugin]] row per plugin subdirectory.", "# A [[plugin.release]] row names an older revision for a Noctalia below the tip's", "# plugin_api, so an older release stays installable instead of the plugin vanishing.", + "# On every row, updated_at dates the commit that first shipped that row's version.", "", ] @@ -207,6 +256,7 @@ def render_catalog(plugins: list[dict]) -> str: "[[plugin.release]]", f"plugin_api = {release['plugin_api']}", f"version = {toml_string(release['version'])}", + f"updated_at = {release['updated_at']}", f"rev = {toml_string(release['rev'])}", ] )