Wenn Ihre Build-Pipeline nicht laufen kann, ohne dass Sie ein Terminal öffnen und make tangle eintippen, haben Sie kein literates Programm. Sie haben ein Tagebuch mit einem Compiler.
Der ganze Sinn von Literate Programming ist, dass Prosa und Code eine einzige Quelle der Wahrheit teilen. Die Markdown-Datei ist das Artefakt. Alles andere — der ausführbare Source, die gerenderte Dokumentation, die Testdateien — ist abgeleitet. Abgeleitete Artefakte gehören in CI, nicht in Ihr Arbeitsgedächtnis.
Die Frage ist nicht, ob CI kann weave und tangle. Natürlich kann es. Jede Maschine, die Python und pandoc ausführt, kann das. Die Frage ist, ob Ihre Repository-Struktur es CI erlaubt, die abgeleiteten Dateien zu besitzen, oder ob Sie immer noch so tun, als wären .py-Dateien Source Code, wenn sie eigentlich Build-Output sind.
Was Weave und Tangle eigentlich tun
Donald Knuth nannte die beiden Operationen weave und tangle, und die Namen sind so verwirrend, dass Leute das Thema ganz meiden.
tangle nimmt das narrative Dokument — die .md- oder .w-Datei — und extrahiert die Code-Blöcke in der Reihenfolge, die der Compiler erwartet. Es produziert die lauffähigen Source-Dateien. weave macht das Gegenteil. Es nimmt dasselbe Dokument und produziert die menschenlesbare Dokumentation, mit pretty-printed Code, Querverweisen und einem Inhaltsverzeichnis.
Beide sind deterministische Transformationen. Sie nehmen einen Input und produzieren konsistente Outputs. Das ist die Definition von etwas, das CI handhaben sollte.
Warum die meisten Teams die Pipeline verkehrt herum aufbauen
Der typische Literate-Programming-Workflow sieht so aus. Sie schreiben algorithm.md. Sie führen lokal ein Skript aus, um algorithm.py zu extrahieren. Sie führen Tests gegen algorithm.py aus. Sie committen sowohl algorithm.md als auch algorithm.py in Git. Dann öffnen Sie einen Pull Request.
Das ist schon kaputt. Sie haben zwei Kopien derselben Logik in der Versionskontrolle. Wenn ein Reviewer eine Änderung an algorithm.py vorschlägt, müssen Sie sich daran erinnern, sie zurück in algorithm.md zu portieren. Wenn Sie algorithm.md bearbeiten und vergessen, erneut zu tanglen, ist die committed .py-Datei stale. Die einzige Quelle der Wahrheit ist eine Fiktion.
Der korrekte Workflow ist einfacher. Sie committen nur die .md-Datei. CI führt tangle aus, um die .py-Datei zu produzieren, und führt dann die Test-Suite dagegen aus. Wenn die Tests bestehen, führt CI optional weave aus, um Dokumentation zu veröffentlichen. Die .py-Datei berührt niemals die Versionskontrolle. Sie ist Build-Output, wie eine .o-Datei oder ein Docker-Image.
Eine funktionierende CI-Pipeline für Markdown-zu-Python
Hier ist ein kompletter GitHub Actions Workflow, der die Markdown-Datei als einzige Quelle der Wahrheit behandelt. Speichern Sie ihn als .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/
Der paths-Filter ist wichtig. Dieser Workflow läuft nur, wenn sich eine Markdown-Datei ändert, weil Markdown der einzige Input ist, der zählt.
Das tangle.py-Skript ist derselbe dreißigzeilige Extractor, den Sie lokal ausführen würden. Hier ist eine Version, die mehrere Dateien und ein Output-Verzeichnis handhabt:
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()
Führen Sie es lokal aus, um zu verifizieren:
$ python scripts/tangle.py src/calculator.md --out build/
$ python -m pytest build/ -v
Der CI-Job macht genau dasselbe. Der einzige Unterschied ist, dass CI es in einer sauberen Umgebung ausführt, sodass eine stale .py-Datei, die in Ihrem Arbeitsverzeichnis versteckt ist, die Test-Suite nicht täuschen kann.
Dokumentation im selben Pipeline webben
tangle produziert Code. weave produziert Docs. Beides kann in CI laufen.
Für weave brauchen Sie ein Tool, das Markdown in ein präsentierbares Format umwandelt. Pandoc ist die langweilige, zuverlässige Wahl. Wenn Sie Querverweise, Syntax-Highlighting und ein Inhaltsverzeichnis wollen, bringt Sie schon eine kleine Template dahin:
- 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/
Das gerenderte HTML ist ebenfalls ein abgeleitetes Artefakt. Sie können es bei jedem Merge auf main zu GitHub Pages, Netlify oder einem S3-Bucket veröffentlichen. Die Markdown-Datei bleibt kanonisch. Das HTML ist das, was Leser sehen.
Der Kompromiss zwischen Commit und Generieren
Einige Teams wehren sich dagegen. Sie wollen die .py-Dateien in Git, weil es git grep ermöglicht und weil die Code-Review-UI von GitHub Python-Syntax besser versteht als Markdown.
Das sind echte Probleme, aber sie sind Tooling-Probleme, keine Architektur-Probleme. Generierten Code einchecken, weil Ihr Code-Review-Tool schlecht ist, ist wie node_modules einchecken, weil Ihr package manager langsam ist. Es funktioniert, aber es schafft Schulden.
Wenn Sie die .py-Dateien unbedingt im Repository brauchen, generieren Sie sie in CI und committen Sie sie mit einem Bot-Account zurück. Hier ist das Muster:
- Ein Entwickler pusht eine Änderung an
algorithm.md. - CI führt
tangle.pyaus und produziertalgorithm.py. - Wenn der extrahierte Code von dem in
mainabweicht, öffnet CI einen Pull Request mit der aktualisierten.py-Datei. - Ein Mensch reviewt den generierten Diff und merged ihn.
Das hält die generierten Dateien in der Versionskontrolle, ohne dass sie auseinanderlaufen. Der Bot wird zur einzigen Quelle der Wahrheit für generierten Output, und der Bot ändert nur das, was die Markdown-Datei verlangt.
Wann CI-Automatisierung sich lohnt, und wann nicht
Automatisiertes Weave und Tangle brillieren, wenn die Prosa lang, der Code komplex und mehrere Personen das Dokument bearbeiten. Forschungscode, Library-Dokumentation und Algorithmus-Implementierungen profitieren alle von einer kanonischen Markdown-Datei, die CI bei jedem Push validiert.
Es lohnt sich nicht für kleine Skripte oder boilerplate-lastigen Applikationscode. Wenn Ihre Markdown-Datei zwei Absätze und eine zwanzigzeilige Funktion enthält, kosten der CI-Overhead, Runner-Minuten und das mentale Modell mehr als das Synchronisierungsproblem, das Sie lösen. Schreiben Sie einfach einen guten Docstring.
Der andere Fall, in dem das scheitert, ist wenn Ihr Language-Tooling .py-Dateien als primär ansieht. Debugger, Profiler und Coverage-Tools wollen normalerweise auf den generierten Source zeigen, nicht auf das Markdown. Sie können das umgehen, indem Sie Dateien lokal vor dem Debuggen generieren, aber das ist Reibung. Wenn Ihr Team im Debugger lebt, ist Literate Programming möglicherweise trotz einer guten CI-Pipeline der falsche Fit.
FAQ
Sollte ich die generierten .py-Dateien commiten oder .gitignoren?
Gitignorieren Sie sie. Wenn sie deterministisch aus Markdown generiert werden, sind sie Build-Artefakte. Die einzige Ausnahme ist, wenn ein externes System, wie eine package registry, Ihren Tangle-Schritt nicht ausführen kann. In diesem Fall verwenden Sie das Bot-Commit-Muster, das oben beschrieben wurde.
Was ist, wenn mein literates Dokument mehrere Sprachen mischt?
Das tangle.py-Skript ist sprachunabhängig. Verwenden Sie # filename.rs für Rust-Blöcke, # filename.go für Go-Blöcke und so weiter. CI führt dasselbe Skript für jede Sprache aus. Sie brauchen nur einen Test-Schritt pro Sprache im Workflow.
Funktioniert das mit benannten Code-Chunks wie Knuths WEB?
Moderne Tools wie Entangled unterstützen benannte Chunks und Dependency-Tracking. Wenn Sie dieses Expressivitätsniveau brauchen, verwenden Sie Entangled in CI anstelle eines Custom-Skripts. Die Pipeline-Struktur ist dieselbe. Nur das Extraktionstool ändert sich.
Wie handle ich Imports, die mehrere Markdown-Dateien überspannen?
Jede Markdown-Datei tangelt in ihr eigenes Output-Verzeichnis. Wenn math.md add definiert und geometry.md es importiert, tanglen Sie entweder beide Dateien in dasselbe Build-Verzeichnis oder strukturieren Sie Ihren Code als Package mit relativen Imports. Der CI-Job kann alle Dateien tanglen, bevor er Tests ausführt, sodass Cross-File-Dependencies sich natürlich auflösen.
Machen Sie Ihre Markdown-Datei zur einzigen Datei, die zählt
Richten Sie die Pipeline einmal ein. Committen Sie die .md-Datei. Lassen Sie CI den Rest handhaben. Wenn ein Test fehlschlägt, zeigt der Fehler auf die Markdown-Zeile, die den Code produziert hat, nicht auf eine generierte Datei, die sowieso nicht in der Versionskontrolle existieren sollte.
Das Ziel von Literate Programming ist eine einzige Quelle der Wahrheit. Eine Quelle der Wahrheit, die einen Menschen erfordert, sich an einen manuellen Schritt zu erinnern, ist keine Quelle der Wahrheit. Sie ist ein Vorschlag.