the marketplace publishes releases, not working trees
The sync read `platform/plugins/` — the directories this machine has installed — and imported each manifest off the working tree. So publishing meant "get officer-dev's checkout to the release, then run the script", and nothing showed when that had not happened. Live proof at the time of writing: av-club 2026.08.29.01, kanban 2026.08.29.02, openseo 0.1.0 and fofos-admin 2026.08.29.26 were all in the store, and none of the four has ever been tagged. It is also why the job could not go on a timer. A cron over it published whatever was in a working tree, on a schedule. Now every registered plugin is mirrored bare into ~/.cache/officer-marketplace/mirrors and read AT ITS NEWEST TAG — manifest, README and file list all out of `git cat-file`. No working tree in the path, so publishing something uncommitted stops being a mistake anyone can make. docs/marketplace.md proposed reading manifests over Gitea's API, which needed a sandbox or a committed manifest.json. Git gives the same guarantee with neither, and the trust model is unchanged: we import our own repositories' code. Third-party submissions still need one of those two. The list of plugins now comes from the `plugins` registry table, which the docs always called the authoritative half and which was in fact a by-product of what happened to be installed. That is what lets `finances` be published without being installed here, and it spans orgs — fofos-admin lives under fofos/, which an org listing would have missed. Adding one is `--add <clone url>`. A plugin with no tag is left alone and named in the report, not deleted. It has made no release, so there is nothing true to say about it; dropping its row would be a decision about somebody's plugin dressed up as a refresh. Also: `release_tag` on plugin_metadata, because the tag is the fact when a manifest version disagrees with it; the sync warns on a mismatch. And icon URLs pin to the tag — `/raw/HEAD/` served whatever was on the default branch, via a 303, so a plugin could show one icon and ship another. Verified: 21/25 published from a release, 4 named as untagged, all 14 tag-pinned icons answer 200. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -14,6 +14,14 @@ export type CatalogueEntry = {
|
||||
appName: string;
|
||||
publisher: string;
|
||||
version: string;
|
||||
/**
|
||||
* The git tag this entry was published from.
|
||||
*
|
||||
* Optional on the wire, not in the database, and the two mean different things. Here it is absent for
|
||||
* a row that predates release-based publishing OR for a catalogue too old to send the field at all —
|
||||
* so a consumer must treat "no tag" as "cannot tell" and fall back to whatever it knew before.
|
||||
*/
|
||||
releaseTag?: string;
|
||||
platform: string;
|
||||
label: string;
|
||||
summary: string;
|
||||
@@ -52,6 +60,7 @@ function toCatalogueEntry(row: Row): CatalogueEntry | null {
|
||||
appName: row.appName,
|
||||
publisher: row.developer,
|
||||
version: m.version,
|
||||
...(m.releaseTag ? { releaseTag: m.releaseTag } : {}),
|
||||
platform: m.platform,
|
||||
label: m.label,
|
||||
summary: m.summary,
|
||||
@@ -106,6 +115,22 @@ export async function catalogueEntry(appName: string): Promise<CatalogueEntry |
|
||||
return { ...entry, ...(row.metadata.readme ? { readme: row.metadata.readme } : {}) };
|
||||
}
|
||||
|
||||
/**
|
||||
* Every registered plugin — identity and clone URL, no metadata.
|
||||
*
|
||||
* This is what the sync iterates, and it is the reason the registry is called authoritative: the
|
||||
* catalogue lists what somebody deliberately registered, not what happens to be checked out on the
|
||||
* machine that runs the job. Unlisted rows are included; hiding a plugin from the public API is not the
|
||||
* same as no longer tracking it, and one taken down for a bad release has to be able to come back
|
||||
* current.
|
||||
*/
|
||||
export async function listRegistry(): Promise<{ id: number; appName: string; repoUrl: string }[]> {
|
||||
return marketplaceDb
|
||||
.select({ id: plugins.id, appName: plugins.appName, repoUrl: plugins.repoUrl })
|
||||
.from(plugins)
|
||||
.orderBy(plugins.appName);
|
||||
}
|
||||
|
||||
// ── Writes. Used by the sync script, never by the public API ──
|
||||
|
||||
type RegisterParams = { appName: string; developer: string; repoUrl: string };
|
||||
|
||||
@@ -73,6 +73,19 @@ export const pluginMetadata = pgTable(
|
||||
.references(() => plugins.id, { onDelete: 'cascade' }),
|
||||
|
||||
version: text('version').notNull(),
|
||||
/**
|
||||
* The git tag this row was read from — what "published" actually means.
|
||||
*
|
||||
* `version` above is the manifest's own claim, and the two are the same string every time somebody
|
||||
* tags correctly. They are kept apart because when they DISAGREE the tag is the fact: it is what was
|
||||
* pushed, and a manifest whose version nobody bumped would otherwise make a real release invisible
|
||||
* to anything comparing versions.
|
||||
*
|
||||
* Nullable because rows existed before releases were the unit of publishing. A null one is a
|
||||
* snapshot taken from a working tree by the old sync, and every consumer should read it as "unknown"
|
||||
* rather than "no release".
|
||||
*/
|
||||
releaseTag: text('release_tag'),
|
||||
/** The platform range it declares support for, e.g. `>=1.0.0`. */
|
||||
platform: text('platform').notNull(),
|
||||
label: text('label').notNull(),
|
||||
|
||||
Reference in New Issue
Block a user