Si votre pipeline de build ne peut pas s’exécuter sans que vous ouvriez un terminal et tapiez make tangle, vous n’avez pas de programme littéraire. Vous avez un journal intime avec un compilateur attaché.

Tout l’intérêt de la programmation littéraire est que la prose et le code partagent une source unique de vérité. Le fichier Markdown est l’artefact. Tout le reste — le source exécutable, la documentation rendue, les fichiers de test — est dérivé. Les artefacts dérivés appartiennent à CI, pas à votre mémoire de travail.

La question n’est pas de savoir si CI peut faire du weave et du tangle. Bien sûr que oui. Toute machine qui exécute Python et pandoc peut le faire. La question est de savoir si la structure de votre dépôt permet à CI de posséder les fichiers dérivés, ou si vous faites encore semblant que les fichiers .py sont du code source alors qu’ils sont en réalité des produits de compilation.

Ce que font réellement weave et tangle

Donald Knuth a appelé les deux opérations weave et tangle, et les noms sont assez confus pour que les gens évitent complètement le sujet.

tangle prend le document narratif — le fichier .md ou .w — et extrait les blocs de code dans l’ordre attendu par le compilateur. Il produit les fichiers source exécutables. weave fait l’inverse. Il prend le même document et produit la documentation lisible par l’homme, avec du code pretty-printed, des références croisées et une table des matières.

Les deux sont des transformations déterministes. Elles prennent une entrée et produisent des sorties cohérentes. C’est la définition de quelque chose que CI devrait gérer.

Pourquoi la plupart des équipes construisent la pipeline à l’envers

Le workflow typique de programmation littéraire ressemble à ceci. Vous écrivez algorithm.md. Vous exécutez un script local pour extraire algorithm.py. Vous exécutez les tests contre algorithm.py. Vous commitez à la fois algorithm.md et algorithm.py dans Git. Puis vous ouvrez une pull request.

C’est déjà cassé. Vous avez deux copies de la même logique dans le contrôle de version. Si un reviewer suggère une modification à algorithm.py, vous devez vous souvenir de la rétroporter vers algorithm.md. Si vous modifiez algorithm.md et oubliez de re-tangler, le fichier .py commité est obsolète. La source unique de vérité est une fiction.

Le workflow correct est plus simple. Vous ne commitez que le fichier .md. CI exécute tangle pour produire le fichier .py, puis exécute la suite de tests contre celui-ci. Si les tests passent, CI exécute éventuellement weave pour publier la documentation. Le fichier .py ne touche jamais le contrôle de version. C’est un produit de compilation, comme un fichier .o ou une image Docker.

Une pipeline CI fonctionnelle pour Markdown-vers-Python

Voici un workflow GitHub Actions complet qui traite le fichier Markdown comme la seule source de vérité. Enregistrez-le sous .github/workflows/literate.yml :

name: Tangle and Test

on:
  push:
    paths:
      - "**/*.md"
  pull_request:
    paths:
      - "**/*.md"

jobs:
  tangle:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"

      - name: Tangle code from Markdown
        run: python scripts/tangle.py src/*.md --out build/

      - name: Run tests on extracted code
        run: python -m pytest build/ -v

      - name: Upload generated source as artifact
        uses: actions/upload-artifact@v4
        with:
          name: generated-source
          path: build/

Le filtre paths est important. Ce workflow ne s’exécute que lorsqu’un fichier Markdown change, car Markdown est la seule entrée qui compte.

Le script tangle.py est le même extracteur de trente lignes que vous exécuteriez localement. Voici une version qui gère plusieurs fichiers et un répertoire de sortie :

import argparse
import re
import sys
from pathlib import Path


def tangle(md_path: Path, out_dir: Path) -> None:
    content = md_path.read_text()
    files: dict[str, list[str]] = {}

    for block in re.findall(r"```python\n(.*?)```", content, re.DOTALL):
        lines = block.strip().split("\n")

        if lines and lines[0].startswith("# "):
            filename = lines[0][2:].strip()
            code = lines[1:]
        else:
            filename = "module.py"
            code = lines

        files.setdefault(filename, []).extend(code)

    out_dir.mkdir(parents=True, exist_ok=True)
    for filename, code_lines in files.items():
        (out_dir / filename).write_text("\n".join(code_lines) + "\n")


def main() -> None:
    parser = argparse.ArgumentParser(description="Extract code from Markdown files.")
    parser.add_argument("inputs", nargs="+", type=Path, help="Markdown files to process")
    parser.add_argument("--out", type=Path, required=True, help="Output directory")
    args = parser.parse_args()

    for md in args.inputs:
        tangle(md, args.out)


if __name__ == "__main__":
    main()

Exécutez-le localement pour vérifier :

$ python scripts/tangle.py src/calculator.md --out build/
$ python -m pytest build/ -v

Le job CI fait exactement la même chose. La seule différence est que CI l’exécute dans un environnement propre, donc un fichier .py obsolète caché dans votre répertoire de travail ne peut pas tromper la suite de tests.

Tisser la documentation dans la même pipeline

tangle produit du code. weave produit de la documentation. Les deux peuvent s’exécuter dans CI.

Pour weave, vous avez besoin d’un outil qui convertit Markdown en un format présentable. Pandoc est le choix ennuyeux mais fiable. Si vous voulez des références croisées, de la coloration syntaxique et une table des matières, un petit modèle vous y mène :

      - name: Weave documentation
        run: |
          mkdir -p docs/output
          pandoc src/*.md \
            --from markdown \
            --to html5 \
            --standalone \
            --toc \
            --highlight-style=tango \
            --output docs/output/index.html

      - name: Publish to GitHub Pages
        uses: actions/upload-pages-artifact@v3
        with:
          path: docs/output/

Le HTML rendu est également un artefact dérivé. Vous pouvez le publier sur GitHub Pages, Netlify ou un bucket S3 à chaque merge sur main. Le fichier Markdown reste canonique. Le HTML est ce que les lecteurs voient.

Le compromis entre commit et générer

Certaines équipes s’y opposent. Elles veulent les fichiers .py dans Git parce que cela permet de faire git grep et parce que l’interface de code review de GitHub comprend mieux la syntaxe Python que Markdown.

Ce sont de vrais problèmes, mais ce sont des problèmes d’outillage, pas d’architecture. Commiter du code généré parce que votre outil de code review est mauvais, c’est comme commiter node_modules parce que votre gestionnaire de paquets est lent. Ça marche, mais ça accumule de la dette.

Si vous avez absolument besoin des fichiers .py dans le dépôt, générez-les dans CI et commitez-les avec un compte de bot. Voici le modèle :

  1. Un développeur pousse une modification vers algorithm.md.
  2. CI exécute tangle.py et produit algorithm.py.
  3. Si le code extrait diffère de ce qui est dans main, CI ouvre une pull request avec le fichier .py mis à jour.
  4. Un humain révise le diff généré et le fusionne.

Cela maintient les fichiers générés dans le contrôle de version sans les laisser diverger. Le bot devient la source unique de vérité pour la sortie générée, et le bot ne change que ce que le fichier Markdown exige.

Quand l’automatisation CI en vaut la peine, et quand non

Le weave et tangle automatisés brillent lorsque la prose est longue, le code est complexe, et plusieurs personnes modifient le document. Le code de recherche, la documentation de bibliothèques et les implémentations d’algorithmes bénéficient tous d’un fichier Markdown canonique que CI valide à chaque push.

Ça ne vaut pas le coup pour les petits scripts ou le code d’application lourd en boilerplate. Si votre fichier Markdown est deux paragraphes et une fonction de vingt lignes, le surcoût CI, les minutes de runner et le coût du modèle mental dépassent le problème de synchronisation que vous résolvez. Écrivez simplement une bonne docstring.

L’autre cas où cela échoue est lorsque votre tooling de langage considère les fichiers .py comme primaires. Les débogueurs, les profileurs et les outils de couverture veulent généralement pointer vers le code généré, pas vers le Markdown. Vous pouvez contourner cela en générant des fichiers localement avant de déboguer, mais c’est de la friction. Si votre team vit dans un débogueur, la programmation littéraire pourrait ne pas convenir quelle que soit la qualité de votre pipeline CI.

FAQ

Dois-je commiter les fichiers .py générés ou les .gitignore ?

Les .gitignore. S’ils sont générés de manière déterministe à partir de Markdown, ce sont des artefacts de compilation. La seule exception est si un système externe, comme un registre de paquets, ne peut pas exécuter votre étape de tangle. Dans ce cas, utilisez le modèle de commit par bot décrit ci-dessus.

Et si mon document littéraire mélange plusieurs langages ?

Le script tangle.py est indépendant du langage. Utilisez # filename.rs pour les blocs Rust, # filename.go pour les blocs Go, et ainsi de suite. CI exécute le même script pour chaque langage. Vous avez juste besoin d’une étape de test par langage dans le workflow.

Est-ce que ça marche avec des chunks de code nommés comme le WEB de Knuth ?

Des outils modernes comme Entangled supportent les chunks nommés et le suivi de dépendances. Si vous avez besoin de ce niveau d’expressivité, utilisez Entangled dans CI au lieu d’un script personnalisé. La structure de la pipeline est la même. Seul l’outil d’extraction change.

Comment gérer les imports qui s’étendent sur plusieurs fichiers Markdown ?

Chaque fichier Markdown est transformé par tangle dans son propre répertoire de sortie. Si math.md définit add et geometry.md l’importe, soit vous tanglez les deux fichiers dans le même répertoire de build, soit vous structurez votre code comme un package avec des imports relatifs. Le job CI peut tangler tous les fichiers avant d’exécuter les tests, donc les dépendances entre fichiers se résolvent naturellement.

Faites de votre fichier Markdown le seul qui compte

Configurez la pipeline une fois. Commitez le fichier .md. Laissez CI gérer le reste. Si un test échoue, l’échec pointe vers la ligne de Markdown qui a produit le code, pas vers un fichier généré qui ne devrait de toute façon pas exister dans le contrôle de version.

Le but de la programmation littéraire est une source unique de vérité. Une source de vérité qui exige qu’un humain se souvienne d’une étape manuelle n’est pas une source de vérité. C’est une suggestion.