# Build-Skripte als Produktcode

URL: https://www.mikebild.dev/de/blog/build-skripte-als-produktcode/

Ein Build-Skript ist keine Sammlung bequemer Abkürzungen für die Kommandozeile. Es ist die Beschreibung sämtlicher Eingaben, aus denen ein auslieferbares Artefakt entsteht: Quellen, Abhängigkeiten in konkreten Versionen, Toolchain, Umgebung. Was dort nicht steht, wird stillschweigend von der Maschine geliehen, auf der gerade gebaut wird.

Trotzdem landen diese Skripte gern in einer Grauzone zwischen Code und Infrastruktur: selten reviewt, fast nie getestet, meist von einer einzigen Person gepflegt. Genau diese Grauzone ist der Grund, warum Releases wehtun.

## Die These

Ein Feature ist erst fertig, wenn ein zweiter Mensch es aus dem leeren Verzeichnis heraus bauen, testen und ausliefern kann – ohne dass ich danebensitze. Der Weg dorthin ist kein Nebenprodukt. Er ist das Produkt. Wenn `git clone` plus ein Kommando nicht ausreicht, um dieselbe Artefaktdatei wie auf meinem Rechner zu erzeugen, dann existiert das System eigentlich nur bei mir lokal – und ist damit nicht ausgeliefert, sondern nur vorgeführt.

Ich behandle das Build-Skript deshalb mit derselben Sorgfalt wie Produktcode: Es hat einen klaren Vertrag, es wird versioniert, es wird gelesen, und es hat einen definierten Fehlerpfad. Das macht es nicht in jeder Hinsicht so laufzeitkritisch wie den ausgelieferten Dienst. Gemeint ist das Verantwortungsniveau: Andere müssen sich auf sein Ergebnis verlassen können.

## Warum das eine Architekturfrage ist

„Build“ klingt nach Handwerk, nach Nebensache. Tatsächlich entscheidet der Build über zwei Eigenschaften, die sonst nur teuer nachrüstbar sind: Reproduzierbarkeit und Zykluszeit.

Reproduzierbarkeit heißt, dass gleiche Eingaben gleiche Ausgaben erzeugen – unabhängig von Maschine, Uhrzeit und Laune des Netzwerks. Ein Build, der auf meinem Laptop grün ist und auf dem CI-Server rot, hat kein Testproblem. Er hat ein Zustandsproblem: Irgendwo fließt undeklarierter Zustand ein – eine global installierte CLI, eine Umgebungsvariable, eine Version, die „latest“ auflöst.

Zykluszeit heißt, wie lange es dauert, von einer Änderung zu einer belastbaren Aussage zu kommen. Diese Zeit multipliziert sich mit jeder Person und jedem Tag im Team. Ein Build, der zehn Minuten braucht und alle zwei Läufe scheitert, ist kein Komfortthema. Er ist ein Produktivitätsleck, das man in Stunden pro Woche messen kann.

Beides sind Architektureigenschaften, keine Tool-Details. Deshalb kommt die Werkzeugfrage zuletzt. Davor steht die Klärung: Welche Eingaben gehen in den Build? Welche davon sind deklariert und welche implizit? Was ist das erwartete Artefakt? Erst danach entscheidet sich, ob das ein `npm`-Script, ein Gulpfile oder ein Makefile wird.

## Ein kleines, unspektakuläres Beispiel

Der häufigste Fehler ist nicht das falsche Tool, sondern der undeklarierte Zustand. Ein typisches Frontend-Setup aus dieser Zeit sieht oft so aus:

```json
{
  "scripts": {
    "start": "webpack-dev-server --hot",
    "build": "webpack -p"
  }
}
```

Das liest sich harmlos. Der versteckte Vertrag steht aber gar nicht im Skript: Es setzt voraus, dass `webpack` global installiert ist, dass die richtige Node-Version läuft, dass `NODE_ENV` gesetzt ist. Nichts davon ist Teil des Repositories. Der neue Kollege bekommt am ersten Tag einen anderen Build als ich – und niemand hat den Vertrag gebrochen, weil es nie einen gab.

Die Korrektur ist billig und wirkt sofort: Abhängigkeiten lokal binden, den Einstieg auf ein Kommando reduzieren, die eine Umgebungsentscheidung explizit machen.

```json
{
  "scripts": {
    "start": "webpack-dev-server --hot --config webpack.dev.js",
    "build": "NODE_ENV=production webpack --config webpack.prod.js",
    "test": "mocha --recursive",
    "verify": "npm run test && npm run build"
  }
}
```

Der Gewinn ist nicht Eleganz. Der Gewinn ist, dass `npm run verify` jetzt eine überprüfbare Aussage ist: Wenn es grün ist, existiert ein Artefakt, und die Tests, die es absegnen, sind mitgelaufen. Ich habe keinen Build-Server ersetzt. Ich habe nur den Vertrag ins Repository geholt, wo er reviewt werden kann.

## Determinismus ist kein Luxus

Der teuerste Bruch der Reproduzierbarkeit sind Versionen, die sich zur Laufzeit entscheiden. `^1.2.0` in der `package.json` bedeutet: Der Build von heute und der von nächster Woche installieren möglicherweise unterschiedlichen Code, ohne dass sich eine Zeile im Repository geändert hat. Für ein Experiment ist das egal. Für ein Produkt ist es eine offene Flanke.

```bash
# freeze the exact dependency tree, then commit the lockfile
npm shrinkwrap
git add npm-shrinkwrap.json
```

Ab hier ist der Abhängigkeitsbaum ein Artefakt wie jedes andere: versioniert, diffbar, im Review sichtbar. Wenn ein Update etwas kaputt macht, sehe ich es als Änderung an einer Datei – und nicht als Geist, der nur auf dem CI-Server erscheint. Das ist der Kern des Ganzen: Zustand, der vorher unsichtbar durch die Ritzen floss, wird zu etwas, das man lesen und ablehnen kann.

Wo mir das Sprachökosystem nicht reicht, ziehe ich die Grenze eine Ebene tiefer. Ein schlankes Image, das genau die Toolchain enthält, macht die Umgebung selbst zum versionierten Artefakt.

```makefile
IMAGE = myapp-build:1.0

build:
	docker build -t $(IMAGE) .
	docker run --rm -v $(PWD):/src $(IMAGE) npm run verify
```

Ich benutze Docker hier nicht als Deployment-Strategie und schon gar nicht als Selbstzweck – das Thema Cluster und Orchestrierung lasse ich bewusst weg. Ich benutze es als Reproduzierbarkeits-Werkzeug: Die Frage „welche Node-Version, welches `make`, welche System-Bibliothek?“ hat jetzt eine Antwort, die im Repository steht.

## Ein Kommando pro Absicht

Ein Build-Skript ist eine Schnittstelle, bevor es eine Implementierung ist. Deshalb interessiert mich zuerst das Vokabular, nicht der Inhalt. Wenn jedes Projekt im Haus dieselben vier Verben versteht – `install`, `verify`, `build`, `clean` –, dann muss niemand mehr das README lesen, um mitzuarbeiten. Die Absicht steht im Namen, die Mechanik dahinter darf sich pro Projekt unterscheiden.

Das klingt nach Kosmetik, ist aber eine Architekturentscheidung mit Reichweite. Sobald das Vokabular stabil ist, kann ich Werkzeuge dahinter austauschen, ohne dass sich für das Team etwas ändert. Der Wechsel von Grunt auf Gulp, von Gulp auf pures `npm run`, von einem Test-Runner auf einen anderen – all das bleibt hinter `npm run verify` verborgen. Die Grenze, die ich ziehe, trennt Absicht von Mechanik. Genau diese Trennung macht den Build wartbar, während sich die Toolchain unter ihm bewegt.

Der andere Effekt ist, dass das Skript zur Dokumentation wird. Ein Neuzugang muss nicht wissen, dass der Build intern Babel, ein Bundling und einen Uglify-Schritt hat. Er muss `npm run build` kennen. Das verschobene Wissen ist der Punkt: Es steht jetzt im Code und nicht mehr in den Köpfen – und geht damit nicht verloren, wenn jemand das Team verlässt.

## Der Build gehört in die Pipeline, nicht auf meinen Laptop

Ein Vertrag, den nur ich einhalte, ist keiner. Deshalb ist die konsequente Fortsetzung, denselben Einstieg auf einem neutralen Server auszuführen – bei jedem Push, ohne Ausnahme.

```yaml
language: node_js
node_js:
  - "4"
install:
  - npm install
script:
  - npm run verify
```

Das ist bewusst langweilig. Die CI ruft exakt dasselbe Kommando auf, das ich lokal benutze – nicht eine eigene, parallele Wahrheit. Damit ist der Server kein zweites Build-System, das auseinanderdriften kann, sondern ein unbestechlicher Zeuge für denselben Vertrag. Sobald ein Kollege eine undeklarierte Annahme einbaut, wird die Pipeline rot, bevor der Code in den Hauptzweig kommt. Der teure Fehler – Umgebungsdrift, die erst im Release auffällt – wird zu einem billigen Fehler, der im Pull Request auffällt.

Genau hier werden die Kennzahlen aus der nächsten Schicht überhaupt erst messbar: Zeit bis grüner Build, Anteil roter Läufe, Dauer bis zum reproduzierten Artefakt. Ohne die Pipeline sind das Anekdoten. Mit ihr sind es Zahlen, an denen man eine Verbesserung belegen kann.

## Der Fehlerpfad, nicht der glückliche Pfad

Eine Idee zum Build ist erst belastbar, wenn der Fehlerpfad beschrieben ist. Genau hier trennt sich Demo von Betrieb.

Die erste Frage ist Idempotenz. Was passiert beim zweiten Lauf? Ein Build, der ein `dist/`-Verzeichnis füllt, ohne es vorher zu leeren, produziert nach ein paar Wochen Geisterdateien: alte Bundles, die niemand mehr referenziert, aber ausgeliefert werden. Ein sauberer Build beginnt mit `clean`, nicht aus Ordnungsliebe, sondern damit der zehnte Lauf dasselbe ergibt wie der erste.

Die zweite Frage ist das Scheitern selbst. Ein Skript, das mehrere Schritte mit `&&` verkettet, muss beim ersten Fehler abbrechen – und mit einem Exit-Code ungleich null enden. Sonst meldet die CI grün, obwohl der Test-Schritt still weggelaufen ist. Ich habe mehr kaputte Releases auf verschluckte Exit-Codes zurückgeführt als auf falsche Logik.

Die dritte Frage ist Rückbaubarkeit. Kann ich das Artefakt von letzter Woche exakt neu erzeugen, wenn ein Fehler in Produktion auftaucht? Wenn der Build nicht deterministisch ist, ist die Antwort nein – und dann ist ein Rollback keine Entscheidung mehr, sondern ein Ratespiel.

## Die betriebliche Konsequenz

Ob die Arbeit wirkt, sehe ich an einfachen Dingen: Wie lange braucht ein neuer Kollege bis zum ersten lokalen Build? Wie oft scheitert die CI an einer Annahme, die nur auf einem Rechner galt? Lässt sich das Artefakt eines alten Releases aus den damaligen Eingaben erneut erzeugen? Ein deklarierter Build verbessert genau diese Fälle. Wie groß der Effekt ist, muss das Team an seinen eigenen Laufzeiten und Fehlern messen.

## Typische Fehler

Der erste Fehler ist, das Werkzeug mit der Lösung zu verwechseln. Gulp ist keine Reproduzierbarkeit. Ein Makefile ist keine Determinismus-Garantie. Diese Werkzeuge können einen sauberen Vertrag ausdrücken, aber sie erzeugen ihn nicht von allein. Ich habe Gulpfiles gesehen, die genauso viel verstecktes Wissen voraussetzten wie das kaputte `npm`-Script davor.

Der zweite Fehler ist, den Build als fertig zu betrachten, sobald er einmal grün war. Ein Build ist lebender Code. Er verrottet mit jeder undeklarierten Annahme, die sich einschleicht – die eine Umgebungsvariable, die „nur bei uns“ gesetzt ist, das Tool, das „eh jeder installiert hat“. Deshalb gehört das Build-Skript in denselben Review wie das Feature, nicht in einen separaten, unbeaufsichtigten Ordner.

Der dritte Fehler ist, Geschwindigkeit gegen Vertrauen zu tauschen. Ein Build, der Tests überspringt, um schneller grün zu sein, verkauft künftige Zykluszeit für heutige Bequemlichkeit. Der Kredit wird immer zurückgefordert, meist im ungünstigsten Moment.

## Worauf ich im Review prüfe

Drei Fragen reichen mir meistens, um zu beurteilen, ob ein Build den Namen Produktcode verdient. Kann ein Mensch, der das Projekt nicht kennt, aus dem leeren Verzeichnis mit einem Kommando ein Artefakt erzeugen? Ist jede Eingabe, die das Ergebnis beeinflusst, im Repository sichtbar – Versionen, Umgebung, Toolchain? Bricht der Build bei einem Fehler sichtbar ab, statt still grün zu melden?

Wenn eine dieser Fragen mit Nein beantwortet wird, fehlt kein Tool. Es fehlt ein Teil der Architektur. Und dieser Teil ist meist billiger zu schließen, als das Team glaubt – solange man den Build als das behandelt, was er ist: Produktcode, der bestimmt, ob ein System reproduzierbar existiert oder nur vorgeführt wurde.

## Schluss

Am Ende steht eine praktische Prüfung: Kann ein neuer Rechner aus den versionierten Eingaben dasselbe Artefakt bauen, bricht ein Fehler sichtbar ab und lässt sich ein Release zurücknehmen? Wenn ja, erfüllt das Build-Skript seine Verantwortung. Ob darunter Gulp, Grunt oder ein Shell-Skript läuft, ist zweitrangig.
