update means something now: it depends on tags

"update" was `git pull` and re-install, unconditionally, for every plugin.
nothing could say whether there was anything to pull, so the button was always
there and usually did nothing — and "update all" was twenty-one no-ops with a
spinner on it. the button's own comment admitted it: gating on `outdated` would
hide it for ever, because nothing bumped a version.

versions are dated and tagged now, so the rule the owner asked for is possible:

  · a checkout NOT at a tag is always updateable — it is between releases, no
    claim is being made about it, pulling is always reasonable
  · a checkout AT a tag is updateable only when a NEWER tag exists

the part worth defending is how "newer" is decided. the obvious version-string
comparison is wrong the first time a plugin is tagged `v2` or `1.0.0-rc1`, and
wrong SILENTLY — a plugin that never offers an update looks like one nobody has
released. so newness is asked of git: the newest tag reachable from the REMOTE
branch, compared against the tag at HEAD. that holds for any names anyone
chooses, and there is a test with a tag called `beta` that sorts before `v1` to
keep it honest.

it never fetches. every answer comes from refs already on disk, because this
runs once per plugin on a request that renders a list and twenty-one network
round trips would make the screen feel broken. tags arrive at clone and at the
fetch `pullPlugin` already does.

the VERB refuses too, not just the UI. a plugins list is fetched once and a tag
can be pushed a second later, so the copy someone clicks is routinely older than
the truth — and `updateAll` iterates a list rather than asking per plugin.

"up to date · <tag>" is said out loud rather than shown as an absent button. no
button and up-to-date look identical, and only one of them is information.

verified on the real installs: example pulled to its tag and flipped to
up-to-date, caldav on an older commit stayed updateable, and updatePlugin on
example answered "already at 2026.08.27.01, the newest release" without pulling.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-27 07:44:00 +00:00
co-authored by Claude Opus 5
parent d58f58813c
commit 68d6dd942e
7 changed files with 261 additions and 13 deletions
+6 -1
View File
@@ -129,7 +129,7 @@ pluginsRouter.get('/', async (ctx) => {
};
})(),
})),
plugins: states.map(({ plugin, install, outdated }, i) => ({
plugins: states.map(({ plugin, install, outdated, release }, i) => ({
appName: plugin.appName,
prefix: mountPrefix(plugin),
label: plugin.manifest.label,
@@ -165,6 +165,11 @@ pluginsRouter.get('/', async (ctx) => {
enabled: install?.enabled ?? false,
installedVersion: install?.version ?? null,
outdated,
// The tag rule — whether the REMOTE has something this checkout does not. See plugins/release.ts.
updateable: release.updateable,
releaseReason: release.reason,
tagAtHead: release.tagAtHead,
newestTag: release.newestTag,
processStatus: statuses[i] ?? null,
// Probed live, against this machine's PATH, on every read.
//