Deployment Pipeline und CI/CD: Das SaaS-Handbuch 2026

Die Deployment Pipeline ist das Produkt. Ein komplettes Praxis-Handbuch 2026 zu CI/CD-Mustern für mehr als 12 TanStack Start SaaS-Apps auf Cloudflare Workers.

Huifer
Huifer
17. Juli 202612 min read
Auch verfügbar auf:English · 中文

Geschrieben von Huifer, Solo-Entwickler und Maintainer von TanStack Ship. Ich habe dieselbe Struktur der CI/CD-Pipeline seit 2023 in mehr als 12 TanStack Start SaaS-Anwendungen auf Cloudflare Workers im Einsatz und habe persönlich jeden schmerzhaften Teil debuggt: eine Preview-Umgebung, die Produktions-Stripe-Webhook-Secrets durchsickern ließ, eine D1-Migration, die Schreibvorgänge während eines Checkout-Anstiegs blockierte, ein Workers-Rollback, das die falsche Version wiederherstellte, weil die Deploy-ID um eins abwich, ein GitHub Actions-Secret, das mitten in der Pipeline auslief. Jedes nachfolgende Muster ist genau das, welches ich tatsächlich einsetze; jedes Limit verweist auf die offizielle Cloudflare-Dokumentation; jede "Skalierungs"-Behauptung enthält die von mir gemessene Skalierung.

Verifizierte Quellen: Cloudflare Workers Builds (CI/CD) · GitHub Actions documentation · Wrangler environments · D1 migrations · Workers rollbacks · TanStack Start documentation · GitHub Actions encrypted secrets · TanStack Ship features

Zuletzt aktualisiert: 17. Juli 2026 · Changelog


TL;DR: Eine SaaS Deployment Pipeline im Jahr 2026 besteht aus vier Phasen – Build, Test, Stage/Preview und Promote/Release – und die wirkungsvollste Disziplin ist eine echte pro-Pull-Request Preview-Umgebung mit eigener Datenbank, eigenen Secrets und eigener URL. Ich nutze GitHub Actions für die Build-, Test- und Migrationsphasen, weil ich die Kontrolle über das Migrations-Gate und die Testmatrix benötige. Das finale Promote übergebe ich dann an Cloudflare Workers Builds, weil der plattformnative Weg derjenige ist, dem ich am wenigsten traue, wenn etwas schiefgeht. Migrationen sind ein erstklassiges Release-Gate – eine Schema-Änderung, die Schreibvorgänge während des Deploys sperrt, ist ein P0-Vorfall. Observability ist das Release-Kriterium, nicht das Postmortem-Werkzeug. Rollback-Kriterien werden quantifiziert, bevor das Deploy ausgeführt wird.


Warum die Deployment Pipeline das Produkt ist

Es gibt einen Unterschied zwischen „Ich habe eine SaaS-App deployed“ und „Ich habe eine Deployment Pipeline“. Das erste ist eine einmalige Handlung; das zweite ist ein System, das den nächsten Entwickler, die nächste Schema-Änderung, den nächsten Alarm um 3 Uhr morgens überlebt. Über die mehr als 12 TanStack Start-Anwendungen hinweg, die ich auf Cloudflare Workers betreibe, ist die Pipeline der wichtigste Faktor dafür, wie schnell ich liefern kann und wie oft ich ein Rollback durchführen muss.

Dieser Beitrag beschreibt die Pipeline, die ich tatsächlich betreibe, entstanden aus drei Jahren Debugging derselben fünf Probleme: Preview-Leaks, Konflikte durch Migrations-Locks, Rollback-Versionsdrift, Lücken bei der Secret-Rotation und Observability-im-Nachhinein. Er ist die Ergänzung zum Full-Stack Deployment on Cloudflare guide, der die wrangler-Konfiguration und die Runtime-Schicht abdeckt. Die API-Designmuster, die die Pipeline bewahren muss, finden Sie im SaaS API Design guide.

Die vier Phasen einer SaaS Deployment Pipeline

Jede Produktions-Pipeline, die ich betreibe, hat dieselben vier Phasen. Die Namen variieren; die Verantwortlichkeiten nicht.

Build macht aus einem Git-Commit ein deploybares Artefakt. Für eine TanStack Start-App auf Cloudflare Workers ist das pnpm build, welches dist/worker.js und dist/public/ erzeugt. Der Build läuft in der CI mit derselben Node.js-Version (22 LTS) und demselben eingefrorenen Lockfile wie auf meinem Laptop. Diskrepanzen zwischen „funktioniert auf meiner Maschine“ und „funktioniert in der CI“ sind die teuerste Fehlerklasse, die ein Solo-Entwickler ausliefern kann.

Test and verify führt Unit-, Integrations- und Contract-Tests gegen jede externe API aus, von der die App abhängt. Ich nutze die Cloudflare Workers Vitest integration; Miniflare bietet den Tests eine echte workerd-Runtime. Ich plane drei bis fünf Minuten echte Testzeit pro Pull Request ein und parallelisiere nach Testdomäne über die GitHub Actions-Matrix.

Stage and preview deployed das Artefakt in eine Nicht-Produktionsumgebung, die wie die Produktion aufgebaut ist – eine Preview pro Pull Request, mit eigener D1, eigenem R2, eigenen Secrets und eigener URL. Die Preview ist die einzige Umgebung, in der ich einen Stripe-Webhook-Handler, eine D1-Migration oder einen Workers AI-Inferenzaufruf gegen reale Cloudflare-Infrastruktur validieren kann, ohne die Kosten eines Produktionsvorfalls zu tragen. Smoke-Tests, die für die Unit-Phase zu langsam sind, laufen hier: ein vollständiger Checkout-Ablauf, ein echter E-Mail-Versand, ein echter Upload zu R2.

Promote and release ist die einzige Phase, die einen für zahlende Nutzer sichtbaren Zustand ändert. Deshalb sollte sie am schwersten auszulösen und am einfachsten rückgängig zu machen sein. Ich trenne den Auslöser (ein gemergter PR in main) durch eine bewusste Lücke von der Aktion (ein Workers Deploy). Das Deploy läuft in einem separaten Job mit eigenem Service-Konto, einer Freigabeumgebung und einer Rollback-Bereitschaftsprüfung.

GitHub Actions im Vergleich zu Cloudflare Workers Builds

Im Jahr 2026 hat ein Solo-Entwickler, der eine TanStack Start-App auf Cloudflare veröffentlicht, zwei glaubwürdige Wege: GitHub Actions und Cloudflare Workers Builds. Keiner von beiden ist universell besser; die richtige Wahl hängt davon ab, was die Pipeline leisten muss.

Wann GitHub Actions gewinnt

GitHub Actions gewinnt, wenn die Pipeline etwas außerhalb der Workers-Runtime tun muss: eine Testmatrix über verschiedene Runner, ein Migrations-Gate, einen Secret-Rotationsschritt, ein Multi-Target-Deploy (Workers + R2 + Pages in einem Workflow) oder eine Freigabeumgebung, die den Promote-Job pausiert, bis ein Mensch auf „Zulassen“ klickt. Es ist auch der einzige Weg, der mir encrypted secrets scoped to environments bietet – production-Secrets sind aus einem pull_request-Workflow heraus nicht lesbar, was der einzig sichere Standard für eine Preview-Pipeline ist.

Wann Workers Builds gewinnt

Workers Builds gewinnt, wenn die Pipeline exakt lautet: „Build bei Push in den Main-Branch, Deploy in die Produktion.“ Kein Migrationsschritt, keine Testmatrix. Bei einer statischen Seite oder einer Single-Worker-App ist es der einfachere Weg: Verbinden Sie ein GitHub-Repo, wählen Sie einen Branch aus, wählen Sie eine Umgebung, und die Plattform übernimmt den Rest. Kein wrangler deploy-Token, kein YAML. Der Kompromiss besteht darin, dass Workers Builds eine geschlossene Plattform ist: Der Build läuft in der Infrastruktur von Cloudflare, Sie sehen den Runner nicht und Sie können keinen beliebigen Code außerhalb der von Cloudflare erlaubten Build-Befehle ausführen. Wenn die Pipeline jemals einen Schritt benötigt, der nicht „Build und Deploy“ ist, sind Sie wieder bei GitHub Actions.

Die hybride Pipeline, die ich betreibe

Die Pipeline, die ich tatsächlich betreibe, ist hybrider Natur: GitHub Actions für alles bis zum finalen Promote, gefolgt von einer Übergabe für die Produktion. Der nachfolgende Workflow ist das, was ich für jede TanStack Start-App verwende.

yaml
# .github/workflows/deploy.yml
name: Deploy
on:
  pull_request:
    branches: [main]
  push:
    branches: [main]
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: pnpm }
      - run: pnpm install --frozen-lockfile
      - run: pnpm typecheck
      - run: pnpm test
      - run: pnpm build
      - uses: actions/upload-artifact@v4
        with: { name: dist, path: dist/ }

  preview:
    if: github.event_name == 'pull_request'
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: preview
      url: https://preview-${{ github.event.pull_request.number }}.ship-app.dev
    steps:
      - uses: actions/download-artifact@v4
        with: { name: dist }
      - run: pnpm wrangler deploy --env preview
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN_PREVIEW }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}

  migrate-preview:
    if: github.event_name == 'pull_request'
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pnpm wrangler d1 migrations apply ship-preview --env preview --remote
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN_PREVIEW }}

  promote:
    if: github.event_name == 'push'
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://ship-app.com
    steps:
      - uses: actions/download-artifact@v4
        with: { name: dist }
      - run: pnpm wrangler d1 migrations apply ship-prod --env production --remote
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN_PROD }}
      - run: pnpm wrangler deploy --env production
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN_PROD }}
          CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}

Drei Details sind wichtig. Der concurrency-Block bricht laufende Workflows ab, wenn ein neuer Commit eintrifft; ohne ihn würde ein schnell pushender Entwickler drei Deploys in die Warteschlange stellen. Der Job migrate-preview führt Migrationen gegen die Preview-D1 aus, bevor die Preview deployed wird. Der promote-Job nutzt ein GitHub environment mit erforderlichen Prüfern, sodass ein Push in main nicht ohne einen menschlichen Klick deployed wird. Die GitHub Actions environment protection rules bilden das Gate, welches „Ich kann deployen“ von „Ich darf deployen“ trennt.

Preview-Umgebungen als die wirkungsvollste Disziplin

Wenn ich aus diesem Beitrag nur eine Disziplin beibehalten könnte, wäre es die pro-Pull-Request Preview-Umgebung. Es ist die einzige Änderung mit der größten Hebelwirkung, die ich an meiner Pipeline vorgenommen habe; sie fängt Unstimmigkeiten in Binding-Strukturen, D1-Abfragefehler und Routing-Bugs ab, bevor sie die Produktion erreichen. Jede andere Disziplin ist günstiger als die Behebung eines 9-minütigen Ausfalls, der durch ein Feature verursacht wurde, das in Unit-Tests funktionierte und dann gegen echte Cloudflare-Infrastruktur abstürzte.

URL-Konvention und Isolierung

Jeder Pull Request erhält eine URL in der Form preview-<number>.ship-app.dev, die auf ein einzigartiges Workers-Deployment mit eigenen Bindings, Secrets und eigener D1-Datenbank verweist. Das Workers preview deployments platform feature handhabt das URL-Routing; ich kümmere mich um die PR-spezifische Binding-Isolierung im wrangler.jsonc env.preview-Block. Die Wrangler environments documentation ist dabei maßgeblich. Die Disziplin besteht darin, niemals ein Produktions-Binding in einer Preview wiederzuverwenden, nicht einmal schreibgeschützt. Wenn der PR geschlossen wird, wird die Preview abgebaut; eine Preview, die echte Kunden-E-Mails speichert, stellt ein Haftungsrisiko dar.

Datenbank- und Secret-Isolierung

Eine Preview-D1 ist eine eigenständige Datenbank, keine schema-präfixierte Ecke der Produktion. Schema-Präfixe sehen aus wie Isolierung; sie sind es aber nicht. Eine Migration, die die Tabelle users im Preview-Schema sperrt, sperrt diese auch in der Produktion, wenn der Migration-Runner das Präfix nicht berücksichtigt – und der D1-Runner tut dies nicht. Das sichere Muster ist eine D1-Datenbank pro Umgebung und eine pro Preview, falls die Preview eine Migration ausführt. Ich setze die Preview-D1 beim Öffnen des PR zurück und nehme in Kauf, dass der erste PR nach einer Schema-Änderung eine etwas höhere Wahrscheinlichkeit hat, einen Migrationsfehler aufzudecken. Die Kosten für einen übersehenen Migrationsfehler in einer Preview entsprechen einem Review-Zyklus; die Kosten für einen übersehenen Migrationsfehler in der Produktion sind ein Alarm um 3 Uhr morgens.

Preview-Secrets sind keine Produktions-Secrets, niemals. Das GitHub Actions environment secrets-Feature fungiert als Sicherheitsgurt: Secrets, die auf eine preview-Umgebung beschränkt sind, können nicht von einem production-Job gelesen werden und umgekehrt. Ich verwende getrennte Cloudflare-API-Token, die auf Cloudflare-Seite begrenzt sind, sodass ein durchgesickertes Preview-Token nicht in die Produktion deployen kann. Die Direktive ::add-mask:: ist die letzte Verteidigungslinie.

Migrationen als erstklassiges Pipeline-Anliegen

Die größte Klasse von Produktionsvorfällen, die ich in den letzten drei Jahren debuggt habe, bestanden aus Konflikten bei Datenbankmigrationen. Eine Migration, die Schreibvorgänge während eines Deploys sperrt, ist ein P0-Vorfall, keine unbekannte Unbekannte.

Abwärtskompatible Schema-Änderungen

Jede Schema-Änderung, die ich ausliefere, ist vorwärtskompatibel: Der alte Code kann das neue Schema lesen und der neue Code kann das alte Schema lesen. Das Muster ist die expand-contract migration – fügen Sie eine Spalte hinzu, löschen Sie keine; benennen Sie um, indem Sie eine neue Spalte hinzufügen und die Daten in einem Hintergrundjob kopieren, und löschen Sie dann die alte Spalte in einem späteren Deploy. Die Disziplin kostet ein zusätzliches Deploy pro Änderung, und es ist der einzige Weg, ein Rollback einer Workers-Version durchzuführen, ohne eine Datenbankversion zu haben, die nicht mehr passt. Die D1 migrations documentation ist die maßgebliche Quelle für die SQL-Syntax. Die operative Disziplin besteht darin, niemals eine Schema-Änderung mit einer Datenmigration in einem einzigen PR zu kombinieren. Schema in einem PR, Daten im nächsten, Löschen im darauffolgenden PR. Drei Deploys, null Lock-Konflikte während des Übergangs.

Das Migrations-Gate und der Kill-Switch

Das Migrations-Gate ist ein separater Workflow-Job, der vor dem Produktions-Deploy läuft. Er wendet ausstehende Migrationen auf die Produktions-D1 an, validiert, ob die Migration abwärtskompatibel ist, und lässt den Workflow fehlschlagen, falls die Validierung scheitert. Der Kill-Switch ist eine GitHub Actions-Umgebungsvariable, die den Migrations-Job komplett überspringen kann; er wird nur genutzt, wenn die Migration bereits manuell durchgeführt wurde.

yaml
# .github/workflows/migrate.yml
name: Migration Gate
on:
  workflow_call:
    inputs:
      environment:
        required: true
        type: string

jobs:
  migrate:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - uses: actions/checkout@v4
      - uses: pnpm/action-setup@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: pnpm }
      - name: Check for breaking changes
        run: |
          if grep -E "DROP COLUMN|RENAME COLUMN|DROP TABLE" migrations/*.sql; then
            echo "::error::Migration contains a breaking change"
            exit 1
          fi
      - name: Apply migrations
        if: env.SKIP_MIGRATION != 'true'
        run: pnpm wrangler d1 migrations apply ship-${{ inputs.environment }} --env ${{ inputs.environment }} --remote
        env:
          CLOUDFLARE_API_TOKEN: ${{ secrets[format('CF_API_TOKEN_{0}', inputs.environment)] }}
      - name: Verify schema
        run: pnpm wrangler d1 execute ship-${{ inputs.environment }} --env ${{ inputs.environment }} --remote --command "SELECT name FROM sqlite_master WHERE type='table'"

Der Check for breaking changes-Schritt ist der einfachstmögliche Lint: ein grep nach den drei SQL-Mustern, die die Expand-Contract-Disziplin verletzen. Der Apply migrations-Schritt ist an SKIP_MIGRATION geknüpft, sodass ein Operator das Deploy erneut ausführen kann, ohne die Migration ein weiteres Mal anzuwenden.

Observability in der Pipeline verankert

Observability ist das Release-Kriterium, nicht das Postmortem-Werkzeug. Ein Deploy, das ohne eine Möglichkeit zur Messung seiner Auswirkungen ausgerollt wird, ist ein Deploy, das ich im Dunkeln debuggen werde. Die Pipeline sendet bei jedem Deploy drei Signale aus: ein strukturiertes Log, einen Trace der ersten Anfragen und einen synthetischen Check gegen die Produktions-URL.

Jedes Workers Deploy aktiviert standardmäßig observability – observability: { enabled: true } ist das Erste, was ich in wrangler.jsonc einstelle, und das Letzte, was ich deaktivieren würde. Der Log-Tail ist die erste Stelle, auf die ich schaue, wenn ein Deploy fehlschlägt; der Trace ist die zweite. Die Pipeline bringt beides als Release-Gates an die Oberfläche: Der promote-Job schlägt fehl, wenn das Deploy-Log innerhalb der ersten 30 Sekunden einen Fehler zeigt, und ein nachgeschalteter synthetischer Check lässt den Workflow scheitern, wenn die p99-Latenz für die Checkout-Route die Basislinie um mehr als 20 % überschreitet.

Ein synthetischer Check ist eine automatisierte Anfrage, die die wichtigste Route testet – bei einer SaaS also den Checkout oder das passwortgeschützte Dashboard. Der Check läuft alle 5 Minuten gegen die Produktion und einmal bei jedem Deploy. Der Post-Deploy-Check hat eine engere Toleranz: 500 ms p99 anstelle von 1500 ms, vollen Abgleich der Antwortstruktur anstelle von nur HTTP 200 und einen separaten Check pro Umgebung. Ein synthetischer Check, der nicht der Struktur der Route entspricht, ist schlimmer als gar kein Check, da das drohende Falsch-Positiv das Team dahingehend trainiert, den Alarm zu ignorieren.

Was ich messe und wann ich ein Rollback durchführe

Der schwierigste Teil einer Deployment Pipeline ist das Rollback. Eine Pipeline, die Rollbacks nicht einfach macht, führt dazu, dass Rollbacks selten bleiben, und ein Rollback, das selten ist, findet nicht statt, wenn es eigentlich sollte.

Rollback-Kriterien – vor dem Deploy quantifiziert

Jedes Produktions-Deploy hat ein in der Pull Request-Beschreibung definiertes Rollback-Kriterium, noch bevor der Code gemerged wird. Das Kriterium nimmt, abhängig von der Risikoklasse, eine von drei Formen an. Latenz-Regression: Die p99-Latenz überschreitet die 7-Tage-Basislinie über 5 aufeinanderfolgende Minuten hinweg um mehr als 20 %. Fehlerraten-Regression: Die 5xx-Fehlerrate überschreitet 0,5 % für 3 aufeinanderfolgende Minuten. Business-Regression: Die Checkout-Abschlussrate fällt 30 Minuten nach dem Deploy im Vergleich zur selben Stunde in der Vorwoche um mehr als 10 %. Das Kriterium wird deklariert und nicht erst zum Zeitpunkt des Rollbacks erfunden; die Diskussion darüber, ob das Symptom schlimm genug ist, kostet mehr Zeit als das Rollback selbst.

Das Rollback-Playbook

Die Workers rollbacks documentation beschreibt die plattformnative Handhabung: Jedes Deploy ist mit einer Deploy-ID versehen und ein Rollback ist ein einzelner wrangler rollback-Befehl (oder ein Klick im Dashboard), welcher auf die vorherige Deploy-ID zurücksetzt. Das plattformnative Rollback ist schnell – typischerweise unter 60 Sekunden – und es ist der einzige Rollback-Pfad, den ich für Workers-Code verwende. Das Datenbank-Rollback ist schwieriger, weswegen ich bezüglich der obigen Migrationsdisziplin am ehesten zu starker Strenge neige.

Das komplette Playbook: Bestätigen Sie, dass das Kriterium erfüllt wurde; führen Sie wrangler rollback --env production aus; falls das Deploy eine Migration beinhaltete, schätzen Sie ab, ob sie reveribel ist (falls nicht, betrifft das Rollback nur die Workers-Version und die Migration bleibt bestehen); posten Sie ein Status-Update (eine SaaS ohne Statusseite verschweigt Vorfälle); eröffnen Sie ein Postmortem mit der Rollback-Zeit, den Auswirkungen auf den Kunden und dem Kriterium, welches das Rollback ausgelöst hat. Der fünfte Schritt ist derjenige, der aus einem Rollback eine Pipeline-Verbesserung macht. Ein Rollback, dem kein Postmortem folgt, ist ein Rollback, das wieder passieren wird.

Fazit: Die Pipeline, der ich vertraue

Eine Pipeline, der ich vertraue, ist eine, die ich auch an einem Freitagnachmittag zu mergen bereit bin. Die vierstufige Form, die hybride Übergabe zwischen GitHub Actions und Workers Builds, die pro-PR Preview, das Migrations-Gate, die Observability-Release-Kriterien, die quantifizierte Rollback-Regel – nichts davon ist neu. Jedes Muster ist eine gut dokumentierte Praxis, jeder Befehl steht in der offiziellen Cloudflare- oder GitHub Actions-Dokumentation, jedes von mir genannte Limit ist ein Limit, an das ich tatsächlich gestoßen bin. Das Neue liegt in der Disziplin: Jedes dieser Muster funktioniert nur, wenn es vor dem Vorfall verdrahtet ist, nicht danach.

Wenn Ihre Pipeline lautet: „Mergere in main, führe wrangler deploy aus, hoffe“, dann ist die weitreichendste Änderung in dieser Woche die Preview-Umgebung. Der Rest der Pipeline kann davon ausgehend wachsen.

Für Informationen zu Runtime, Bindings und wrangler-Konfiguration ist der Full-Stack Deployment on Cloudflare guide die perfekte Begleitlektüre. Für die API-Designmuster, siehe den SaaS API Design guide. Für die Kostenberechnung zeigt die TanStack Ship pricing page den Plan mit der in diesem Beitrag vorverdrahteten Pipeline-Struktur; die feature highlights, comparison guides und der alternatives index decken den Rest ab.