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
130 lines
6.7 KiB
Markdown
130 lines
6.7 KiB
Markdown
# 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-DE` and
|
|
`fr-FR` were translated, in parity, and offered by the picker — while all three catalogue files still
|
|
imported only `en-GB` and `pt-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.ts` is now the thing that knows.
|
|
- **Roles are a table with a foreign key, not a TypeScript union.** `USER_ROLES` is gone, `UserRole` is
|
|
`string`, 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 — `execution` goes 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'` runs
|
|
`dpkg --add-architecture` and `apt-get update` before 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:push` churn 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.
|
|
|
|
```sql
|
|
-- 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:
|
|
|
|
```sql
|
|
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
|
|
|
|
1. `git fetch origin --tags` then `git merge --ff-only deploy/2026.08.23.05`
|
|
2. **The SQL above**, then the verification query. Stop on the `RAISE EXCEPTION` and report it.
|
|
3. `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.
|
|
4. `bun run plugins:update`
|
|
5. `pm2 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, per `docs/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.04` when 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.
|