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:
@@ -1,4 +1,5 @@
|
||||
import { existsSync, readdirSync } from 'node:fs';
|
||||
import { releaseState } from './release';
|
||||
import { join } from 'node:path';
|
||||
import { PLUGINS_DIR } from './discover';
|
||||
import { OFFICER_ITEMS_DIR, PLATFORM_DIR } from '../data-path';
|
||||
@@ -111,6 +112,24 @@ export async function updatePlugin(
|
||||
onLog?: OnLog,
|
||||
): Promise<PluginActionResult> {
|
||||
const steps: string[] = [];
|
||||
|
||||
/*
|
||||
Refuse when there is nothing to update to.
|
||||
|
||||
The UI already hides the button, and that is not enough: 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. More to the
|
||||
point, `updateAll` iterates a list rather than asking per plugin — without this it would re-install
|
||||
twenty-one plugins to land on the versions they were already on.
|
||||
|
||||
Checked BEFORE the pull, because a pull is what would make the answer stale.
|
||||
*/
|
||||
const state = await releaseState(join(PLUGINS_DIR, appName));
|
||||
if (!state.updateable && state.reason === 'at-latest') {
|
||||
steps.push(`already at ${state.tagAtHead}, the newest release`);
|
||||
onStep?.(steps[steps.length - 1]!);
|
||||
return { ok: true, appName, steps };
|
||||
}
|
||||
|
||||
const pulled = await pullPlugin(appName, onLog);
|
||||
if (!pulled.ok) return { ok: false, appName, steps, error: pulled.error };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user