yavsc/CONTRIBUTING.md
Paul Schneider c645973b52
Some checks failed
Dotnet build and test / build (pull_request) Failing after 9m2s
no object
2026-08-21 20:33:30 +01:00

12 KiB

Contribuer à Yavsc

Pré-requis

  • .NET 10 SDK
  • PostgreSQL (≥ 14) — local ou via docker compose up (cf. docker-compose.yaml)
  • Node.js (uniquement pour la toolchain Avalonia Browser/WASM)
  • Pour Android : Android SDK + workload dotnet workload install android

Premier build

git clone https://github.com/pazof/yavsc.git
cd yavsc
dotnet restore
dotnet build

Pour exécuter Yavsc.Org en local :

cp src/Yavsc.Org/appsettings-org.json src/Yavsc.Org/appsettings-org.Development.json
$EDITOR src/Yavsc.Org/appsettings-org.Development.json   # renseigner Site.Authority, ConnectionStrings.YavscConnection, …
ASPNETCORE_ENVIRONMENT=Development dotnet run --project src/Yavsc.Org

Cf. doc/Architecture.md section "Paramétrage" pour le détail des variables à renseigner.

Tests

dotnet test

Les tests sont répartis en :

  • src/Yavsc.Org.Tests/ — tests d'intégration du front web (TestWebApplicationFactory<Program> + EF InMemory). Comprend les smoke tests par BC sous Smoke/ : AccountSmokeTests (GET /signin), BlogSmokeTests (GET /BlogSpot/Index). Chaque test couvre une bounded context DDD au sens de doc/ddd-exploration-2026-06-14.md : il démarre le host en mémoire via WebApplicationFactory<Program> et vérifie qu'une route publique de la BC répond en 2xx/3xx (ou 401/403 si elle exige une authentification). Cf. ROADMAP.md item « Tests d'intégration smoke par BC ».
  • src/PostIt.Tests/ — tests unitaires du client desktop PostIt.

Navigation (PostIt)

La navigation est centralisée dans App.PushPageAsync(ViewModelBase vm) (src/PostIt/PostIt/App.axaml.cs). Pour ouvrir un écran, un ViewModel (généralement dans une commande [RelayCommand]) appelle await ((App)App.Current!).PushPageAsync(targetVm).ConfigureAwait(true);. PushPageAsync résout la Control correspondante via le ViewLocator (un IDataTemplate enregistré dans Application.DataTemplates au boot), l'identifie comme Page, lui assigne le VM comme DataContext, et appelle NavRoot.PushAsync(page). Une garde anti-empilement compare par référence la nouvelle page au sommet courant de la stack pour éviter un push doublon.

Pour qu'une nouvelle page soit navigable, il faut deux enregistrements : la page dans le DI (AddTransient<TPage> ou AddSingleton<TPage>) et une case dans le switch de ViewLocator.Build. Si l'un manque, l'app affiche "No view for X" sans crash.

Règles :

  • On n'instancie jamais une View à la main depuis un ViewModel, on ne récupère jamais une View depuis la DI directement dans un ViewModel.
  • Le ViewModel qui déclenche la nav ne pousse pas lui-même la page ; il appelle App.PushPageAsync(vm) et laisse App orchestrer le PushAsync physique.
  • Le ViewModel qui déclenche la nav ne capture pas de référence à MainWindow ou NavigationPage. Il passe par App.Current (l'app Avalonia est un singleton).

Exemple canonique (depuis MainPageViewModel) :

[RelayCommand]
internal async Task OpenSettings()
{
    var settingsVm = ((App)App.Current!).ServiceProvider
        .GetRequiredService<Settings>();
    await ((App)App.Current!).PushPageAsync(settingsVm)
        .ConfigureAwait(true);
}

Cf. doc/architecture/postit.md pour la topologie complète (host de navigation, SessionStatusViewModel, signaux de cycle de vie vs nav utilisateur).

Conventions de code

Le repo applique .editorconfig (UTF-8, LF, indent_size = 4 en C#, 2 en Razor / XML / .axaml). Pas de formatter dédié — les EDI (Visual Studio, Rider) appliquent automatiquement les règles Roslyn

  • EditorConfig.

Quelques règles non capturées par .editorconfig :

  • using triés par groupe (System, puis packages, puis local). Pas de séparation visuelle entre groupes (cf. dotnet_separate_import_directive_groups = false).
  • Préférer les types BCL (int, string) aux types framework (Int32, String).
  • Préférer les expressions de pattern matching aux casts explicites.
  • Pas de object dans le code source applicatif. Types de retour, paramètres, champs, propriétés, variables locales : tout doit être typé statiquement. dynamic est interdit pour les mêmes raisons. Un cast en object est presque toujours le symptôme d'un contrat qu'on a laissé s'effriter (DTO, payload, handler) — refactore le contrat (record typé, DTO dédié, méthode dédiée) au lieu de shimer avec un cast.

Branches & commits

  • Trunk-based sur main. Pas de branche longue durée pour le moment ; on lande directement sur main via PR (quand le projet attire de l'attention) ou push direct (mode solo).

  • Un commit = un changement logique. Regrouper les fichiers qui touchent au même flux dans un seul commit (login + tests + doc = un commit), mais séparer les commits de reformulation d'historique (« refactor », « typo ») des commits de feature dans la mesure du possible.

  • Messages de commit en anglais, format :

    <scope>: <imperative summary>
    
    <body — what changed and why, not how>
    

    Le scope est le nom du sous-projet (postit, doc, readme, yavsc.org, yavsc.api, …). Pour les commits qui touchent plusieurs sous-projets, préférer un scope générique (readme:, doc:).

Découpage des projets

Cf. doc/architecture/decoupage-organisation.md.

qui couvre à la fois le build, la publication et les images runtime, plus un second Dockerfile minimal (Dockerfile.backend) utilisé uniquement par le workflow de publication de l'image de production.

Dockerfile Stages Construit par
Dockerfile build-env (default), publish-org/api/blogs, web-runtime, api-runtime, blogs-runtime docker compose build + .github/workflows/docker-publish-android.yml
Dockerfile.backend Idem limité à build-env + publish-org (suffisant pour publier l'image pazof/yavsc) .github/workflows/docker-publish-backend.yml

L'image de base est construite depuis le dépôt sibling dotnet-android-build-image (Debian 12 + .NET 10 SDK + Android SDK 36 + workload .NET Android). Elle est poussée sur Docker Hub sous le tag pazof/yavsc-build-env:debian12-dotnet10-android36-v1. Le tag est déclaré comme ARG BUILD_ENV_TAG au début du Dockerfile (et de Dockerfile.backend) — il faut le bumper en lockstep dans les deux fichiers et dans docker-compose.yaml (chaque bloc build.args.BUILD_ENV_TAG) quand l'image de base est reconstruite.

docker compose up

sudo docker compose up --build

Démarre 4 services : db (PostgreSQL 16), web (Yavsc.Org), api (Yavsc.Api), blogs (Yavsc.Blogs). Chaque service runtime pointe sur le stage correspondant du Dockerfile multi-stage via build.target. Les services runtime attendent le healthcheck pg_isready de db avant de démarrer.

Sur une machine vierge, db, api et blogs démarrent. Le service web (Yavsc.Org) échoue avec :

Production IdentityServer requires a signing certificate. Configure Kestrel:Endpoints:Https:Certificate:{Path,KeyPath}.

C'est attendu : IdentityServer8 en mode Production exige un cert HTTPS pour signer les tokens (cf. src/Yavsc.Org/Extensions/HostingExtensions.cs:374). Le critère de sortie Jalon 0 « docker compose up vert sur machine vierge » n'est donc pas « tout démarre sans rien » — c'est « tout démarre après la procédure d'installation qui monte un cert HTTPS valide, cf. HTTPS en production ci-dessous ». Le montage de /etc/letsencrypt (volume commenté par défaut dans docker-compose.yaml) est l'étape qui distingue une machine configurée d'une machine vierge.

appsettings-org.json

Le fichier de configuration de prod (src/Yavsc.Org/appsettings-org.json) n'est pas commité. Il est injecté dans chaque image runtime via un BuildKit secret mount — le fichier reste sur l'hôte, n'apparaît dans aucun layer :

secrets:
  yavsc_appsettings:
    file: ./src/Yavsc.Org/appsettings-org.json

Compose le passe automatiquement à docker build via le bloc build.secrets de chaque service runtime.

HTTPS en production

En dev local les services runtime sont forcés à HTTP seul par un bloc environment explicite dans docker-compose.yaml :

environment:
  ASPNETCORE_URLS: "http://+:5000"
  ASPNETCORE_HTTPS_PORT: ""

C'est nécessaire parce que appsettings-org.Development.json positionne Site.Authority = https://localhost:5001, ce qui pousse Kestrel à essayer de binder HTTPS même sans certificat disponible — et échoue proprement avec « Unable to configure HTTPS endpoint. No server certificate was specified ».

En production, sur chaque service runtime de docker-compose.yaml :

  1. Remplacer l'environment.ASPNETCORE_URLS par la forme double-bind http://+:5000;https://+:5001 (ou équivalent pour Api / Blogs).

  2. Décommenter le port HTTPS correspondant (5001 pour Org, 5003 pour Api, 5005 pour Blogs).

  3. Décommenter le volume /etc/letsencrypt:/etc/letsencrypt:ro — monter le répertoire Let's Encrypt de l'hôte en lecture seule pour que Kestrel accède aux fichiers .pem.

  4. Dans appsettings-org.json, ajouter un bloc Kestrel:Endpoints pointant vers les chemins du volume monté. Exemple :

    "Kestrel": {
      "Endpoints": {
        "Https": {
          "Url": "https://+:5001",
          "Certificate": {
            "Path": "/etc/letsencrypt/live/yavsc.example/fullchain.pem",
            "KeyPath": "/etc/letsencrypt/live/yavsc.example/privkey.pem"
          }
        }
      }
    }
    
  5. Régénérer l'image runtime (les appsettings sont baked dans l'image via BuildKit secret mount — cf. section appsettings-org.json).

Bumper l'image de build

Quand on bumpe Debian, .NET SDK ou Android SDK, reconstruire l'image de base :

cd ../dotnet-android-build-image
docker build -t pazof/yavsc-build-env:debian12-dotnet10-android36-v2 .
docker push pazof/yavsc-build-env:debian12-dotnet10-android36-v2

Puis bumper en lockstep dans :

  • Dockerfile (ARG BUILD_ENV_TAG en tête de fichier)
  • Dockerfile.backend (idem)
  • docker-compose.yaml (chaque bloc build.args.BUILD_ENV_TAG).

Vérifier un build isolé d'un stage runtime

docker build \
  --secret id=yavsc_appsettings,src=src/Yavsc.Org/appsettings-org.json \
  --target web-runtime \
  -t yavsc-org:dev .
docker run --rm -p 5000:5000 yavsc-org:dev

et une roadmap à jour dans ROADMAP.md. Toute modification de modèle doit être précédée d'une note DDD ; les BC (Conciliation, etc.) listés dans ces docs sont les cibles de conception.

Sessions DDD

Le repo tient un journal de design DDD sous doc/ddd-exploration-*.md et une roadmap à jour dans ROADMAP.md. Toute modification de modèle doit être précédée d'une note DDD ; les BC (Conciliation, etc.) listés dans ces docs sont les cibles de conception.

Sécurité

Cf. la section "🔒 Hygiène transverse (à tous les jalons)" de ROADMAP.md. En particulier : ne jamais committer de secret, utiliser dotnet user-secrets en développement et les variables d'environnement ASPNETCORE_* en production.

Questions

Ouvrir une issue GitHub, ou — pour les questions de design — démarrer une session DDD et la consigner dans doc/.