The developer's review of 4 Nov 2026: "an actual conversation with the AI chatbot is not desirable and necessary". The field's voice-mode button, the screen it opened, reading answers aloud and its texts go, and with them flutter_tts, its patched copy in third_party/ (one override fewer), the TTS_SERVICE query in the manifest and the plugin's registrants. The microphone stays and dictates into the question. ImageApproval is the conversation sheet's own again, as only voice mode needed it. Whats-New: Voice mode is gone; the microphone dictates into your question. Whats-New-De: Der Sprachmodus ist weg; das Mikrofon diktiert in deine Frage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
91 lines
27 KiB
Markdown
91 lines
27 KiB
Markdown
# The Halkyon project
|
|
|
|
Halkyon's apps and the packages they share, in one repository and one pub workspace (ADR 0015, decided 2026-10-19). This file holds what applies to every app and package; each app's own `AGENTS.md` holds what is its alone, and is read before working in it: [Halkyon Learn](apps/learn/AGENTS.md), [Halkyon Meditate](apps/meditate/AGENTS.md), [Halkyon Todo](apps/todo/AGENTS.md), [Halkyon Train](apps/train/AGENTS.md).
|
|
|
|
- **Working notes**: if `NEXT.md` exists at the repository's root, read it first. It holds the current step, the plan and the reasoning behind each decision. It is local and never committed.
|
|
|
|
## Layout
|
|
|
|
- **`apps/<app>/`**: each app's packages, as `serverpod create` lays them out (`halkyon_<app>_server`, `_client`, `_flutter`), plus what is the app's alone (Learn's curator, the scripts writing Learn's assets in `apps/learn/tool/`, run from `apps/learn`; the apps' icons are the root's `tool/app_icons.py`, ADR 0031). Learn is the app that runs; Train, Todo and Meditate run too (ADR 0030, ADR 0033, ADR 0038), Todo and Meditate not released yet.
|
|
- **`packages/`**: what the apps share, each with its README and licence, which the apps take by path (`path: ../../../packages/<package>`), so a change to a shared package and to the apps using it is one commit:
|
|
- `halkyon_ui`: Halkyon's look, the theme around cobalt and copper, Noto Sans bundled, the window-size classes and the widgets every screen is built of (the floating bar and rail, `FoldingTabs`' tab row folding into the title), with texts of its own (English, German).
|
|
- `halkyon_base_server`, `halkyon_base_client`: the Serverpod module every Halkyon server includes (ADR 0014): signing in (a password with TOTP and the resets of ADR 0017, SimpleLogin on `halkyon_oidc`, email codes proving an address; no anonymous accounts since ADR 0020, D25, nor GitHub since D36), an account's ways in and its deletion with a hook for each app's own data, the pruning, the instance's basics, people's handles and public profiles, who may sign up (`SIGN_UP`: open, approval, closed) with the accounts waiting for approval and the instance's operators who approve them (Halkyon Learn's ADR 0021, step 1), the operator's assistants forwarded on halkyon_llm with their grants, each person's budget, the efforts they allow and counted figures, and the instance's privacy page made from its settings (step 2), and the instance's register (ADR 0016): one account per person across an operator's services, held by one service (Learn's server on learn.halkyon.eu) and asked by the others, its members (`ACCOUNT_REGISTER_URL`), all sharing a secret (`INSTANCE_SECRET`); its README says how.
|
|
- `halkyon_app`: every app's shell on the module's client (ADR 0014): the instances a device keeps accounts on and the first start, signing in and switching person, the gates building an app's dependencies per instance and per person, the sync service and where it stands, the account section of an app's settings (`AccountTiles`), every app's Profile (`HalkyonProfile`, decided 2026-11-02: the person's own tab, the same in every app, with their account, the ways they sign in and their public profile, one for all apps, then the app's own tabs) and people's public profiles (`PublicProfileRepository`, `PublicProfileSection`, `showProfileOf`), the wait screen of an account waiting for approval and the operator's card, badge and screen (ADR 0021), the assistants of an app's settings (`AssistantsCard`: granted, by provider, agreed to, what is left of the person's budget, used, and the person's own keys) and the operator's grants from that screen (`AssistantGrantsScreen`: several assistants to several people at once, each with one budget, D57 to D63), with the repositories asking them through the module, at an effort the operator allows, or a person's own key directly (step 2) and what an app's warning before a private studyset goes names (`providerTermsOf`, `OwnAssistant.onThisMachine`, step 3), About with the instance's source link (`showAppAbout`), the device's own speech recognition (`SpeechInput`, `DeviceSpeechInput` on `speech_to_text`, `FakeSpeechInput` in `testing.dart`; none on Linux) and dictation into a text at its cursor, listening again after each pause (`Dictation`, Meditate's ADR 0038, D15; an app that listens declares the microphone and its usage texts itself), what's new after an update (`WhatsNew`, `WhatsNewOnUpdate`, `WhatsNewTile`, from the app's `assets/whats_new/`, Train's ADR 0030, D61), the language every app picks (`resolveAppLocale`) and countries' and subdivisions' names (Unicode CLDR's, bundled), with texts that name the app.
|
|
- `halkyon_core` (low-level primitives in pure Dart: ids, a logger, `SerialQueue`, `CoalescingRunner`) and `halkyon_interop` (contracts between apps, pure Dart).
|
|
- `halkyon_keys` (pure Dart on libsodium, ADR 0017): an account's keys on the device, the password's Argon2id and SRP's x, the key pairs and their wrapping, the recovery phrase, data keys, content sealed and signed, row IDs keyed by the account (`keyedRowId`). SRP-6a's two halves are the module's (`halkyon_base_server`'s `base.dart`, `halkyon_base_client`'s `srp.dart`), as are TOTP and the recovery codes. `sodium`'s build hook compiles libsodium, so every machine that builds or tests needs a C compiler and `make`.
|
|
- `halkyon_llm` (pure Dart, Halkyon Learn's ADR 0021, D15 and D46): language models' APIs streamed over plain HTTP, no SDKs: Anthropic's Messages API, OpenAI's chat completions (each known host's fields, D45) and its Responses API, behind one interface (`LlmClient`), with `FakeLlmClient` for tests; tested on replies written from the providers' documentation and on those `tool/record_llm.dart` records with a key (D36). The module forwards the operator's assistants with it, and the apps call a person's own provider. BSD-3-Clause as `halkyon_oidc`, `publish_to: none`.
|
|
- `halkyon_oidc_server`, `halkyon_oidc_client` (ADR 0023): signing in with OpenID Connect, with a SimpleLogin preset, as a Serverpod module that halkyon_base builds on; `halkyon_oidc_flutter`, the device's half: the provider's page opened, the way back caught (the app's scheme, or the loopback on Linux and Windows, `LoopbackWebAuth`) and its state checked. Nothing of Halkyon's is in it, so that others can use it with Serverpod: under BSD-3-Clause, Serverpod's own licence, where the other packages are AGPL; `publish_to: none` until it is published to pub.dev, which is asked again first.
|
|
- **`third_party/`**: others' packages patched for us, each a pub release copied whole with only what its README names changed, overridden by path in the root `pubspec.yaml` (today the two Android plugins still applying the Kotlin Gradle Plugin themselves, which AGP 9's built-in Kotlin refuses soon); not our code, so left out of the analysis and the format check; the images drop its overrides (`tool/workspace_only.sh`): they have Dart without Flutter, and pub refuses an override depending on the Flutter SDK, used or not. Drop an override once its package's release has the change.
|
|
- **`deploy/`**: the instance deploy at the root, learn.halkyon.eu's today (Learn's `AGENTS.md`, *CI and deploys*), where other services' compose files join Learn's (Train's `compose.train.yaml`, Todo's `compose.todo.yaml`, each switched on in `.env`). **`snap/`**: Learn's snap, built from the root, which its build needs whole; Todo's and Train's are `apps/todo/snap/` and `apps/train/snap/`, which the release tool builds each in a clone of the repository with it in place of `snap/` (Todo's `AGENTS.md`, *Releases*). **`docs/adr/`**: the decision records of every app and package, one numbering. **`tool/`**: the repository's tools (below).
|
|
- **One workspace, one lock**: the root `pubspec.yaml` lists every package, `pubspec.lock` holds one version of each dependency for all of them, and its `dependency_overrides` hold for every app (the offline sync fork, Learn's `AGENTS.md`; `cli_util`, below). **Serverpod's version moves as one**: every package pins Serverpod's packages to the same version (4.0.4), the module's, Meditate's, Todo's and Train's too, together with the CLI version CI generates code with (`SERVERPOD_VERSION` in `.forgejo/workflows/ci.yaml`) and the sync packages Learn's server depends on.
|
|
|
|
## How work is done here
|
|
|
|
- **The agent implements; the developer decides.** Put every decision to the developer before building (design choices, alternatives, framework defaults kept or changed). The first time a new pattern appears (e.g. the first repository/gateway pair, the first synced model, the first endpoint with its denial tests, the first Serverpod module), walk through it in detail with the developer; afterwards, implement exceedingly similar cases directly and point out only deviations.
|
|
- **Tests come first** and state the specification. Every access rule gets a test that asserts **denial**, not only permission.
|
|
- **Explain every migration's SQL.**
|
|
- **Before keeping a framework default, name the alternatives** and recommend one, especially where a choice becomes expensive once users or data exist.
|
|
- Every decision carries its reason (in `NEXT.md` or an ADR under `docs/adr/`).
|
|
- **Commits** are GPG-signed and carry a DCO `Signed-off-by` trailer (`git commit -s`; a local `prepare-commit-msg` hook adds it automatically), and **keep their type prefix** (`feat`, `fix`, `perf`, `refactor`, `docs`, `build`, `chore`, a `!` or a `BREAKING CHANGE:` line for what breaks): releases work out their versions from them (Learn's `AGENTS.md`, *Releases*). A commit whose change people using an app would notice says so for them in its trailers, `Whats-New: <one sentence>` and `Whats-New-De: <its German>`, once a change, short (a release's lines share F-Droid's 500 characters) and in the app's own names for its screens and buttons; the release drafts its *What's new* from them alone (Train's ADR 0030, D61, 4 Nov 2026). A commit scoped to apps (`feat(todo,meditate):`) counts for those alone, a shared package's for every app it touches; a refactor, a build change or a test carries none.
|
|
- **Servers are named in prose by the domain they serve** (learn.halkyon.eu's server, the halkyon.app server), which says what a reader needs; the SSH aliases and the CI runner's label keep the servers' own names (`sahaquiel-deploy`, `ramiel-fdroid`, `sahaquiel:host`).
|
|
- **Pushes** pass the checks first, through a local `pre-push` hook (in a fresh clone: `ln -s ../../tool/pre-push .git/hooks/pre-push`): `tool/pre-push` first refuses a push to `main` that brings a commit not signed by a key in `deploy/allowed-signers` (`tool/check_signatures.sh`, the deploy's own rule), so it fails on the laptop rather than after CI, then runs `tool/check.dart`, but for a push of release commits alone (the release tool's `chore(release):` commits changing an app's version line and its texts of what's new, nothing else: `tool/release_push.dart`, decided 3 Nov 2026, so that releases in a row don't run the checks once each; CI still runs them). CI runs the checks again, and a green `main` deploys learn.halkyon.eu (see *CI and deploys*), so an unsigned commit on `main` blocks every deploy after it.
|
|
- **Lints**: LeanCode's strict set (`leancode_lint`), configured once in the root `analysis_options.yaml`, which also enables its analyzer plugin. Plugins can only be enabled at the root of a pub workspace and only reach packages whose options include that file: Learn's, Todo's and Train's servers and apps, Learn's curator and every shared package include it; the generated client packages keep their own, but for the module's, which includes it for its SRP half and leaves `lib/src/protocol/` out. Every `// ignore:` gets a comment saying why (`document_ignores`). Each server package switches off the four rules for Dart v3.13's constructor syntax (`use_primary_constructors`, `use_declaring_parameters`, `empty_container_bodies`, `unnecessary_type_name_in_constructor`): Serverpod's code generator (checked with v4.0.3) can't parse that syntax in endpoint files yet.
|
|
|
|
## Flutter and Serverpod
|
|
|
|
Each app is a Flutter app (frontend) backed by a Serverpod server (backend). Always build an app's backend with Serverpod. Build for multiple users, with Serverpod's built-in authentication, which a server gets from `halkyon_base` (Learn's and Todo's `server.dart` configure the module; people sign in with a password and TOTP, ADR 0017, through halkyon_app's screens).
|
|
|
|
The user starts an app's server and Flutter app with `serverpod start` in that app's server package (Learn's: `apps/learn/halkyon_learn_server`). There is no need to check if the server is running: make the changes and call the `serverpod` MCP tools as needed. If the server is not running, an informative error message will be received from the MCP server. Then STOP and ask the user to start it. NEVER start the server yourself. The Flutter app is started along with it, or can be launched from the MCP tool `spawn_flutter_app`.
|
|
|
|
While running, `serverpod start` watches for file changes to run incremental code generation and hot reload both the server and the Flutter app. Calling `serverpod generate` directly is not needed, but might be useful to troubleshoot when an incremental generation fails.
|
|
|
|
ALWAYS use the MCP server instead of the command line. Use the MCP server to:
|
|
|
|
- `create_migration` and `apply_migrations` for database (after you change data models).
|
|
- `create_repair_migration` if the database has drifted out of sync with the migrations.
|
|
- `tail_server_logs` to read logs from the server.
|
|
- `tail_flutter_logs` to read the raw stdout/stderr of the Flutter app.
|
|
- `hot_reload` / `hot_restart` to reload or restart the server and the Flutter app. ALWAYS call `hot_restart` after doing changes in the Flutter app that may not work with normal hot reload (which is automatically applied).
|
|
- `spawn_flutter_app` to start a Flutter app declared under `serverpod: flutter_apps:` in the server `pubspec.yaml`.
|
|
- `get_flutter_app_dtd` (Dart tooling daemon) for connecting to the app through the `dart` MCP.
|
|
|
|
The `serverpod` MCP tools come from `.mcp.json`: `serverpod mcp-server`, a bridge to the running `serverpod start` of one app's server, the one its `--server-dir` names (each app's `AGENTS.md` gives its own). They load when a session starts; if they are missing, the server wasn't running or the session predates `.mcp.json`. Like all agent configuration, `.mcp.json` is git-ignored. A session works with one app's server at a time; work on another app's server package with the CLI below, in a worktree if that app's `serverpod start` runs.
|
|
|
|
NEVER edit generated code. Each server's `lib/src/generated/` directory and its whole client package (`apps/<app>/halkyon_<app>_client`, `packages/halkyon_base_client`) are rewritten by the code generator, but for the module client's SRP half, written by hand (`lib/srp.dart`, `lib/src/srp/`, `test/`): the generator writes only a client's `lib/src/protocol/` (checked with v4.0.4). Change the `.spy.yaml` models, the endpoints, or the server's `lib/server.dart` instead.
|
|
|
|
Migrations are a narrow exception: the `migration.sql` of a generated migration MAY be edited by hand when the generated SQL would lose data — to add a data transformation, or to reach a destructive change through non-destructive steps. Never touch the other files in the migration directory, and keep the schema the SQL ends up with identical to `definition.sql` — new databases are created from that file and never run `migration.sql`. A module's tables are part of each server's migrations that includes it; moving a table into a module unchanged needs no SQL but the version rows (`create-migration --empty`, ADR 0014).
|
|
|
|
**Pass `--no-analytics` to every `serverpod` call** (before the command: `serverpod --no-analytics generate`). Otherwise the CLI reports the commands run and the Serverpod features the project uses to PostHog; there is no setting or environment variable to switch that off. The VS Code task, `.mcp.json` and Melos' `generate` script pass it already.
|
|
|
|
Only when the server cannot be started at all, or the MCP tools serve another app's server or checkout, fall back to the CLI in the server package:
|
|
|
|
- `serverpod generate` to regenerate the client and the generated server code (`dart run melos run generate` for every server package at once).
|
|
- `serverpod create-migration` after changing a model with a `table` (add `--force` for destructive changes). It only writes the migration; `serverpod start` applies pending migrations when it boots the server.
|
|
|
|
Tests need no Docker. Each server's `config/test.yaml` sets `database.dataPath`, so Serverpod starts and manages the test database (an embedded PostgreSQL) itself. Just run `dart test` in the server package, which needs the test passwords in its git-ignored `config/passwords.yaml` (throwaway values for the module's, Todo's and Train's tests; Learn's real file for Learn's) or from the environment as CI gives them.
|
|
|
|
Tooling traps: rules of `leancode_lint`'s analyzer plugin are ignored with its prefix (`// ignore: leancode_lint/missing_equatable_props`); `dart fix --apply` moves `flutter_driver` into an app's `dependencies` (as `any`) on every run, so restore it as a dev dependency (`sdk: flutter`) afterwards; never `dart format` generated code, only the hand-written paths, as `tool/check.dart` does (a server's `test/integration/test_tools/` is generated too, so format test files one by one, never the whole `test/` folder); a UUID table's `id` column takes only a set typed `Set<UuidValue>` in `inSet` and `notInSet` (`_uuids` in Learn's `sync_tag_gateway.dart`): it is nullable as declared, so a set literal in its place becomes a set of nullable ids, which Serverpod (v4.0.4) refuses at run time with a type error fake gateways never meet (found 2026-10-17: taking a tag off anything had failed on devices since v0.1.0); and never rebase or check out other commits while `serverpod start` runs: its watcher regenerates code for each intermediate state, leaves stale generated files behind and hot-reloads them. Stop it first, or work in a separate worktree.
|
|
|
|
If the user asks you to test an app:
|
|
|
|
1. Use `get_flutter_app_dtd` (`serverpod` MCP) to get the Flutter app's DTD
|
|
2. Pass the DTD to `connect_dart_tooling_daemon` (`dart` MCP) to connect to the app
|
|
3. Use `flutter_driver` (`dart` MCP) to navigate through the app (each app's `AGENTS.md` says where it starts the driver extension)
|
|
|
|
## Checks, Melos and tools
|
|
|
|
Checklist after doing changes, in this order:
|
|
|
|
- `dart format` (CLI) on the files you changed
|
|
- `create_migration` and `apply_migrations` (MCP - only if necessary)
|
|
- `dart run tool/check.dart` (CLI): the packages the lock names (`flutter pub get --enforce-lockfile`, so a merge never leaves the resolution behind), the images' resolution (`dartImages` in `tool/src/images.dart`: each Dockerfile's build stage replayed up to its `dart pub get` from the working tree's files in an empty folder, with Dart alone at the image's version, downloaded once into `~/.cache/halkyon-check/` and checked against its SHA-256, in an environment holding nothing of the machine's Dart or Flutter, offline; only the deploy builds the images, and two deploys failed on what pub refuses without Flutter, 4 Nov 2026; `--images-only` runs it alone, in seconds, after changing a Dockerfile, the root pubspec or the lock), the generated code (every server's, `dart run melos run generate`, about 15 s: it fails, naming the files, where they differ from what the generator makes of the models and endpoints now, leaving them written anew to be committed, and refuses a Serverpod CLI at another version than the lock holds Serverpod at, `dart install serverpod_cli@<version>`), format check of our own Dart files, `dart analyze --fatal-infos` over the whole workspace, the tools' own tests, and every package's tests through Melos. With `--changed-since=origin/main` it tests only the packages changed since then and those depending on them. The pre-push hook runs it in full, so a push only goes through when it passes.
|
|
- Do `serverpod` MCP `hot_restart` if required (hot reload is done automatically). Will also hot restart Flutter app
|
|
- Check `serverpod` MCP `tail_server_logs` and `tail_flutter_logs` for any issues.
|
|
|
|
- **Melos** (decided 2026-10-19, ADR 0015) runs scripts across the workspace's packages: a dev dependency of the root (`dart run melos …`, its version in the lock), its scripts under `melos:` in the root `pubspec.yaml`: `analyze`; `test:dart` (the packages without Flutter, one at a time, as each server's tests start a PostgreSQL of their own on the same port); `test:flutter` (`flutter test -x requires-server`, one package at a time); `generate` (Serverpod's code generation in every package with a `config/generator.yaml`, the CLI on the PATH); `check` (`tool/check.dart`). Every script takes Melos' filters: `dart run melos run test:flutter --scope=halkyon_app`, or `--diff=origin/main --include-dependents` for what changed and what depends on it; `dart run melos list` lists the packages. The root overrides `cli_util` to 0.5.2 for it: Learn's `flutter_launcher_icons` (0.14.4, its latest) asks for ^0.4.1 and uses only the logger, which 0.5 keeps; drop the override once that package allows 0.5.
|
|
- **Tools are Dart where they hold logic** (decided 2026-10-19): conditions, parsing, parallel work, anything worth a test, as `tool/check.dart` (its file selection in `tool/src/check_files.dart`, tested in the root's `test/`). **Bash for glue**, short and linear (`tool/pre-push`, `tool/check_signatures.sh`, `tool/workspace_only.sh`, which narrows the root pubspec's workspace to the packages an image builds), **and for everything run on the production host**, which has no Dart SDK and gets none (`deploy/redeploy.sh`, `deploy/backup.sh`). **Python only where a library exists only there** (the apps' icons with librsvg and Pillow, `tool/app_icons.py`, its Anki fixtures with Anki itself); Learn's data scripts and `packages/halkyon_app/tool/subdivision_names.py` need nothing but Python's standard library and may move to Dart when they next change. **Releases come from `tool/release.dart`** (decided 2026-10-19, ADR 0015), shared by the apps, each configured in Dart beside it (`apps/<id>/release.dart`, typed and commented, registered in `tool/src/release/apps.dart`): `dart run tool/release.dart learn [major | minor | patch] [--dry-run]`, or several apps one after another (`train:minor learn`, decided 3 Nov 2026: each as alone, the run stopping at the first that fails; `tool/src/release/targets.dart`), on a developer's laptop, never in CI, through `tool/with_signing_keys.sh learn …` (Bash glue, fetching every named app's key): the keystores are kept in Proton Pass alone (vault *Infra · Signing*, each a note's attachment, the password its hidden field), and it downloads the app's and the F-Droid index's into a folder in memory (`$XDG_RUNTIME_DIR`), hands their paths and, but for a dry run, their passwords (each item's hidden field *Password*, in the environment as `<keystore variable>_PASSWORD`, so the tool asks for neither) and the Forgejo token (a login in the vault *Infra*, `HALKYON_FORGEJO_TOKEN`) to the release tool and removes the files however the run ends (`test/release/signing_keys_test.dart` holds it to every app released). Tags are per app (`learn-v0.19.0`; an app's earlier tags, Learn's `v…`, count as its history), and only commits touching the app decide its version: its folder, the shared packages its packages use by path (transitively), the root pubspec and lock, its release files (key pins, F-Droid listing, snap recipe) and where its packages were before a move. Each release's *What's new* (Train's ADR 0030, D61), one file a language in the app's `assets/whats_new/` (`<version>.<language>.txt`, English and German), is drafted by it from the commits after *Go on?* (their `Whats-New:` and `Whats-New-De:` trailers, below) and edited in `$VISUAL`/`$EDITOR` until it fits (no `TRANSLATE:` left, F-Droid's 500 characters), committed with the version, and is the app's sheet after the update, the Forgejo notes and F-Droid's changelog. `--dry-run` reads everything a release would (the version, the commits that count and what each touches, the steps, the notes it would publish), changes nothing, asks for no secret, and lists what a release would refuse. Its pure logic is in `tool/src/release/`, tested in the root's `test/release/`. `tool/fdroid_repo.sh` stays Bash (glue for fdroidserver); `tool/with_signing_keys.sh fdroid [APK]` runs it alone with the index's key, e.g. when a release's upload failed after its release was made; a release colliding with the entry Forgejo makes of the pushed tag itself asks again, a few seconds later. Build 21 (`v0.18.0`) was the last release of `tool/release.sh`, the Bash script it replaced, removed since.
|
|
|
|
## CI and deploys
|
|
|
|
- **CI** (`.forgejo/workflows/ci.yaml`) runs `dart run tool/check.dart` on every push of a branch (not of a tag: a release pushes its tag right after `main`, on the same commit), on the Forgejo runner on learn.halkyon.eu's server (`deploy/runner/`), except for a commit that passed before: the job marks passed commits in the runner's home (`checked/`, kept 90 days), so a branch's commit fast-forwarded into `main` goes straight to its deploy. It checks every package, whichever a push changed. Jobs run directly on the host as the unprivileged user `runner`, never in containers: Docker access would be root on the production server. The runner's systemd unit keeps jobs away from production (no `/home`, no Docker socket, Docker's container networks unreachable, memory and CPU capped); keep it that way when changing it. `runner` has user ID 61000 and must never share one with a container's processes (PostgreSQL and Learn's server run as 999, not remapped): the same user ID can read their environment and files. The job's first step (*Sandbox*) fails if any of this leaks. Its second (*Leftovers*) stops whatever an earlier job left running in a job's working folder: the runner (forgejo-runner v13.1.0) kills a cancelled or timed-out step's process group and runs no later step, not even an `if: always()` one, and on 22 Oct 2026 a cancelled job still left its 4 GB analysis server behind; with one job at a time, any such process is a leftover. Flutter is pinned in the workflow (`FLUTTER_VERSION`): move it together with the local one. Every server package's tests get throwaway passwords from the environment (`SERVERPOD_…`, new for every job), since no `passwords.yaml` is ever committed; without them they exit with code 1 and no message (Serverpod's own template does the same). CI installs the Serverpod CLI at `SERVERPOD_VERSION` (which the root's tests hold to the lock's Serverpod), so the checks' generated-code step fails there too if the committed generated code of any server differs from what the generator makes, as it does on the laptop since 23 Oct 2026 (before, only CI compared, and a push went red for an endpoint's changed comment). So never run `dart format` over a server's `lib/` or `test/` as a whole: it reformats the generated code (`lib/src/generated/`, `serverpod_test_tools.dart`); format the files you wrote. CI runs on pushes only: a pull request from a fork would run its code on the production server.
|
|
- **A green `main` deploys learn.halkyon.eu**: this repository's `main`, whichever app or package a push changed, through `deploy/redeploy.sh` (Learn's `AGENTS.md`, *CI and deploys*, with the rules a deploy checks, the sidecars, the backups and the releases).
|