Contributing
The repository holds a single copy of this document: both languages of this site point at the same text.
{% raw %}
🇫🇷 Ce document est en français ; un résumé en anglais suit chaque section clé. 🇬🇧 This document is in French, with an English summary after each key section.
Merci de votre intérêt. Crabster a terminé sa Phase 3 (plan de travail) : un modèle CDL génère un projet Rust qui compile, passe ses tests et sert une API CRUD complète, et crabster record ajoute un enregistrement à un projet existant. La Phase 4 — authentification et sécurité — est ouverte.
Par où commencer🔗
Avant toute contribution de code, lisez dans l'ordre :
- Vision et objectifs — les principes qui arbitrent les désaccords
- Architecture technique — les choix techniques et leurs alternatives écartées
- Plan de travail — la phase en cours et ce qui est hors périmètre
Une proposition qui contredit un principe de la vision ou un ADR sera discutée sur ce terrain-là. Ce n'est pas un refus de principe : les ADR peuvent être révisés, mais explicitement, par un nouvel ADR qui documente le changement — pas par accumulation de PR.
EN: Read the vision, architecture, and roadmap docs first. Proposals that contradict a documented principle or ADR get discussed on those terms. ADRs can be revised — explicitly, via a new ADR, not by accumulated PRs.
Prérequis🔗
- Rust stable (la toolchain est épinglée par
rust-toolchain.toml, rustup s'en occupe) git
Cela suffit pour ./check. Le palier --all demande davantage, et le tableau plus bas dit quel test réclame quoi et pourquoi : Docker, tsc, npm, Chrome, python3, kubectl, helm, et un minikube démarré avec son addon ingress pour le seul test qui déploie.
Boucle de développement🔗
./check # format, lint, docs, tests. Moins d'une minute à chaud.
./check --all # plus les projets générés, l'empaquetage et l'audit. Minutes.
./check --all avant chaque push, pas avant certains. C'est le seul garde-fou : le dépôt est privé, ses minutes GitHub Actions sont facturées et ne sont pas payées, donc .github/workflows/ci.yml ne se déclenche plus sur rien. Les jobs ne sont pas supprimés et ne sont pas cassés — seuls les déclencheurs push, pull_request et schedule ont été retirés, et le fichier dit comment les remettre.
Ce que ./check fait tourner, et pourquoi chaque étape existe :
| Étape | Ce qu'elle seule attrape |
|---|---|
cargo fmt --check | — |
cargo clippy --workspace --all-targets | RUSTFLAGS: -D warnings est actif : un avertissement fait échouer |
cargo doc --workspace --no-deps | Les deux crates bibliothèque portent #![warn(missing_docs)], et c'est lancé sous RUSTDOCFLAGS: -D warnings — un item public non documenté échoue là et nulle part ailleurs |
cargo test --workspace | — |
--all : cargo test -- --ignored | Compile des projets générés entiers et les exécute. C'est le seul endroit où les templates sont prouvés produire du Rust valide. Docker et tsc sont requis : quatre de ces tests démarrent un vrai PostgreSQL et un vrai MySQL et y font tourner les migrations générées, un autre compile le client TypeScript généré. Ils échouent plutôt que de se sauter — un test qui passe en ne s'exécutant pas est précisément la couverture que ce palier existe pour cesser de feindre. npm i -g typescript pour le second. Un troisième démarre Keycloak et rejoue en python3 tout le flux de connexion d'un navigateur. Un quatrième, un cinquième et un sixième construisent les blueprints React, Vue et Angular (npm) et rendent leur page dans Chrome en mode headless — un bundle qui se construit ne dit pas qu'une application démarre, ce qu'un polyfill manquant a prouvé sur Angular. Un septième génère un projet MongoDB et exécute sa propre suite contre une base qu'il démarre. Un huitième fait de même avec Meilisearch. Un neuvième rend une surcouche Kustomize avec kubectl, un dixième passe le chart au crible de helm — deux binaires à installer plutôt qu'à démarrer, là où les autres tirent leurs conteneurs. Le onzième est le seul du dépôt à demander quelque chose de déjà en route : il déploie un système entier sur minikube et le fait répondre par son Ingress, ce qui est la definition of done de la phase 17 exécutée plutôt que retenue. Il est donc le seul que ./check --all saute en le disant plutôt que d'échouer : Docker absent est une machine qui ne l'a pas, un cluster arrêté est un état qu'on démarre — et un cluster arrêté a bloqué un push dont le diff ne touchait pas Kubernetes. minikube start && minikube addons enable ingress et il tourne ; CRABSTER_K8S=1 le force sur le cluster que kubectl vise. Il retire son namespace même s'il explose au milieu, et prend une quarantaine de secondes une fois les trois images en cache — plusieurs minutes la première fois, puisqu'il les construit. À part lui, aucun ne demande qu'on lui fournisse quoi que ce soit : une suite générée démarre les serveurs dont elle a besoin, et TEST_DATABASE_URL ou TEST_SEARCH_ADDRESS ne sont là que pour lui en imposer un autre |
--all : cargo package --workspace | Reconstruit chaque crate depuis son propre tarball : un fichier hors du paquet ne se voit que là. Sauté si l'arbre n'est pas propre, cargo package lisant ce que git suit |
--all : cargo audit | Sauté s'il n'est pas installé. Un avis est accepté, et un seul : RUSTSEC-2023-0071, la faille Marvin de rsa, moyenne et sans correctif. Elle arrive par sqlx-mysql, dont crabster introspect a besoin pour lire un schéma MySQL. Ce crate n'emploie rsa que dans connection/auth.rs, et seulement pour chiffrer un mot de passe avec la clé publique du serveur ; Marvin récupère une clé privée par le temps de déchiffrement. Ce client n'en détient aucune et ne déchiffre rien. Le raisonnement est écrit dans check, avec les deux commandes qui permettent de le revérifier plutôt que de le croire |
./check --all efface target/e2e en terminant, et ce n'est pas une
optimisation mais l'inverse : chaque passage suivant recompile des projets
générés entiers depuis rien.
La raison est le disque. target/e2e est le cache partagé du palier lourd, rien
n'y évince jamais rien — chaque projet généré y est un crate différent — et il
grossit d'une vingtaine de gigaoctets par passage complet. Time Machine prend un
instantané local toutes les heures, un passage complet dure plus longtemps que
cela : un répertoire laissé debout est donc un répertoire qu'un instantané
épingle, et l'effacer après coup ne libère alors plus rien. L'espace ne revient
qu'à la purge des instantanés. C'est ainsi que cette machine a atteint 47 Go de
cache, rempli son disque, et laissé le disque virtuel de Docker en lecture seule
sans qu'aucun prune puisse le récupérer.
L'effacer dans l'heure est ce qui le tient hors des instantanés. Le coût est une exécution lente à chaque fois, payée sciemment.
CRABSTER_KEEP_E2E=1 ./check --all le conserve, pour une session passée à le
lancer en boucle.
Ce que la machine ne peut pas remplacer : trois systèmes d'exploitation, et un cache vide. Gardez-le en tête sur tout ce qui touche aux chemins ou aux dépendances.
Si vous touchez aux templates🔗
Trois vérifications, dont aucune ne tourne dans un cargo test ordinaire :
cargo test --workspace -- --ignored
cargo run -p crabster-cli -- import-cdl examples/shop.cdl --path /tmp/fmt-shop
(cd /tmp/fmt-shop && cargo fmt --all --check)
(cd /tmp/fmt-shop && RUSTFLAGS= cargo +"$(grep -m1 '^rust-version' Cargo.toml | cut -d'"' -f2)" check --all-targets)
La première est indispensable et facile à oublier. Le test qui génère un projet, le compile et interroge son /health est marqué #[ignore] parce qu'il compile un projet entier — donc cargo test --workspace ne le lance pas. C'est pourtant le seul test qui prouve que les templates produisent du Rust valide : sans lui, vous pouvez casser un template et voir toute la suite au vert.
EN: CI runs the same commands, with warnings denied. Note that cargo test --workspace skips the end-to-end tests, which are #[ignore]d because they compile whole generated projects — yet they are the only tests proving the templates produce valid Rust. Whenever you touch the templates, run all three commands above: the ignored tests, a rustfmt check on a generated project, and a compile of it on the Rust version it declares (which is not the generator's).
Contre PostgreSQL et MySQL, en local🔗
Les trois vérifications ci-dessus compilent ; elles n'exécutent rien contre les
deux autres bases. C'est là que se cachent les fautes qui ne se voient pas
autrement — un ordre de migrations que SQLite accepte et que les deux autres
refusent, un type qui perd ses décimales, une migration supprimée au second
crabster apply. Chacune de ces trois-là a été trouvée en exécutant, jamais en
compilant.
MySQL vient du docker-compose.yml que le projet généré livre lui-même —
donc si les identifiants et le .env.example cessent de s'accorder, ça se voit
ici :
crabster import-cdl examples/shop.cdl --path /tmp/my-shop --database mysql
cd /tmp/my-shop && cp .env.example .env
docker compose up -d --wait db
TEST_DATABASE_URL="$(grep '^DATABASE_URL=' .env | cut -d= -f2-)" RUSTFLAGS= cargo test
RUSTFLAGS= cargo run
PostgreSQL, sans Docker, avec un cluster à soi :
PG=$(brew --prefix postgresql@14)/bin
$PG/initdb -D /tmp/pgdata -U postgres --auth=trust -E UTF8 --locale=C
mkdir -p /tmp/pgsock
$PG/pg_ctl -D /tmp/pgdata -o "-p 5439 -k /tmp/pgsock -h 127.0.0.1" -l /tmp/pg.log start
$PG/psql -h 127.0.0.1 -p 5439 -U postgres -c "CREATE DATABASE shop" \
-c "ALTER USER postgres PASSWORD 'postgres'"
puis la même chose que pour MySQL, avec --database postgres et un .env
pointant sur le port choisi. À la fin :
$PG/pg_ctl -D /tmp/pgdata stop et docker compose down -v.
Et faites-le deux fois. Le parcours qui compte le plus est celui qui
enchaîne deux crabster apply : le premier passe toujours, et c'est le second
qui a montré que les migrations qu'il écrit n'étaient pas gelées.
EN: the three checks above compile; they run nothing against the other two
databases, and that is where the faults live that nothing else shows — a
migration order SQLite accepts and the other two refuse, a type that loses its
decimals, a migration deleted by the second crabster apply. MySQL comes from
the generated project's own Compose file; PostgreSQL can be a cluster of your
own on a spare port. Run the walkthrough twice: the second apply is the one
that finds things.
Style de code🔗
rustfmtpar défaut, sans configuration maison. Le formatage n'est pas un sujet de débat.- Pas de
unwrap()/expect()dans le code de production ; en tests, c'est acceptable. - Erreurs :
thiserrordans les crates bibliothèque,anyhowdans la CLI. - Commentaires : expliquer le pourquoi, pas le quoi. Un commentaire qui paraphrase le code sera signalé en revue.
Une règle spécifique aux générateurs🔗
Le code Rust des templates (crates/crabster-codegen/templates/) est du code destiné à être copié chez l'utilisateur. Il est donc jugé plus sévèrement que le code du générateur lui-même : il doit être lisible par quelqu'un qui découvre Rust, sans astuce ni abstraction gratuite. Un générateur qui produit du code que son utilisateur ne comprend pas a échoué.
Corollaire structurant pour la V2 : dans les templates, séparez nettement le code purement généré du code destiné à être édité par l'utilisateur. C'est ce qui rendra la fusion incrémentale (Phase 11) praticable. Ne pas le faire maintenant coûtera très cher plus tard.
EN: Template code is held to a higher standard than generator code — it must be readable by a Rust newcomer. And keep purely-generated code clearly separated from user-editable code: V2's incremental merging depends on it.
Commits et Pull Requests🔗
- Messages de commit à l'impératif, en anglais :
add CDL enum parsing, pasaddedniadds. - Une PR = un sujet. Les refactorings de confort vont dans une PR séparée du changement fonctionnel.
- Une PR qui touche au comportement du générateur doit inclure ou mettre à jour un test.
- Indiquez la phase du plan de travail concernée dans la description.
Politique de versionnement🔗
Crabster verse deux artefacts au cycle de vie distinct, et c'est délibéré :
| Artefact | Versionnement | Ce qu'une rupture signifie |
|---|---|---|
Le générateur (crates crabster-*) | SemVer classique | La CLI, ses options, ou l'API publique des crates changent de façon incompatible |
Les templates (crates/crabster-codegen/templates/) | Version propre, suivie dans les projets générés | Le code généré change de façon qui casse un projet existant lors d'une mise à jour |
Pourquoi les séparer : ajouter une sous-commande à la CLI n'affecte en rien un projet déjà généré, tandis que restructurer un template peut casser la mise à jour de milliers de projets sans que la CLI ait changé d'un octet. Les confondre rendrait le numéro de version ininterprétable pour l'utilisateur.
Concrètement, depuis la Phase 3, chaque projet généré porte les deux numéros dans son .crabster/project.toml : generator, et templates. C'est le second qui compte pour crabster record, qui refuse un projet dont la version de templates n'est pas la sienne — rendre à nouveau des templates qui ont bougé ferait passer tout le projet pour modifié à la main. Un correctif de CLI qui ne touche à aucun template laisse donc les projets existants intacts, ce qui est précisément la raison d'être de la séparation.
Franchir cette version plutôt que la refuser, c'est crabster upgrade : il régénère depuis .crabster/, réécrit ce que personne n'a touché, et fusionne le reste avec ce que son propriétaire a écrit. Donc : si vous touchez à un template, montez TEMPLATES_VERSION — c'est ce numéro qui dit d'où vient un projet, et c'est ce qui donne à upgrade quelque chose à franchir.
Avant la 1.0, conformément à SemVer, les versions 0.x peuvent introduire des ruptures sur une version mineure. Elles seront listées dans le CHANGELOG.
EN: The generator and the templates are versioned separately, because adding a CLI subcommand doesn't affect an already-generated project, whereas restructuring a template can break upgrades for thousands of projects without the CLI changing at all. Since Phase 3 both numbers are stamped into every generated project's .crabster/project.toml; the templates one is what crabster record refuses on. Crossing it rather than refusing it is crabster upgrade. So: if you touch a template, bump TEMPLATES_VERSION — it is what says where a project came from, and what gives upgrade something to cross.
Ce qu'un blueprint peut casser🔗
La version de templates sert deux publics, et le second est plus facile à
oublier. Un projet généré est rattrapé par crabster upgrade ; un blueprint,
lui, vit dans un autre dépôt, appartient à quelqu'un d'autre, et n'apprend
qu'une chose a bougé que le jour où il ne génère plus.
Ce contre quoi un blueprint est écrit tient en cinq sortes de noms : les modules qu'il peut requérir, les fichiers qu'ils engendrent, les slots à l'intérieur de ces fichiers, les régions protégées, et les variables de contexte. Aucun n'est déclaré nulle part comme une promesse — ce sont simplement ceux que les templates contiennent.
Ils le sont désormais. crates/crabster-codegen/blueprint-contract.txt
les énumère, et un test compare cet instantané aux templates tels qu'ils sont :
| Ce que vous faites | Ce qu'il faut |
|---|---|
| Ajouter un slot, un fichier, un module, une variable | Rien de cassé. Ré-enregistrez l'instantané ; un correctif suffit |
| Renommer ou supprimer l'un d'eux | Rupture pour les blueprints. Montez TEMPLATES_VERSION sur une mineure, dites ce qui a bougé dans son historique, et ré-enregistrez |
| Changer ce qu'un template engendre sans toucher à ces noms | Rupture éventuelle pour les projets, pas pour les blueprints. La règle ci-dessus s'applique telle quelle |
cargo run -p crabster-cli -- blueprint contract > crates/crabster-codegen/blueprint-contract.txt
Le test ne juge pas le changement — avant la 1.0 une mineure a le droit de tout casser. Il le rend visible au moment où on le fait, plutôt qu'à la génération de quelqu'un d'autre.
Côté auteur de blueprint, crabster blueprint check <dir> répond à la même
question dans l'autre sens : est-ce que ce blueprint nomme encore des choses qui
existent, sans rien générer pour le savoir.
EN: The templates version serves two audiences, and the second is easier to
forget: a generated project is caught up by crabster upgrade, while a
blueprint lives in somebody else's repository and learns that something moved
the day it stops generating. blueprint-contract.txt records every name a
blueprint can be written against and a test compares it against the templates.
Adding is a patch; renaming or removing is a minor bump and a CHANGELOG entry.
crabster blueprint check asks the same question from the author's side.
Licence des contributions🔗
En soumettant une contribution, vous acceptez qu'elle soit distribuée sous la double licence MIT OR Apache-2.0 du projet, sans conditions supplémentaires. Voir LICENSE.md.
Notez en particulier la clause sur le code généré : il appartient à l'utilisateur du générateur. Toute contribution aux templates est faite en connaissance de ce fait.
EN: Contributions are dual-licensed MIT OR Apache-2.0. Note the generated-code clause in LICENSE.md: generated output belongs to the user.
Signaler un problème de sécurité🔗
Ne pas ouvrir d'issue publique. Passez par le signalement privé de GitHub — Security → Report a vulnerability — décrit dans SECURITY.md, qui dit aussi ce qui relève du code généré et ce qui relève du générateur : les deux n'ont pas la même gravité.
EN: Do not open public issues for security problems. Use GitHub's private vulnerability reporting, described in SECURITY.md.
{% endraw %}