efa570333795d50a1559c94e34a4e4c3a65c844e
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
19bbbab907 |
publishing a plugin is pushing a tag
Three mechanisms where there should be one. The catalogue was filled by hand, and each host separately polled twenty-one repositories with git to work out whether a release existed — a second source of truth for a fact one place already knew. Shipping photos 2026.09.03.02 correctly and watching alpha not notice for ten minutes is what started this. The timer. scripts/marketplace-cron.sh, every minute, on the machine that publishes. Safe now only because the previous commit made the sync read tags instead of working trees; over the old one this would have published somebody's afternoon on a schedule. Nothing is written unless the release commit differs from the one already published, so a tick with nothing to do is one query, one fetch per repo and no writes — about 2.7s. One timestamped summary line per tick regardless, because a fresh timestamp is the only thing that separates "nothing to publish" from "the timer died"; a named line only for a plugin that moved. The badge. `updateable` now compares the tag at HEAD with the tag the catalogue says is newest — difference, not ordering, since parsing 2026.08.27.01 works right up until somebody tags v2 and then fails silently. refreshRemote() and its two-minute window are gone with the polling they existed to paper over. The git rule stays as the fallback for a plugin the store cannot speak for: an unregistered checkout, or an unreachable store. That case now reads whatever refs the last pull left, which is the accepted cost of not making every machine poll every repository forever. fetchCatalogue()'s cache went from five minutes to one. It only drew tiles before; now the wait for a release to appear would have been the sync's minute plus its five. Also: the Update button names its destination whenever a release is known, not only on `new-release`. An untagged checkout reports `untagged` and used to lose the tag with it, so the button said "Update" and the owner had to guess where it landed — the marketplace knows, even for a checkout that has never fetched it. Verified: forced a stale row, the timer republished it inside 60s and logged one line. Badge decisions checked against the live catalogue for seven plugins, including the two shapes that matter — at a tag the store agrees with, and untagged with a release known only to the store. 9 release tests pass, 100% of release.ts covered, four of them new and all against real repositories. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
78e89ebcec |
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> |
||
|
|
d381140d54 |
plugins say what they are for, and their checkouts stop lying about themselves
two things, and the second is why the first was painful. TAGS. the marketplace is going to be browsed rather than scrolled: tags on the left, the plugins carrying one on the right. a plugin declares its own in the manifest and the catalogue caches it on sync, exactly as it already does for label and summary — the alternative is editing a table every time a plugin ships, and a plugin that cannot say what it is. the vocabulary is CLOSED (PLUGIN_TAGS) because free text becomes homelab, home-lab, Home Lab and selfhosted inside a month, and a filter list of synonyms is worse than no filter. an unknown tag is refused at discovery BY NAME, so the author is told which tag rather than which array index. all 21 are tagged, each with a one-line note saying why, so a later disagreement is with a stated reason rather than a mystery. every tag has at least three plugins behind it — no filter opens onto an empty page. PLUGINS:CHECK. getting one commit into 21 repositories failed seven times and none of the reasons were about the commit: six remotes were HTTPS and cannot authenticate unattended, three checkouts were behind their remote, one URL had no .git suffix so the catalogue published a clone URL unlike every other, and offscale's fetch refspec named a branch that does not exist. that last one is why this is a script and not a habit. every command reported on a phantom — `git push` said "Everything up-to-date" while the branch looked diverged — and the repository was fine the whole time. twenty minutes spent in a repo with nothing wrong with it. none of it is visible until you try to push, which is the worst moment to learn it: mid-task, across 21 repositories, and every failure looks like it is about your change. so it checks the facts with one right answer — remote URL, fetch refspec, branch, upstream, dirty tree, ahead/behind — and exits non-zero. `--fix` is a separate flag so running the check can never be what changes your tree, and divergence is never auto-repaired because it needs a decision about content. verified by breaking each case on purpose and watching it catch them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043 |
||
|
|
b9524ee4d5 |
prefix the env with OFFICERDEV_, and keep reading the bare names
PORT, PUBLIC_URL and POSTGRES_URL are names half the software in the world uses. Bun auto-loads .env into EVERY process started in the platform directory, so a bare PORT reaches the sidecars, the tools they spawn and anything an agent runs from here — and a bare POSTGRES_URL reaches things with no business holding a database credential. Four parts. officer-setup now writes OFFICERDEV_-prefixed names, with the reasoning in the generated file so whoever opens it later knows why. Every consumer reads `OFFICERDEV_X ?? X` — eleven sites across the databases, the DAV router, officer-url.mjs, os-user-postgres and two scripts. The bare name is the FALLBACK, never the source, which is what makes this safe to ship before every machine has been converted. `bun run fix:env` converts a file in place. Idempotent, preserves comments and `export`, leaves the value untouched including an `=` inside it, and refuses to touch names belonging to other software — prefixing PATH or NODE_ENV does not rename a variable, it deletes it. And it runs on every restart from the server settings screen. There rather than at boot because a restart is the one moment the file can be rewritten and take effect in the same breath: the process about to start is the one that reads it. Non-fatal, since the fallback means the worst case is that nothing changed. gen-index reads either spelling out of the file directly, because it is run by hand as often as by the setup and a bare shell has no .env loaded. Verified by converting this machine's own .env: PORT, PUBLIC_URL and the database all still resolve, gen:index still finds its URL, 1152 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
921fbff9fe |
marketplace.officer.dev: the catalogue becomes a service
The plugins page read a TypeScript constant. It now reads a public API backed by its own database, and falls back to that constant when it cannot. ── Four pieces ── `src/databases/marketplace_db` — a second Postgres database, `officer_marketplace`. Two tables, split by owner and lifetime: `plugins` is the registry (app name, developer, clone URL) and is authoritative; `plugin_metadata` is a CACHE of what each repository says about itself, so the website can render without cloning three repos per page load. Keeping them apart is what makes the cache safe to be wrong — a refresh can never lose the fact that a plugin exists. `scripts/marketplace-sync.ts` — fills it from the plugin repositories. Reads each `manifest.ts` (imported, since a manifest is TypeScript), derives `has` from the directory rather than trusting a declaration, and rewrites SSH remotes to HTTPS: `example` and `offscale` had `git@gitea…` origins, which would have handed every installer a URL that only works on the machine that published it. `src/servers/marketplace/server.tsx` — its own process, its own port, two GET routes and no others. Not part of the platform server: it has no users, no sessions and no request it answers differently per caller, and mounting a public unauthenticated surface inside the process holding mail, a wallet and passkeys means writing permission-gate exemptions that later turn out to be too wide. `src/apps/marketplace` — the public site. A window, deliberately: nothing here installs anything, because installing means cloning a repo and starting a process on someone else's machine. Every plugin page ends at "install it from your own Officer". ── The platform side is the part that had to be careful ── `fetchCatalogue()` now answers live → cached → bundled, in that order, and never throws. The plugins page is where an owner manages plugins they ALREADY have, so the store being down must not take that page with it. Network responses are validated per entry — not against a hostile marketplace, which has already won if the owner installs what it lists, but against a wrong one: a proxy serving HTML, a half-deployed API, a field renamed on one side. A non-https `source` is refused outright because that string reaches `git clone`. 8 tests on the degradation path. Verified end to end against the running service: live catalogue, cached answer surviving the store dying, bundled fallback. ── One thing found on the way ── `bunfig.toml` and CLAUDE.md both document `bun install --no-frozen-lockfile` as the way to change a dependency. Bun 1.3.14 has no such flag — only `--frozen-lockfile` — so the documented escape hatch does not work. Sidestepped by not making marketplace_db a workspace member (relative imports, zero lockfile change), and `bun install` is frozen-clean again. Worth fixing the docs. tsgo clean, 873 pass / 7 fail unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |