Five languages, roles behind a foreign key, and Minesweeper out of the platform. This one has a SCHEMA CHANGE, which is why the plan is longer than usual: two foreign keys onto roles.name with ON UPDATE CASCADE, and the CHECK that hardcoded four role names dropped. The plan carries the exact SQL, guarded and idempotent, and treats bun db:push as verification rather than the mechanism — push can fail and still exit 0, and this is precisely the create-then-constrain shape that catches it. Checked before cutting the tag, so the plan can state it rather than hope: every host is on deploy/2026.08.23.04, which is the release that created and seeded `roles`. alpha, fofos, production-pertento-bridge and deadpool-rezio-bridge each report roles=4 and zero orphans. macbookpro has no psql client, so only its tag could be read — the plan says so and tells the commander to check it there rather than assume. The SQL was run against a scratch database first: applies, applies again unchanged, produces both FKs with ON UPDATE CASCADE and no ON DELETE, drops ck_role_permissions_role, leaves ck_role_permissions_not_owner. Seeded with an orphan role it raises and creates nothing, which is the case that would otherwise half-apply. tsgo clean, 1409 tests, docs:check green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
6.7 KiB
2026.08.23.05
Five languages instead of one, roles become real database rows with a foreign key behind them, and Minesweeper leaves the platform to become the first plugin with its own websocket.
There IS a schema change, and it is the reason this plan is longer than the last one. Two foreign keys
and one dropped CHECK, on users and role_permissions. Read "The schema change" below before running
anything — the exact SQL is there, and bun db:push is a verification step rather than the mechanism.
No dependency change. bun.lock is untouched. Do not run bun install.
What is in it
- Five locales, complete and in step.
en-GB,pt-PT,es-ES,de-DE,fr-FR— 1,129 keys each. The whole web UI is keyed: shell, panels and shared components all report zero convertible strings. - Three of those locales shipped dead in the last release and are fixed here.
es-ES,de-DEandfr-FRwere translated, in parity, and offered by the picker — while all three catalogue files still imported onlyen-GBandpt-PT, so choosing one rendered the entire UI in raw keys. The parity tests could not have caught it: they compare JSON files to each other, and a file nobody imports is as parity-correct as one that is loaded.lib/catalogues.test.tsis now the thing that knows. - Roles are a table with a foreign key, not a TypeScript union.
USER_ROLESis gone,UserRoleisstring, and the compile-time guard moves to a runtime check at every write path that takes a role from outside. Renaming a role now moves its holders and grants atomically, as Postgres's job. - Minesweeper is a plugin (
gitea.officer.dev/plugins/minesweeper), not part of the platform. The permission registry drops from 22 entries to 21 —executiongoes 4 to 3. - Plugins can have websockets.
/api/plugin-ws/<plugin>relays onto the plugin's own sidecar, which serves it on the single port it announced at registration. Authorisation is the plugin's own permission, so there is no second grant to forget. - A plugin can declare the architecture its packages need.
multiarch: 'i386'runsdpkg --add-architectureandapt-get updatebefore installing, on apt hosts, skipped when already enabled. A closed enum rather than a command, because it runs as root. - Foreign keys are named explicitly across the fleet's plugins, ending the
db:pushchurn where Postgres truncated an over-long generated name and drizzle-kit re-created the constraint every run.
The schema change
Both foreign keys reference roles.name, so every value in users.role and role_permissions.role must
already exist as a row in roles, or the constraint cannot be created.
That was checked on every host before this tag was cut. All five are on deploy/2026.08.23.04, which is
the release that created and seeded roles; alpha, fofos, production-pertento-bridge and
deadpool-rezio-bridge each report roles = 4 and 0 orphans. macbookpro was NOT verified — it has
no psql client, so only its tag could be read. Check it there before running the SQL, by whatever means
that host offers.
Run this first, before bun db:push. It is idempotent and safe to run twice.
-- Refuse to proceed if any role in use is not a real row. Raises rather than half-applying.
DO $$
DECLARE bad int;
BEGIN
SELECT count(*) INTO bad FROM (
SELECT u.role FROM users u LEFT JOIN roles r ON r.name = u.role WHERE r.name IS NULL
UNION ALL
SELECT p.role FROM role_permissions p LEFT JOIN roles r ON r.name = p.role WHERE r.name IS NULL
) x;
IF bad > 0 THEN
RAISE EXCEPTION 'STOP: % role value(s) have no matching roles row. Do not continue; report this.', bad;
END IF;
END $$;
ALTER TABLE role_permissions DROP CONSTRAINT IF EXISTS ck_role_permissions_role;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_users_role') THEN
ALTER TABLE users ADD CONSTRAINT fk_users_role
FOREIGN KEY (role) REFERENCES roles(name) ON UPDATE CASCADE;
END IF;
END $$;
DO $$ BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'fk_role_permissions_role') THEN
ALTER TABLE role_permissions ADD CONSTRAINT fk_role_permissions_role
FOREIGN KEY (role) REFERENCES roles(name) ON UPDATE CASCADE;
END IF;
END $$;
ck_role_permissions_not_owner stays. It is a different constraint and this release does not touch it.
Then verify by querying, not by reading an exit code:
SELECT conname, pg_get_constraintdef(oid) FROM pg_constraint
WHERE conname IN ('fk_users_role','fk_role_permissions_role',
'ck_role_permissions_role','ck_role_permissions_not_owner');
Both fk_ rows must be present and say ON UPDATE CASCADE with no ON DELETE.
ck_role_permissions_role must be absent. ck_role_permissions_not_owner must be present.
Put that output in the report — for this release it is the evidence, and "schema ✓" without it is a guess.
Every host
git fetch origin --tagsthengit merge --ff-only deploy/2026.08.23.05- The SQL above, then the verification query. Stop on the
RAISE EXCEPTIONand report it. bun db:push— expect no changes. If it still wants to alter something after step 2, the SQL was incomplete; stop and report rather than letting it improvise.bun run plugins:updatepm2 restart officer
officer only at step 5. Step 4 restarts each plugin's own sidecar, which belongs in the report.
macbookpro: append /opt/homebrew/bin to PATH for the bun commands, run pm2 separately.
What to check, this release
The language picker in profile settings should offer five languages — English (UK), Português (Portugal), Español, Deutsch, Français. Pick one that is not English and look at a panel, not just the shell: the last release's defect was invisible in the shell and total in the panels.
A host still offering two languages did not pick up the frontend bundle.
Minesweeper should be absent from the dock and /minesweeper should 404. It is a plugin now and no host
has it installed. That is correct, and installing it is not part of this release.
Hosts
edge-pertento-bridge— skipped, perdocs/servers.md. Name it and spend nothing on it.macbookpro— the one host whose roles table could not be verified before the tag. Check it, then proceed; do not skip it on that account.- All five others were on
deploy/2026.08.23.04when this was cut, so each is exactly one release behind.
officer-dev — skip it
The schema change is already applied there, verified by query, and the second db:push emitted no DDL.
Restarting officer on the host running the rollout ends the rollout. Name it as skipped and say it is
current.