__init__.py 187 lignes · 6580 octets
"""
Module Dagger pour la documentation gitrust (girust-doc).

Conventions gitrust :
- `.dagger/` à la racine du dépôt = mode Power (pipeline CI custom).
- Gitrust détecte la présence du dossier et exécute `dagger call -m .dagger/ ci`.

Fonctions exposées (appelables via `dagger call`) :
- build             : construit un site mdBook pour UNE langue (par défaut fr).
- build-all         : construit les 6 langues avec landing page Accept-Language.
- lint              : markdownlint-cli2 sur tous les .md.
- validate-mermaid  : vérifie chaque .mmd via mmdc.
- ci                : pipeline complet (lint + validate + build-all).

Exemples :
    dagger call -m .dagger/ build --source=. --lang=en export --path=./dist
    dagger call -m .dagger/ build-all --source=. export --path=./dist
    dagger call -m .dagger/ ci --source=. export --path=./dist
"""

from typing import Annotated

import dagger
from dagger import DefaultPath, Doc, dag, function, object_type

LANGS = ["fr", "en", "de", "es", "pt", "it"]


@object_type
class GirustDoc:
    """Pipeline de build de la documentation gitrust (mdBook + i18n + Mermaid)."""

    @function
    def base(self) -> dagger.Container:
        """
        Image outillée réutilisée par toutes les autres fonctions
        (Rust + Node + gettext + mdBook + preprocessors + linters).
        """
        return (
            dag.container()
            .from_("rust:1.82-slim-bookworm")
            .with_exec([
                "bash", "-c",
                "apt-get update && "
                "apt-get install -y --no-install-recommends "
                "curl ca-certificates git gettext "
                "nodejs npm build-essential pkg-config libssl-dev && "
                "rm -rf /var/lib/apt/lists/*",
            ])
            .with_env_variable("CARGO_HOME", "/usr/local/cargo")
            .with_mounted_cache(
                "/usr/local/cargo/registry",
                dag.cache_volume("cargo-registry"),
            )
            .with_exec([
                "cargo", "install", "--locked",
                "mdbook", "mdbook-mermaid", "mdbook-i18n-helpers",
            ])
            .with_exec([
                "npm", "install", "-g",
                "markdownlint-cli2", "@mermaid-js/mermaid-cli",
            ])
        )

    def _sources(self, source: dagger.Directory) -> dagger.Container:
        """Prépare un conteneur avec les sources montées et mermaid installé."""
        return (
            self.base()
            .with_directory(
                "/src",
                source,
                exclude=["book/", "node_modules/", ".git/", ".dagger/"],
            )
            .with_workdir("/src")
            .with_exec(["mdbook-mermaid", "install", "."])
        )

    @function
    async def build(
        self,
        source: Annotated[dagger.Directory, DefaultPath("/")],
        lang: Annotated[str, Doc("Code langue : fr, en, de, es, pt, it")] = "fr",
    ) -> dagger.Directory:
        """Construit le site mdBook pour UNE langue. Retourne le dossier book/<lang>/."""
        return await (
            self._sources(source)
            .with_env_variable("MDBOOK_BOOK__LANGUAGE", lang)
            .with_exec(["mdbook", "build", "-d", f"book/{lang}"])
            .directory(f"/src/book/{lang}")
            .sync()
        )

    @function
    async def build_all(
        self,
        source: Annotated[dagger.Directory, DefaultPath("/")],
    ) -> dagger.Directory:
        """
        Construit les 6 langues et produit un dossier book/ avec une landing
        page racine qui redirige selon Accept-Language.
        """
        ctr = self._sources(source)

        for lang in LANGS:
            ctr = (
                ctr
                .with_env_variable("MDBOOK_BOOK__LANGUAGE", lang)
                .with_exec(["mdbook", "build", "-d", f"book/{lang}"])
            )

        ctr = ctr.with_new_file(
            "/src/book/index.html",
            """<!DOCTYPE html>
<html lang="fr"><head><meta charset="utf-8"><title>Documentation gitrust</title>
<script>
  const langs = ["fr","en","de","es","pt","it"];
  const pref = (navigator.language || "fr").slice(0,2).toLowerCase();
  location.replace((langs.includes(pref) ? pref : "fr") + "/index.html");
</script>
<meta http-equiv="refresh" content="0;url=fr/index.html"></head>
<body><p>Redirection vers <a href="fr/index.html">la documentation</a>…</p></body></html>""",
        )

        return await ctr.directory("/src/book").sync()

    @function
    async def lint(
        self,
        source: Annotated[dagger.Directory, DefaultPath("/")],
    ) -> str:
        """
        Exécute markdownlint-cli2 sur tous les .md du projet.
        Échoue avec code ≠ 0 si le lint trouve des violations.
        """
        return await (
            self.base()
            .with_directory(
                "/src", source,
                exclude=["book/", "node_modules/", ".git/", ".dagger/"],
            )
            .with_workdir("/src")
            .with_exec([
                "markdownlint-cli2",
                "**/*.md", "#!book/**", "#!node_modules/**", "#!.dagger/**",
            ])
            .stdout()
        )

    @function
    async def validate_mermaid(
        self,
        source: Annotated[dagger.Directory, DefaultPath("/")],
    ) -> str:
        """Vérifie que chaque fichier .mmd de diagrams/ est parsable par mmdc."""
        return await (
            self.base()
            .with_directory(
                "/src", source,
                include=["diagrams/**/*.mmd"],
            )
            .with_workdir("/src")
            .with_exec([
                "bash", "-c",
                "set -euo pipefail; "
                "for f in diagrams/*.mmd; do "
                "  echo \"→ $f\"; "
                "  mmdc -i \"$f\" -o \"/tmp/$(basename \"$f\" .mmd).svg\" --quiet; "
                "done; "
                "echo '✓ Tous les diagrammes Mermaid sont valides'",
            ])
            .stdout()
        )

    @function
    async def ci(
        self,
        source: Annotated[dagger.Directory, DefaultPath("/")],
    ) -> dagger.Directory:
        """
        Pipeline complet : lint + validate-mermaid (en parallèle) + build-all.
        Fonction appelée par gitrust-ci en mode Power.
        """
        # Dagger évalue paresseusement : les appels ci-dessous démarrent
        # en parallèle, et un échec propage l'erreur.
        await self.lint(source)
        await self.validate_mermaid(source)
        return await self.build_all(source)