Environnement de test local
Une instance Nextcloud jetable avec l'ExApp branchée dessus, en une commande.
make up
Puis http://localhost:8080 — identifiants admin / admin.
L'entrée « Organisateur Familial » apparaît dans la barre de navigation.
Un second compte, alice / alice-dev-password, est créé sans aucune famille. Deux
comportements ne s'observent que depuis un tel compte : l'assistant de première
utilisation, et le 404 opposé à un non-membre. admin a une famille dès le premier essai
et n'en a plus jamais zéro.
L'assistant de bienvenue de Nextcloud (firstrunwizard) est désactivé : son carrousel
modal se posait par-dessus l'application et interceptait les clics.
Ce que ça monte
navigateur ──▶ nextcloud:80 (publié sur localhost:8080)
│ proxy AppAPI
▼
exapp:9030 ──▶ nextcloud:80 (appels OCS)
│
└─▶ db:5432 (PostgreSQL, pour Nextcloud)
| Service | Image | Rôle |
|---|---|---|
db |
postgres:16-alpine |
Base de Nextcloud |
nextcloud |
nextcloud:34-apache |
L'instance. AppAPI est fourni d'office depuis NC 30.0.1 |
exapp |
dev/Dockerfile.dev |
Le backend, en rechargement à chaud (tsx watch) |
Le mode de déploiement est manual-install : l'ExApp tourne dans son propre conteneur et on
l'enregistre à la main. Pas de socket Docker exposé à Nextcloud, pas de HaRP. C'est le chemin le
plus léger pour boucler vite ; la validation HaRP est un exercice séparé (voir plus bas).
Boucle de développement
| Ce que tu changes | Ce qu'il faut faire |
|---|---|
Backend (ex_app/lib/src/) |
Rien — tsx watch recharge tout seul |
Frontend (ex_app/src/) |
make reload |
appinfo/info.xml (routes, env vars) |
make reload |
package.json du backend |
docker compose up -d --build exapp |
Commandes utiles :
make logs-exapp # logs du backend, en continu
make logs-nc # logs de Nextcloud
make occ ARGS='app_api:app:list'
make shell # shell dans le conteneur ExApp
make stop # arrêt, données conservées
make reset # tout supprimer, volumes compris
Validate application behavior through the AppAPI proxy. Direct access to port 9030 is useful only for process-level diagnostics such as /heartbeat; it bypasses the proxy and cannot validate browser access, route declarations, caching or language forwarding.
curl -u admin:admin http://localhost:8080/apps/app_api/proxy/organisateur_familial/api/hello
Use the Playwright suite for browser behavior. For repeated requests and test seeding, use the app-password setup described below rather than the account password.
Demo data
make seed
Gives admin a family named « Famille Démo » with lists, tasks, recipes, a week of meals
and a few events, every date computed from today. Re-running it deletes that family and
builds it again, so the dates move back to around today; other families of admin are left
alone.
It goes through the application, as the e2e suite does: it makes sure the service account
exists and is configured (with the suite's password, so the suite can still log in as it),
creates the family through the API, deletes the two empty starter lists (D41), then
imports a document in the export format (D36, D37) built by dev/seed-data.mjs. Adding
data means adding to that document.
Nothing runs it but you. make up does not call it, CI does not call it, and the
production image never copies dev/. The e2e suite deletes it, like every family of
admin (D29) — run make seed again after a run.
Traductions
Les catalogues sont des fichiers générés et versionnés (D38). Après avoir ajouté ou reformulé une phrase visible par un membre :
make l10n-build # extrait les sources, régénère ex_app/l10n/{fr.js,fr.json}
make l10n-check # régénère dans un temporaire, compare, et vérifie la couverture
l10n-check refuse une source sans français, une entrée vide ou fuzzy, un message
compté auquel il manque une forme, et une traduction qui a perdu un {placeholder} ou son
%n. Il a besoin de Docker — l'outil officiel est un phar PHP épinglé, exécuté dans le
conteneur dev/Dockerfile.l10n — et c'est pour cela qu'il forme un job de CI à part, sur
le runner hôte.
Deux copies existent sur la stack locale, et make reload s'occupe des deux :
- le backend lit
ex_app/l10n/, monté en lecture seule sur/assets/l10n; - Nextcloud lit
<writable-apps>/organisateur_familial/l10n/, oùdev/setup.shinstalle les catalogues — une installation manuelle n'est jamais dépaquetée par AppAPI, donc rien ne les y met tout seul. Ne jamais y copierappinfo/info.xml: Nextcloud prendrait le dossier pour une app PHP.
Le script que Nextcloud injecte pour le navigateur ne porte pas de paramètre de version : après une mise à jour des catalogues, un navigateur peut servir l'ancien depuis son cache jusqu'à expiration. Un rechargement forcé le règle.
La suite end-to-end lit du français et l'impose (imposeFrench dans
e2e/tests/instance.ts, plus le locale de Playwright) ; tests/i18n.spec.ts tient
l'autre bout avec un compte anglais.
Comment l'enregistrement fonctionne
dev/setup.sh est idempotent et fait trois choses :
- active
app_api; - enregistre un deploy daemon
manual_installdont l'hôte estexapp— le nom de service compose, c'est ainsi que Nextcloud atteint le conteneur ; - enregistre l'ExApp avec
--json-info.
Le JSON est généré depuis appinfo/info.xml par dev/json-info.mjs, pour ne pas maintenir deux
déclarations de routes. Deux raisons de ne pas passer --info-xml directement :
- AppAPI générerait un secret et un port aléatoires, alors que le conteneur a déjà un
APP_SECRETfigé — tout retournerait 401 ; - en
--json-info,access_leveldoit être un entier (PUBLIC=0,USER=1,ADMIN=2). La conversion depuis le nom n'a lieu que dans la branche--info-xmld'AppAPI. Le générateur s'en charge.
Réenregistrer est la façon normale de rejouer /init et /enabled contre le nouveau code.
Pièges rencontrés
Ne pas rm -rf ex_app/src/dist pendant que la stack tourne. Compose bind-monte
ex_app/src/dist/js ; supprimer le répertoire laisse le montage sur un inode mort et l'ExApp
répond 404 sur ses assets jusqu'à un docker compose up -d --force-recreate exapp. make clean
vide le contenu sans supprimer les répertoires.
Le bundle frontend doit s'appeler .js, pas .mjs. AppAPI enregistre un script fictif puis
réécrit le src en concaténant .js en dur au chemin enregistré. Corollaire : le bundle est chargé
en <script> classique, sans type="module" — il doit rester auto-suffisant (IIFE). C'est configuré
dans ex_app/src/vite.config.js.
Un daemon ne peut pas être réenregistré tant qu'il a des ExApps attachées. setup.sh l'ignore
et continue, c'est sans conséquence.
Valider HaRP
L'environnement ci-dessus n'exerce pas le chemin HaRP (socket Unix, FRP), qui est pourtant celui de la production sur NC 32+. Le backend gère les deux modes et le mode socket a été vérifié en local hors Docker, mais le tunnel FRP complet reste à éprouver.
Pour le faire, il faut ajouter le conteneur HaRP à la stack et enregistrer un daemon
docker-install --harp, ce qui implique de laisser AppAPI déployer l'image de production plutôt que
d'utiliser manual-install. À traiter quand la question D3 (polling vs WebSocket) sera tranchée —
voir Architecture.
Continuous integration runs on the same kind of stack
Written in English per the language rule in
AGENTS.md.
CI is a forgejo-runner on the machine that hosts the forge. It brings the stack of this
page up for real — make up, dev/setup.sh, the whole thing — so everything above applies
to it, plus what follows. See Workflow for what the two jobs run.
Its labels are mapped by hand, and only one is a container:
labels:
- self-hosted:host
- ubuntu-latest:host
- linux_arm64:docker://node:22-bookworm
ubuntu-latest is host execution here, not a container — the name is misleading and cost a
wrong assumption once. checks therefore runs on linux_arm64, which isolates it from
whatever the machine has installed; the end-to-end job runs on self-hosted, and must.
The e2e suite has to run on the host, because it drives docker compose itself.
e2e/tests/instance.ts shells out to docker compose exec from inside the Playwright
process, with the repository as its working directory: that is how psql() and occ()
reach the stack. So Nextcloud cannot be a workflow services: — the tests would not find
it — and a containerised job would need the Docker socket, the CLI, and the checkout at
the same path inside and out, since the bind mounts in compose.yaml resolve on the host.
The last condition fails silently: an empty directory mounted over the sources raises
nothing, it just produces a suite testing a bundle that is not there.
A containerised job cannot reach the forge without help. Measured three ways: from the
default bridge to the public address, fine; from a user-defined network — which is what the
runner creates per job — refused in 200 ms; from a user-defined network to the host gateway,
fine. Hence, in the runner's config.yml:
container:
options: --add-host=git.lozach.eu:host-gateway
Without it actions/checkout fails before anything else runs.
The runner's systemd unit is hardened, and two of its options reach into the jobs.
SystemCallFilter= has no SystemCallErrorNumber=, so a filtered call kills the process
rather than failing: npm ci died instantly on Bad system call, which is SIGSYS, until a
drop-in set SystemCallErrorNumber=EPERM. Which set was responsible was never identified,
and the drop-in makes it moot. And ProtectSystem=full keeps /usr read-only, so a job
cannot apt-get: Chromium's system libraries are installed once on the host by hand, and
the workflow calls playwright install chromium without --with-deps.
Two things the workflows deliberately do not use. No cache: npm — the runner's cache
server announces itself on an address a job network cannot reach, which cost one run 4 min 38
of timeout for a cache that never restored. And no actions/upload-artifact, whose v4
refuses any server that is not github.com; forgejo/upload-artifact@v4 is the fork without
that check.
The machine is not reliable, and this is unresolved. One run had Chromium's renderers
collapse progressively — 8 tests passed, 27 failed on Page crashed — and a cleanup and a
reboot made it go away without the cause ever being found. Another timed out a single test
on a click whose element was visible, enabled and stable. Vitest's worker pool is capped at
maxWorkers: '50%' for the same reason: an unconstrained container is told about every core
of the host, including the ones already serving the forge, and starving workers blew a 5 s
test timeout on assertions that take under a millisecond. Expect the suite to go red
occasionally without the code being at fault, and check a red run against a second one
before believing it.
Repartir de zéro
make reset && make up
Supprime les trois volumes (instance Nextcloud, base, données de l'ExApp) et reconstruit tout. Compter deux à trois minutes pour la réinstallation de Nextcloud.