Versionierung interner TypeScript-Pakete: Changesets und Semver im Monorepo
AI generated
<T>
type
TypeScript · Monorepo · Release-Management
Versionierung interner TypeScript-Pakete
Changesets, Semver und unabhängige Releases im Monorepo

Ohne einen strukturierten Versionierungsprozess enden interne TypeScript-Pakete entweder bei einer einzigen globalen Versionsnummer für das gesamte Monorepo oder bei handgeschriebenen Changelogs, die niemand konsistent pflegt. Changesets löst beide Probleme mit einem deklarativen, review-fähigen Workflow.

17 Min. Lesezeit Changesets · Semver · Release-Automatisierung TypeScript 5.x · Node.js 20+

1. Warum eine globale Versionsnummer im Monorepo scheitert

Ein naiver Ansatz für die Versionierung in einem TypeScript-Monorepo ist, allen Paketen dieselbe globale Versionsnummer zu geben und sie bei jedem Release gemeinsam zu erhöhen. Das funktioniert kurzfristig, führt aber schnell zu unnötigen Major-Version-Sprüngen für Pakete, die von einer Änderung gar nicht betroffen sind, und macht es für Konsumenten unmöglich zu erkennen, ob sich ein bestimmtes Paket tatsächlich verändert hat, nur weil die Versionsnummer gestiegen ist.

Die Alternative, jedes Paket komplett unabhängig manuell zu versionieren, scheitert an der menschlichen Konsistenz: Entwickler vergessen, ein Changelog-Eintrag zu schreiben, wählen inkonsistente Semver-Stufen für ähnliche Änderungen, oder ein Breaking Change wird versehentlich als Patch-Version veröffentlicht. Für ein wachsendes TypeScript-Monorepo mit vielen internen Paketen braucht es einen Prozess, der Versionierungsentscheidungen zum Zeitpunkt der Code-Änderung erzwingt, nicht erst beim Release selbst.

Ein weiterer Nachteil beider naiven Ansätze zeigt sich bei der Fehlersuche: Wenn ein internes Paket in einem TypeScript-Monorepo unerwartetes Verhalten zeigt, ist die erste Frage meist, welche Version tatsächlich installiert ist und was sich seit der letzten funktionierenden Version geändert hat. Ohne strukturierte Versionshistorie und nachvollziehbare Changelogs wird diese eigentlich einfache Frage zu einer zeitaufwendigen Archäologie durch Commit-Historien und Slack-Nachrichten.

2. Semver-Disziplin für interne Pakete durchsetzen

Semantic Versioning definiert drei Ebenen: Patch für rückwärtskompatible Bugfixes, Minor für rückwärtskompatible neue Funktionen, und Major für Breaking Changes. Diese Regeln klingen einfach, werden aber in der Praxis regelmäßig falsch angewendet, besonders bei internen Paketen in einem TypeScript-Monorepo, wo Entwickler eher dazu neigen, Versionsentscheidungen als Nebensache zu behandeln, weil kein externer Kunde direkt betroffen scheint.

Dieser Trugschluss ist gefährlich, weil auch interne Konsumenten, also andere Teams innerhalb derselben Organisation, sich auf korrekte Semver-Signale verlassen, um zu entscheiden, ob ein Update sicher automatisiert eingespielt werden kann. Ein fälschlicherweise als Minor deklarierter Breaking Change bricht automatisierte Dependency-Updates genauso wie bei einer öffentlichen Bibliothek. Changesets erzwingt diese Disziplin, indem die Semver-Stufe explizit bei jeder Änderung angegeben werden muss, statt sie implizit dem Release-Verantwortlichen zu überlassen.

Ein oft übersehener Aspekt der Semver-Disziplin betrifft interne Typdefinitionen selbst: Wird ein exportierter TypeScript-Typ in einem Paket verändert, etwa ein Feld von optional zu Pflicht umgewandelt, ist das ein Breaking Change für jeden Konsumenten, auch wenn sich die Laufzeit-Logik des Pakets nicht verändert hat. Diese rein typbezogenen Breaking Changes werden in einem TypeScript-Monorepo besonders häufig unterschätzt, weil sie beim manuellen Testen der Laufzeit-Funktionalität nicht auffallen.

3. Der Changesets-Workflow im Detail

Der Kern von Changesets ist eine einfache Idee: Statt Versionsnummern direkt zu ändern, legt ein Entwickler bei jeder relevanten Codeänderung eine kleine Markdown-Datei im .changeset-Verzeichnis an, die beschreibt, welche Pakete betroffen sind, welche Semver-Stufe angemessen ist, und was sich fachlich geändert hat. Diese Datei wird zusammen mit dem Code-Änderungen im selben Pull Request eingereicht und durchläuft denselben Review-Prozess.

Der Befehl npx changeset führt interaktiv durch diesen Prozess: Er fragt, welche Pakete in einem TypeScript-Monorepo von der aktuellen Änderung betroffen sind, welche Semver-Stufe für jedes Paket gilt, und lässt den Entwickler eine kurze Beschreibung eintippen, die später automatisch ins Changelog übernommen wird. Da diese Datei Teil des Pull Requests ist, sieht der Reviewer die Versionsentscheidung direkt im Kontext der eigentlichen Codeänderung, statt sie später blind zu vertrauen.


# Changesets im Monorepo initialisieren
npx changeset init

# Neuen Changeset für die aktuelle Änderung anlegen
npx changeset add

# Alle offenen Changesets in echte Versions-Bumps anwenden
npx changeset version

# Betroffene Pakete tatsächlich veröffentlichen
npx changeset publish

# Status offener Changesets prüfen, bevor ein Release gebaut wird
npx changeset status --verbose

# Prüfen, ob für die aktuelle Änderung überhaupt ein Changeset fehlt
npx changeset status --since=main

In der Praxis lohnt sich ein CI-Check, der einen Pull Request markiert, wenn Code-Dateien in einem Paket geändert wurden, aber kein zugehöriger Changeset vorhanden ist. Dieser einfache Guard verhindert das häufigste Versäumnis im gesamten Workflow: eine funktionale Änderung, die ohne jede Versionsentscheidung durchrutscht und erst beim nächsten Release aus Versehen mit einer falschen Stufe nachgezogen wird.


{
  "$schema": "https://unpkg.com/@changesets/config/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "access": "restricted",
  "baseBranch": "main",
  "ignore": ["@myorg/internal-docs"]
}

4. Fixed vs. Independent Versioning

Changesets unterstützt zwei grundlegende Versionierungsmodi. Im Fixed-Modus werden mehrere Pakete zu einer Gruppe zusammengefasst und erhalten bei jedem Release dieselbe Versionsnummer, ähnlich wie bei einem klassischen Framework mit vielen eng gekoppelten Kernpaketen. Im Independent-Modus, dem Standardverhalten, versioniert jedes Paket in einem TypeScript-Monorepo vollständig unabhängig, entsprechend den tatsächlich für dieses Paket eingereichten Changesets.

Für die meisten TypeScript-Monorepos ist Independent Versioning die passendere Wahl, weil es genau widerspiegelt, welche Pakete sich tatsächlich verändert haben, und unnötige Versionssprünge bei unveränderten Paketen vermeidet. Fixed Versioning lohnt sich hingegen für Pakete, die konzeptionell immer zusammen als eine Einheit released werden sollen, etwa ein Kernpaket und seine offiziellen Plugin-Pakete, die stets in derselben Version zueinander kompatibel sein müssen.


{
  "fixed": [["@myorg/core", "@myorg/core-plugin-auth", "@myorg/core-plugin-cache"]],
  "linked": [],
  "access": "restricted",
  "baseBranch": "main",
  "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
    "onlyUpdatePeerDependentsWhenOutOfRange": true
  }
}

Ein Mischbetrieb ist ebenfalls möglich: Die meisten Pakete eines TypeScript-Monorepos laufen im Independent-Modus, während nur eine kleine, klar abgegrenzte Gruppe eng gekoppelter Kernpakete über das fixed-Array zusammengefasst wird. Diese Kombination deckt in der Praxis die meisten realen Paketstrukturen ab, ohne dass ein Team sich für das gesamte Repository auf einen einzigen Modus festlegen muss.

5. Automatische Changelog-Generierung

Ein häufig unterschätzter Vorteil von Changesets ist die automatische Changelog-Generierung. Beim Ausführen von changeset version werden alle offenen Changeset-Dateien gesammelt, nach betroffenem Paket gruppiert, und in eine formatierte CHANGELOG.md-Datei für jedes Paket im TypeScript-Monorepo geschrieben. Die Beschreibungstexte stammen direkt aus den Changeset-Dateien, die Entwickler beim Anlegen der Änderung geschrieben haben, wodurch das Changelog automatisch aktuell und konsistent formatiert bleibt.

Diese Automatisierung verhindert das klassische Problem handgepflegter Changelogs, bei denen Einträge vergessen werden oder erst kurz vor dem Release nachträglich in Eile zusammengeschrieben werden. Mit einem angepassten Changelog-Generator lässt sich das Format zusätzlich um Links zu Pull Requests, Commit-Hashes oder interne Ticket-Referenzen erweitern, was besonders bei internen Paketen die Nachvollziehbarkeit für andere Teams deutlich verbessert.

Für ein TypeScript-Monorepo mit mehreren Konsumenten-Teams lohnt sich zusätzlich, das Changelog-Format um eine kurze Migrationshinweis-Sektion zu erweitern, die bei Major-Versionen automatisch verlinkt wird. So findet ein Team, das ein internes Paket aktualisiert, sofort die relevante Anleitung, ohne im Chat nachfragen zu müssen, was sich konkret geändert hat und welche Codeanpassungen nötig sind.

6. CI-Integration: Release-PRs automatisiert erstellen

In der Praxis läuft der Changesets-Workflow über eine GitHub-Action namens changesets/action, die bei jedem Merge in den Hauptbranch prüft, ob offene Changeset-Dateien vorliegen. Sind welche vorhanden, erstellt die Action automatisch einen Release-Pull-Request, der bereits die berechneten Versionsänderungen und aktualisierten Changelogs enthält. Ein Merge dieses speziellen Pull Requests löst dann den eigentlichen Publish-Schritt für alle betroffenen Pakete im TypeScript-Monorepo aus.

Dieser Zwei-Schritt-Prozess, erst Sammel-PR, dann echter Publish, gibt Teams die Möglichkeit, mehrere kleine Änderungen zu einem einzigen, überschaubaren Release zusammenzufassen, statt bei jedem einzelnen Merge sofort eine neue Version zu veröffentlichen. Für interne Pakete, die häufig geändert werden, verhindert das eine unnötige Versionsinflation und macht es für Konsumenten einfacher, Updates in überschaubaren, thematisch zusammenhängenden Schritten einzuspielen.

Für Teams, die stattdessen jede Änderung sofort veröffentlichen möchten, etwa bei sehr kleinen, isolierten Utility-Paketen in einem TypeScript-Monorepo, lässt sich der Sammel-Schritt auch überspringen, indem die CI-Pipeline direkt nach jedem Merge changeset version und changeset publish hintereinander ausführt. Dieser Modus eignet sich aber nur, wenn Releases tatsächlich risikoarm genug sind, um ohne zusätzlichen menschlichen Review-Schritt vor dem Publish auszukommen.

7. Interne Pakete ohne npm-Veröffentlichung versionieren

Ein Missverständnis, das bei Changesets häufig auftritt: Man muss keine öffentliche npm-Registry nutzen, um von Changesets zu profitieren. Für rein interne Pakete, die nur innerhalb des TypeScript-Monorepos über Workspace-Symlinks konsumiert werden, kann changeset publish stattdessen gegen eine private Registry wie Verdaccio oder GitHub Packages laufen, oder der Publish-Schritt wird komplett weggelassen und nur die Versions-Bumps sowie Changelogs werden übernommen.

In diesem eingeschränkten Modus dient Changesets ausschließlich als strukturierter Prozess für Versionsentscheidungen und Changelog-Pflege, ohne dass jemals ein tatsächlicher npm-Publish stattfindet. Das ist besonders für Organisationen sinnvoll, die ihre internen TypeScript-Pakete niemals außerhalb der eigenen Infrastruktur bereitstellen wollen, aber trotzdem von nachvollziehbarer Versionshistorie und automatisch generierten Changelogs profitieren möchten.

Wird dennoch eine private Registry eingesetzt, lohnt sich ein zusätzlicher CI-Schritt, der nach jedem Publish prüft, ob die veröffentlichte Version tatsächlich installierbar ist, bevor der Release als abgeschlossen markiert wird. Dieser einfache Smoke-Test verhindert, dass ein fehlerhaft konfiguriertes exports-Feld oder eine vergessene Build-Datei unbemerkt in einem als erfolgreich markierten Release landet.

Wichtig bleibt in jedem Fall, dass die Versionsnummer in package.json und der über das workspace:-Protokoll referenzierte Bereich in abhängigen Paketen zueinander konsistent bleiben. Ein Major-Release, das eine engere Versionsspanne in workspace:^ voraussetzt, sollte immer zusammen mit den entsprechenden Anpassungen in abhängigen internen Paketen desselben TypeScript-Monorepos ausgeliefert werden.

8. Pre-Releases und Snapshot-Versionen für Testzweige

Für Feature-Branches, die vor dem Merge in den Hauptbranch getestet werden sollen, bietet Changesets den Pre-Release-Modus über changeset pre enter next. Dieser Modus hängt einen Pre-Release-Tag wie -next.0 an jede berechnete Version an, sodass Testinstallationen klar von regulären Releases unterscheidbar bleiben, ohne die reguläre Versionshistorie eines Pakets im TypeScript-Monorepo zu verschmutzen.

Für noch kurzlebigere Testzwecke, etwa das schnelle Verifizieren einer einzelnen Pull-Request-Änderung, bietet das Changesets-Ökosystem zusätzlich Snapshot-Releases, die eine eindeutige, an den Commit gebundene Versionsnummer erzeugen, ohne den regulären Versionierungsablauf oder die Changelog-Historie zu beeinflussen. Diese Snapshot-Versionen eignen sich hervorragend, um eine Vorschau eines internen Pakets in einer anderen Anwendung zu testen, bevor die eigentliche Änderung überhaupt gemerged wurde.

Wichtig ist, Pre-Release-Modi und Snapshot-Versionen niemals in derselben CI-Pipeline wie reguläre Releases laufen zu lassen, ohne sie klar zu trennen. Ein versehentlich als Snapshot markiertes reguläres Release oder umgekehrt sorgt in einem TypeScript-Monorepo schnell für Verwirrung darüber, welche Version tatsächlich als stabil gilt und in Produktionscode verwendet werden darf.

9. Versionierungsstrategien im Vergleich

Je nach Kopplungsgrad und Release-Häufigkeit der Pakete in einem TypeScript-Monorepo passt eine andere Versionierungsstrategie besser.

Strategie Wann sinnvoll Nachteil Werkzeug
Globale Versionsnummer Sehr eng gekoppelte Pakete Unnötige Versionssprünge Manuell, kein Tooling nötig
Independent Versioning Lose gekoppelte, viele Pakete Mehr einzelne Changelogs zu pflegen Changesets im Independent-Modus
Fixed Groups Kernpaket plus offizielle Plugins Versionssprünge auch bei unveränderten Plugins Changesets fixed-Konfiguration
Snapshot-Releases Kurzlebige PR-Vorschauen Nicht für dauerhafte Nutzung gedacht changeset version --snapshot

Für die meisten wachsenden TypeScript-Monorepos mit vielen locker gekoppelten internen Paketen ist Independent Versioning mit Changesets die robusteste Wahl, weil es Versionsentscheidungen direkt in den Code-Review-Prozess integriert und Changelogs automatisch konsistent hält, ohne dass ein Release-Verantwortlicher nachträglich manuell recherchieren muss, was sich in welchem Paket tatsächlich geändert hat.

Der Wechsel zwischen diesen Strategien ist außerdem kein Alles-oder-Nichts-Entscheid für die gesamte Lebensdauer eines TypeScript-Monorepos. Es ist üblich, mit Independent Versioning zu starten und erst später, wenn sich klare, eng gekoppelte Paketfamilien herausgebildet haben, gezielt einzelne Fixed-Gruppen zu definieren, statt die Versionierungsstrategie von Anfang an vollständig festzulegen.

Eine kalenderbasierte Versionierung, bei der alle Pakete das aktuelle Jahr und Monat im Versionsstring tragen, wird bewusst nicht empfohlen, weil sie keinerlei Aussage über tatsächliche Breaking Changes trifft und automatisierte Update-Entscheidungen für Konsumenten unmöglich macht. Für interne TypeScript-Monorepo-Pakete bleibt echtes Semantic Versioning über Changesets daher die verlässlichere Wahl gegenüber rein zeitbasierten Alternativen.

Mironsoft

TypeScript-Monorepo-Tooling, Release-Automatisierung und CI/CD

Chaotische Versionierung interner Pakete?

Wir richten Changesets für euer TypeScript-Monorepo ein, definieren Fixed- und Independent-Gruppen passend zur Paketstruktur und automatisieren Release-PRs sowie Changelogs in der CI-Pipeline.

Changesets-Setup

Versionierungsmodus und Konfiguration passend zur Paketstruktur wählen

Release-Automatisierung

Automatisierte Release-PRs und Publish-Schritte in GitLab-CI oder GitHub Actions

Semver-Schulung

Team-Workshops zu korrekter Semver-Einstufung interner Änderungen

10. Zusammenfassung

Changesets löst das Kernproblem der Versionierung interner Pakete in einem TypeScript-Monorepo, indem Versionsentscheidungen als Teil des Code-Reviews getroffen werden, statt sie dem Release-Verantwortlichen im Nachhinein zu überlassen. Independent Versioning spiegelt tatsächliche Änderungen präzise wider, während Fixed Groups für eng gekoppelte Paketfamilien sinnvoll bleiben. Automatisch generierte Changelogs verhindern das klassische Problem vergessener oder inkonsistenter Release-Notizen.

Die CI-Integration über automatisierte Release-Pull-Requests macht den gesamten Prozess reproduzierbar und nachvollziehbar, auch für rein interne Pakete ohne öffentliche npm-Veröffentlichung. Wer Semver-Disziplin und automatisierte Release-Prozesse früh in einem TypeScript-Monorepo etabliert, vermeidet die Versionschaos, das in unstrukturierten Monorepos mit wachsender Paketanzahl fast unvermeidlich entsteht.

Der Einstieg lohnt sich bereits bei einer Handvoll interner Pakete, nicht erst bei fünfzig. Je früher Changesets als fester Bestandteil des Pull-Request-Workflows etabliert wird, desto selbstverständlicher wird die Gewohnheit für neue Teammitglieder, und desto seltener kommt es zu den typischen Versionierungsfehlern, die in einem gewachsenen TypeScript-Monorepo nur noch mit erheblichem Aufwand nachträglich korrigiert werden können.

Am Ende steht ein einfacher Grundsatz: Versionsentscheidungen gehören dorthin, wo der Code tatsächlich verändert wird, nicht in einen separaten, leicht vergessenen Schritt kurz vor dem Release.

Dieser Grundsatz gilt unabhängig davon, ob ein Paket später öffentlich veröffentlicht wird oder für immer intern bleibt.

Versionierung interner TypeScript-Pakete — Das Wichtigste auf einen Blick

Changesets im Review

Versionsentscheidungen werden als Markdown-Datei Teil des Pull Requests und damit des Code-Reviews.

Independent vs. Fixed

Unabhängige Versionierung für lose gekoppelte Pakete, Fixed Groups für eng gekoppelte Kernpaket-Familien.

Automatische Changelogs

Beschreibungstexte aus Changeset-Dateien werden automatisch zu konsistent formatierten CHANGELOG.md-Einträgen.

CI-Automatisierung

Release-Pull-Requests bündeln mehrere Changesets zu einem übersichtlichen, gemeinsamen Release.

11. FAQ: Versionierung interner TypeScript-Pakete

1Öffentliche Registry nötig?
Nein, funktioniert auch privat oder ganz ohne echten Publish-Schritt.
2Fixed vs. Independent?
Fixed gibt derselben Gruppe immer dieselbe Version, Independent versioniert jedes Paket separat.
3Wo Changeset-Datei anlegen?
Im .changeset-Verzeichnis, üblicherweise über npx changeset add erzeugt.
4Falsche Semver-Stufe verhindern?
Semver-Stufe ist Teil des Pull-Request-Reviews, Reviewer sieht sie direkt neben der Codeänderung.
5Was macht changeset version?
Sammelt Changesets, berechnet neue Versionen, aktualisiert package.json und CHANGELOG.md.
6CI-Integration mit changesets/action?
Erstellt automatisch einen Release-PR bei offenen Changesets, Merge löst Publish aus.
7Was sind Snapshot-Releases?
Testversionen an einen Commit gebunden, ideal für PR-Vorschauen ohne Einfluss auf reguläre Versionen.
8Was ist der Pre-Release-Modus?
Versieht Versionen mit einem Tag wie -next.0, trennt Testinstallationen von regulären Releases.
9Pakete von Versionierung ausschließen?
Ja, über das ignore-Feld in der Changesets-Konfiguration.
10Ersetzt es ein Contracts-Paket?
Nein, komplementär: Changesets regelt Versionen, Contracts-Pakete regeln geteilte Typen.