„Bei mir kompiliert es.“ Wer ein LaTeX-Dokument gemeinsam mit anderen schreibt, sagt diesen Satz früher oder später. Ein Build in CI (Continuous Integration) macht daraus etwas, das eine Maschine prüfen kann: Bei jedem Push holt eine saubere TeX Live-Umgebung das Repository von Grund auf neu und beantwortet stellvertretend die Frage, ob sich das PDF wirklich reproduzieren lässt. Im TeX Live der Mitautorin steckt eine andere Paketversion; \setmainfont verweist auf eine Schrift, die nur auf dem eigenen Notebook liegt; die .bbl wurde nie committet. Alles alltäglich – und nichts davon lässt sich ausgerechnet auf der Maschine reproduzieren, die es verursacht hat. Diese Seite führt vom kleinsten GitHub Actions-Workflow, der ein PDF baut, über die drei Wege, TeX Live auf einen Runner zu bekommen, Caching und die Auslieferung des Ergebnisses bis zum unangenehmsten Fehler überhaupt: CI ist grün, das PDF ist kaputt.
Warum LaTeX überhaupt in CI bauen?
Es gibt genau einen Grund: der eigene Rechner taugt nicht als Beweis. Die Ausgabe eines LaTeX-Dokuments hängt nicht allein von seinem Quelltext ab, sondern davon, welcher TeX Live-Jahrgang installiert ist, welche Revision jedes einzelne Paket hat, welche Schriften im System registriert sind und ob irgendwo in TEXINPUTS eine veraltete .sty liegt. „Bei mir hat es gebaut“ trägt deshalb immer einen langen ungesagten Zusatz mit sich: „auf meinem TeX Live von 2024, mit der Klassendatei, die ich vor drei Jahren von Hand hineinkopiert habe“. CI ist die Maschine, die diesen Zusatz jedes Mal ausspricht. Der Job startet in einem leeren Container und sieht nichts außer dem Inhalt des Repositorys – kommt also ein PDF heraus, ist bewiesen, dass allein der Repository-Inhalt dafür genügt.
Das größte praktische Beispiel für diesen Gedanken ist arXiv. arXiv veröffentlicht nicht einfach das hochgeladene PDF, sondern kompiliert den eingereichten LaTeX-Quelltext auf eigenen Servern neu. Und zur Wahl stehen immer nur zwei TeX Live-Versionen, jede auf einen bestimmten Datumsstand eingefroren. Es ist bezeichnend, dass der größte LaTeX-Build-Server der Welt als Erstes seine Umgebung festgenagelt hat. CI ist das Werkzeug, mit dem sich dasselbe im eigenen Repository erreichen lässt – mit einem Nebengewinn: Mitautorinnen, Mitautoren und Gutachter ohne TeX-Installation bekommen jederzeit das aktuelle PDF. Beschrieben wird der Build in einer YAML-Datei unter .github/workflows/.
Der kleinste GitHub Actions-Workflow, der ein PDF baut
Nötig sind nur drei Schritte: Quelltext holen, kompilieren, PDF aufbewahren. Das folgende YAML als .github/workflows/build.yml ablegen – mehr ist es nicht. actions/checkout entpackt das Repository auf den Runner, xu-cheng/latex-action kompiliert in einem Container, in dem TeX Live bereits liegt, und actions/upload-artifact heftet das entstandene PDF an die Seite des Workflow-Laufs. Als Auslöser dient on: [push, pull_request], damit ein defektes PDF gar nicht erst ins Review gelangt.
# .github/workflows/build.yml
name: Build LaTeX
on: [push, pull_request]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: xu-cheng/latex-action@v4
with:
root_file: main.tex
- uses: actions/upload-artifact@v7
with:
name: pdf
path: main.pdf
if-no-files-found: errorroot_file ist die einzige Pflichteingabe von xu-cheng/latex-action. Intern läuft tatsächlich latexmk, mit den Standardargumenten -pdf -file-line-error -halt-on-error -interaction=nonstopmode – also pdfLaTeX, von vornherein so eingestellt, dass beim ersten Fehler abgebrochen wird. -file-line-error bringt Fehlermeldungen in die Form file:line: message, was das Lesen eines CI-Logs erheblich erleichtert. Für einen anderen Engine latexmk_use_xelatex: true oder latexmk_use_lualatex: true setzen, für einen festen TeX Live-Jahrgang texlive_version. Die Basis ist standardmäßig Alpine Linux und lässt sich mit os: debian umstellen. Zusätzliche Systempakete kommen über extra_system_packages, eigene Schriften über extra_fonts hinein.
Weicht der Ablauf selbst vom Standard ab – etwa ein japanisches Dokument aus upLaTeX und dvipdfmx –, ist es am sichersten, eine .latexmkrc mit ins Repository zu legen. CI und der eigene Rechner lesen dann dieselbe Konfigurationsdatei, und es gibt keine zweite Stelle zu pflegen; wie diese Konfiguration aussieht, gehört auf die Seite zu automatisierten Builds. Und noch etwas, falls eine Vorlage lange halten soll: Action-Hauptversionen bewegen sich. actions/checkout etwa hat sein Verhalten in v7 geändert und checkt Fork-Pull-Request-Code standardmäßig nicht mehr aus, wenn der Workflow durch pull_request_target oder workflow_run ausgelöst wurde. Entweder einen jährlichen Termin zum Nachlesen der offiziellen READMEs setzen oder Pull Requests zur Aktualisierung der Abhängigkeiten zulassen.
Drei Wege, TeX Live auf den Runner zu bekommen
Es gibt drei Möglichkeiten: die Action machen lassen, TeX Live selbst auf dem Runner installieren oder den Job in einem Docker-Image laufen lassen, das es bereits enthält. Welche davon passt, hängt davon ab, wie viel von der Umgebung selbst bestimmt werden soll und wie lange bei jedem Lauf gewartet werden darf. Denn TeX Live ist riesig. Eine lokale Vollinstallation von TeX Live 2024 samt Dokumentation und Quellen bringt gemessene 8,7 GB auf die Waage. Das von der Island of TeX verteilte Image texlive/texlive lässt Dokumentation und Quellen weg und wiegt auf Docker Hub komprimiert immer noch rund 2,5 GB. Diese Zahl entscheidet fast alles Weitere.
| Ansatz | Woher TeX Live kommt | Geeignet, wenn |
|---|---|---|
xu-cheng/latex-action | Die Action zieht selbst ein Docker-Image mit TeX Live | Der kürzeste Weg zu einem laufenden Build zählt, konfiguriert allein über root_file |
TeX-Live/setup-texlive-action | Installiert per tlmgr auf dem Runner und cacht TEXDIR | Nur die tatsächlich genutzten Pakete sollen hinein, oder es werden Runner außerhalb von Linux gebraucht |
texlive/texlive | Ein Docker-Image der Island of TeX als container: des Jobs | Der Inhalt des Containers soll selbst bestimmt oder über einen datierten Tag eingefroren werden |
texlive/texlive wird sowohl über Docker Hub als auch als registry.gitlab.com/islandoftex/images/texlive verteilt, und der Standard-Tag entspricht dem full-Schema – allerdings ohne Dokumentation und Quellen. Die Varianten -doc, -src und -doc-src gibt es bei Bedarf, jede zuverlässig schwerer. Da latest wöchentlich neu gebaut wird, ist alles mit Abgabetermin sicherer auf einen datierten Snapshot-Tag wie TL2022-2022-06-05 festgelegt. Um einen früheren Jahrgang unverändert zu reproduzieren, stehen historische Tags wie TL2018-historic bereit. Wird das Image als container: des Jobs eingetragen, laufen alle folgenden Schritte darin.
# pin the image; latest is rebuilt weekly
jobs:
build:
runs-on: ubuntu-latest
container: texlive/texlive:latest
steps:
- uses: actions/checkout@v7
- run: latexmk -pdf -halt-on-error -interaction=nonstopmode main.tex
- uses: actions/upload-artifact@v7
with:
name: pdf
path: main.pdf
if-no-files-found: errorDie TeX Live-Installation cachen und Wartezeit sparen
Mit TeX-Live/setup-texlive-action ist Caching bereits aktiv. Die Eingabe cache der Action hat den Standardwert true; intern ruft sie @actions/cache auf und legt das gesamte TEXDIR ab. Gespeichert wird in der Nachbereitungsphase nach Ende des Jobs, sodass auch während des Builds Erzeugtes – etwa Font-Caches – mit übernommen wird. Jeder Lauf nach dem ersten spart damit den vollständigen Download von einem tlmgr-Mirror. Abschalten lässt sich das mit cache: false. Ein Hinweis: Diese Action lag früher unter teatimeguest/setup-texlive-action und ist inzwischen in die Organisation TeX-Live umgezogen – aus älteren Artikeln kopiertes YAML löst sich also nicht mehr auf.
- uses: TeX-Live/setup-texlive-action@v4
with:
version: 2025
packages: |
scheme-basic
latexmk
biber
biblatex
- run: latexmk -pdf -halt-on-error -interaction=nonstopmode main.texDer übliche Umgang mit dieser Action besteht darin, scheme-basic in packages zu setzen und darauf aufzubauen. Statt alle 8 GB heranzuziehen, wird nur aufgeführt, was tatsächlich per \usepackage gebraucht wird – biber, biblatex, siunitx und Ähnliches. Wird die Liste zu lang, lagert package-file sie aus und verweist etwa auf .github/tl_packages oder ein Muster wie **/DEPENDS.txt. Die Eingabe version legt den Jahrgang fest und reproduziert damit im Kleinen, was arXiv tut: Statt „es baut auf latest“ lässt sich sagen „es baut auf TeX Live 2025“. Wer stattdessen den Docker-Weg geht, sollte nicht versuchen, das Image selbst mit actions/cache zu cachen – den Tag festlegen und den Pull der Registry überlassen ist der geradere Weg.
Das PDF weitergeben: Artifact oder Release?
Diese beiden ersparen den Satz „Installieren Sie bitte zuerst TeX Live“ gegenüber Gutachtern. Mit actions/upload-artifact hängt das PDF an der Seite des Workflow-Laufs, und jede Person mit Zugriff auf das Repository kann es herunterladen. Die Aufbewahrung richtet sich nach der Repository-Einstellung, mit einer Obergrenze von 90 Tagen. Eine kleine Einstellung wiegt dabei schwerer, als sie aussieht: archive steht standardmäßig auf true, das Artifact wird also vor dem Upload gezippt, und am anderen Ende landet ein Zip statt main.pdf. Mit archive: false geht eine einzelne Datei unverändert hoch – ein Handgriff weniger für die Mitautorin.
Für eine Fassung, die tatsächlich weitergegeben wird, passt ein Release besser. Artifacts verschwinden mit Ablauf der Aufbewahrungsfrist; ein an ein Release gehängtes PDF nicht, es bekommt eine dauerhafte URL und ist von der Startseite des Repositorys aus erreichbar. Üblich ist die Form „Versions-Tag pushen, Release erhalten“, und da die von GitHub gehosteten Ubuntu-Runner-Images die GitHub CLI (gh) bereits mitbringen, genügt dafür eine Zeile ohne zusätzliche Action. Das Anlegen eines Release ist allerdings ein Schreibvorgang, also muss permissions: auf contents: write angehoben und ein GH_TOKEN übergeben werden. Der Build-Workflow bleibt besser bei contents: read, der Release-Job gehört in eine eigene Datei.
# .github/workflows/release.yml
name: Release PDF
on:
push:
tags: ["v*"]
permissions:
contents: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: xu-cheng/latex-action@v4
with:
root_file: main.tex
- run: gh release create "$GITHUB_REF_NAME" main.pdf
env:
GH_TOKEN: ${{ github.token }}- Immer
if-no-files-found: errorsetzen. Der Standard istwarn, ein Tippfehler inpath:endet also trotzdem mit grünem Workflow. - Mit
archive: falsegeht das PDF unverpackt statt im Zip hoch (nur für einzelne Dateien). - Die Build-Konfiguration (
.latexmkrcund Ähnliches) gehört ins Repository, damit lokal und CI dieselben Schritte gehen und eine Abweichungsquelle entfällt. - Für alles, was eingereicht wird, die TeX Live-Version festlegen – über
texlive_version, über die Eingabeversionvonsetup-texlive-actionoder über einen datierten Image-Tag. permissions:standardmäßig aufcontents: readlassen und nur im Job, der ein Release anlegt, aufcontents: writeanheben.
Wenn CI fehlschlägt – und wenn CI grün ist, das PDF aber kaputt
Zuerst das Log öffnen und nach Zeilen suchen, die mit ! beginnen. Jeder LaTeX-Fehler meldet sich in dieser Form, er ist also auch unter Hunderten von Zeilen auffindbar. Die häufigen Symptome lassen sich auf fünf oder sechs eingrenzen, und jedes lässt sich fast eins zu eins einer Ursache zuordnen.
| Meldung im Log | Was tatsächlich passiert ist | Was zu tun ist |
|---|---|---|
! LaTeX Error: File `...sty' not found. | Das TeX Live in CI enthält dieses Paket nicht | In packages aufnehmen, extra_system_packages nutzen oder auf ein Image mit full-Schema wechseln |
! Undefined control sequence. | Ein Tippfehler im Dokument, oder das Paket mit dem Befehl wurde nie geladen | Schreibweise in dieser Zeile und die \usepackage-Liste prüfen; der Fehler sollte auch lokal auftreten |
! Package fontspec Error: The font "..." cannot be found. | Diese Schrift liegt nicht im Container; lokal kam sie vom Betriebssystem | Die Schrift ins Repository legen und über extra_fonts übergeben oder auf eine mit TeX Live gelieferte Schrift wechseln |
LaTeX Warning: There were undefined references. | Nur eine Warnung: Der Exit-Status ist 0, und im PDF bleiben ?? stehen | latexmk die nötigen Durchläufe machen lassen; für einen Fehlschlag das Log mit grep prüfen und ungleich null beenden |
No files were found with the provided path | Der an upload-artifact übergebene path: passt nicht zum tatsächlichen Ausgabenamen | Prüfen, wo das PDF wirklich landet; ohne if-no-files-found: error endet der Lauf trotzdem grün |
! Emergency stop. | TeX ist in seine interaktive Eingabeaufforderung gefallen, und CI hat kein Terminal zum Antworten | -interaction=nonstopmode ergänzen; latex-action übergibt es standardmäßig |
Ein verbreitetes Missverständnis zu -interaction=nonstopmode sei hier ausgeräumt: Die Option verschluckt keine Fehler. Läuft ein Dokument mit einem undefinierten Befehl durch pdflatex -interaction=nonstopmode, ist der Exit-Status ehrlich 1. Was sie sehr wohl tut: Sie schreibt trotzdem ein PDF – sie überspringt die fehlerhafte Stelle und läuft bis zum Ende des Dokuments durch. Die Option verzichtet also lediglich darauf, anzuhalten und einen Menschen zu fragen; über Erfolg oder Misserfolg geht keine Information verloren. Dasselbe Dokument durch latexmk -pdf geschickt, endet ungleich null und hinterlässt kein PDF. In CI ist genau dieses Verhalten erwünscht, und -halt-on-error bricht zusätzlich beim ersten Fehler ab.
Was wirklich lautlos kaputtgeht, ist nicht der Fehler, sondern die Warnung. Lässt sich ein \ref oder \cite nicht auflösen, gibt LaTeX nur LaTeX Warning: There were undefined references. aus – und der Exit-Status ist 0. CI wird grün, und das Artifact ist ein PDF voller ??. Kommt der Standardwert warn von if-no-files-found hinzu, kann ein Workflow mit vertipptem path: von Anfang bis Ende grün durchlaufen und dabei überhaupt nichts hinterlassen. Wer dem Grün trauen will, schreibt mindestens if-no-files-found: error und überlässt latexmk die Entscheidung, wie viele Durchläufe das Dokument braucht.
Bleibt der letzte Fall: Es baut lokal, und nur CI scheitert. Die Ursache ist fast immer eine Abhängigkeit von etwas, das nicht im Repository liegt. Ein Bild oder eine fertig erzeugte .bbl, die es nur auf der eigenen Platte gibt; eine erzeugte Datei, die .gitignore stillschweigend geschluckt hat; und sehr häufig die Groß- und Kleinschreibung eines Dateinamens. Die Standard-Dateisysteme von macOS und Windows unterscheiden nicht zwischen Groß- und Kleinschreibung, \includegraphics{Figure1} findet also lokal problemlos figure1.pdf – auf dem Linux des Runners sind das zwei verschiedene Namen. Bei einem CI-Fehlschlag lautet die erste Frage: Liegt die Sache überhaupt im Repository? Und genau darin besteht, umgekehrt betrachtet, der Wert von CI – sie stellt diese Frage bei jedem Push.