Files
portal/docs/operations.md
T
Bendik Aagaard LynghaugandClaude Opus 5 f18d1a5181 A person stays in dialogue with their case
Five things a building authority needed and portal could not say,
found by building one and replaying its cases through the engine.

The one that mattered: a submitter could only ever act on a record
nobody had touched. `access.rs` pinned a self_transition's from-state
to `open`, though the runtime had always passed the real state and
asked. So the applicant written to for a missing drawing could not
send it, and nobody could withdraw once anything had happened.
`self_transition` now takes `from: [state, ...]` and compiles one rule
per state; omitted it still means the initial state alone, which is
what every unsubscribe link already meant.

Then the four beside it. A record can belong to another record
(`type: record`, `of:`), with `where: { field, equals }` on a listing
as the other half, so an application's page lists the objections that
name it. A public view can be narrowed to the states worth showing,
so a register that is public by law is public about what was decided
and not about what was withdrawn. A state can grant a group, so an
approval that means membership needs no second step. And a state can
be moved by a clock (`after: { days, to }`, src/deadlines.rs) - held
to the same declared edge as a desk button, firing the same mail,
publishing the same event.

Also: the README is a README again, and everything that was crowding
it is in docs/ - architecture, content reference, state machines,
access, operations.

95 tests, up from 82.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-25 21:35:25 +02:00

2.9 KiB

Operations

Portal does not deploy itself. It publishes versions; each site decides when to take one.

Cut a release

cargo release patch        # or minor / major

That bumps Cargo.toml, tags v<version> and pushes. CI (.gitea/workflows/publish.yml) reacts to the tag, builds once, and attaches portal-v<version>.tar.gz (portal, iris, hash.txt, site/) to the Gitea release. Plain pushes to main only run the tests: nothing reaches production from this repo.

The same job re-pins, tests and tags iris to match, so a portal change to anything the lint reads fails the release rather than the next content push.

Before cutting, move the Unreleased lines in CHANGELOG.md under a heading for the new version. That section becomes the release notes, and the table at the end of the file records which version each site runs.

Pkg files are content-hashed (hash-files = true, instances run with LEPTOS_HASH_FILES=true), so a freshly served page can never pair a stale cached bundle with new wasm.

Roll it out

In the content repo, one line of .gitea/workflows/deploy.yml:

env:
  PORTAL_RELEASE: v0.4.0

Its CI downloads the pinned artifact, ships /srv/app/<instance>/releases/<tag>, flips current, rewrites the instance env from that repo's own Actions variables and secrets, restarts app@<instance> and refreshes the Caddy route. Every rollout is a commit, so reverting a bad version is git revert and a push.

Sites upgrade independently and may pin different versions.

Content changes never come through any of this: they hot-reload live from the content repos over NATS.

Add a site

A new content repo with a copy of an existing deploy.yml (change INSTANCE, the port and the domains), Actions variables (KANIDM_URL, OAUTH2_CLIENT_ID, PUBLIC_URL, SITE_NAME) and secrets (NATS_URL, OAUTH2_CLIENT_SECRET, PORTAL_GITEA_API_TOKEN — Gitea reserves the GITEA_ prefix for secret names), a Kanidm OAuth2 client, and DNS. site.yaml handles all branding; no portal change is needed.

Environment

Runtime configuration is environment variables; src/main.rs is the authority and each content repo's deploy.yml holds the production values.

NATS_URL events, projections, content reload, access questions
CONTENT_REPO, CONTENT_BRANCH the repo that is the site
KANIDM_URL, OAUTH2_CLIENT_ID, OAUTH2_CLIENT_SECRET sign-in
KANIDM_API_TOKEN invites and grants:; unset, neither happens
PUBLIC_URL, SITE_NAME what the site calls itself in links and mail
GITEA_API_TOKEN authenticated resource pulls
AUTOMATION_READ_TOKEN the automation KV read endpoint
GARAGE_* uploads
MAPBOX_TOKEN map tiles, proxied; unset, maps are off

Mail is delivered by gdo, a separate unit on the same host reading the same NATS.