Datenkontrakte in der Praxis
Fortschritt
0%
ende
Los geht’s
  • Willkommen15′
  • Setup20′
Teil A · Das Quell-Datenprodukt
  • 1.Daten unter Vertrag nehmen60′
  • 2.Evolution von Datenkontrakten30′
  • 3.Datenprodukt beschreiben20′
Teil B · Das Consumer-Aligned Data Product
  • 4.Contract-first entwerfen30′
  • 5.Datenprodukt implementieren25′
  • 6.Consumer-driven Contracts30′
Teil C · Automatisieren
  • 7.CI/CD mit GitHub Actions45′
Teil D · Datenplattformoptional
  • 8.Auf Entropy Data veröffentlichen40′
  • 9.Semantik25′
Abschluss
  • Abschluss10′
Teil C · Automatisieren

Übung 7 · CI/CD mit GitHub Actions

Jede Änderung automatisch testen und Breaking Changes im Pull Request stoppen.

~45 Min.0 von 10 Schritten erledigt
Zurück
Consumer-driven Contracts
Weiter
Auf Entropy Data veröffentlichen
Gepflegt vonEntropy Data

Deine Kontrakte werden nur getestet, wenn jemand datacontract test ausführt. Irgendwann vergisst es jemand, und ein kaputter Kontrakt rutscht durch. In diesem Kapitel übernimmt GitHub Actions das für dich: Jeder Push lintet und testet alle Kontrakte, und jeder Pull Request wird vor dem Merge auf Breaking Changes geprüft.

Das lernst du
  • Kontrakte, Datenprodukte und SQL in deinem Fork versionieren
  • ODCS- und ODPS-Dateien bei jedem Push linten
  • Alle Kontrakte in der Pipeline gegen eine Datenbank testen
  • Breaking Changes in Pull Requests mit datacontract breaking stoppen
Contracts as Code

Ein Kontrakt ist eine YAML-Datei in Git. Er wird also behandelt wie Code: Reviews, Pull Requests und automatische Checks. Die Pipeline prüft drei Dinge:

  1. Syntax: Ist jede Datei gültiges ODCS oder ODPS? (lint)
  2. Realität: Passen die Daten zum Kontrakt? (ci)
  3. Kompatibilität: Bricht eine Änderung die Konsumenten? (breaking)

Je früher ein Problem auffällt, desto günstiger ist es zu beheben. Das nennt man Shift Left.

Arbeit pushen

Committe deine Arbeit in deinen Fork

Das Workshop-Repository ignoriert die Dateien, die du in den Übungen erstellst. In deinem Fork sind sie dein Quellcode. Öffne .gitignore und lösche den letzten Block: den Kommentar # files created during the exercises und die fünf Zeilen darunter.

Deine Views sollten schon in sql/sku_sales_input.sql und sql/sku_sales_per_year.sql liegen (aus Consumer-driven Contracts). Die Pipeline wendet sie in dieser Reihenfolge an.

Committen und pushen:

git add .gitignore sql/ *.odcs.yaml *.odps.yaml
git commit -m "Add data contracts, data products, and views"
git push
[main 6ee37e4] Add data contracts, data products, and views
 9 files changed, 601 insertions(+)
 create mode 100644 orders.odps.yaml
 create mode 100644 orders_v1.odcs.yaml
 …
 create mode 100644 sql/sku_sales_per_year.sql
Enumerating objects: 14, done.
Counting objects: 100% (14/14), done.
Delta compression using up to 18 threads
Compressing objects: 100% (11/11), done.
Writing objects: 100% (12/12), 5.32 KiB | 5.32 MiB/s, done.
Total 12 (delta 1), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/<your-username>/learn.datacontract.com.git
   18f0f52..6ee37e4  main -> main
Achtung

Dein Fork ist öffentlich. Committe niemals einen API-Key. Prüfe mit git diff .env, dass .env nur die Workshop-Zugangsdaten enthält.

GitHub Actions im Fork aktivieren

GitHub deaktiviert Workflows in Forks standardmäßig. Öffne den Tab Actions deines Forks auf GitHub und klicke auf I understand my workflows, go ahead and enable them.

Bei jedem Push linten

Workflow anlegen

Lege die Datei .github/workflows/datacontract.yml an:

.github/workflows/datacontract.yml
name: Data Contracts

on:
  push:
    branches: [main]
  pull_request:

env:
  COLUMNS: 200 # wider tables in the logs

jobs:
  test:
    name: Lint and test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

      - uses: astral-sh/[email protected]

      - name: Install the CLIs
        run: |
          uv tool install --python 3.11 'datacontract-cli[postgres]==1.2.2'
          uv tool install --python 3.11 'dataproduct-cli==0.2.0'

      - name: Lint data contracts
        run: |
          for file in *.odcs.yaml; do
            datacontract lint "$file"
          done

      - name: Lint data products
        run: |
          for file in *.odps.yaml; do
            dataproduct lint "$file"
          done

Committe und pushe die Datei, dann öffne den Tab Actions. Nach etwa einer Minute ist der Run grün.

Den Lint brechen

Ändere in orders_v1.odcs.yaml den logicalType von order_id auf text. Das ist kein ODCS-Logical-Type. Pushe und öffne den fehlgeschlagenen Run. Das Log nennt das ungültige Feld und listet die erlaubten Werte.

Mach die Änderung rückgängig und pushe erneut.

Gegen die Datenbank testen

Linten prüft die Syntax. Jetzt prüfst du die Realität: Die Pipeline startet die Workshop-Datenbank, legt deine Views an und testet jeden Kontrakt.

Datenbank-Tests ergänzen

Ergänze diese Schritte am Ende des Jobs test:

YAML
      - name: Start the database
        run: docker compose up -d --wait

      - name: Create the views
        run: |
          docker compose exec -T postgres psql -U workshop -d workshop -v ON_ERROR_STOP=1 < sql/sku_sales_input.sql
          docker compose exec -T postgres psql -U workshop -d workshop -v ON_ERROR_STOP=1 < sql/sku_sales_per_year.sql

      - name: Test data contracts
        run: datacontract ci *.odcs.yaml

datacontract ci ist datacontract test für Pipelines. Es testet mehrere Kontrakte auf einmal, schreibt eine Zusammenfassung auf die Run-Seite und annotiert fehlgeschlagene Checks an der Kontraktdatei. Die Datenbank-Zugangsdaten kommen aus der Datei .env in deinem Repository.

Pushe, öffne den Run und scrolle nach unten zur Summary.

Der Workflow-Lauf im Actions-Tab: beide Jobs, ausgelöst durch einen Push auf main
Die Step Summary von datacontract ci: alle vier Kontrakte bestanden, darunter jeder einzelne Check

Einen Test fehlschlagen lassen

Ändere in sku_sales_per_year.odcs.yaml den physicalType von total_quantity auf integer und pushe. Der Run schlägt fehl. Finde die Annotation: Die Spalte ist bigint, nicht integer.

Mach die Änderung rückgängig und pushe erneut.

Platzhalter für Staging und Produktion

Die Datenbank in der Pipeline ist ein Platzhalter. In einem echten Setup testet die Pipeline vor jedem Deployment eine Staging-Umgebung. Ein zeitgesteuerter Workflow (on: schedule: mit Cron-Ausdruck) testet regelmäßig die Produktion, denn Daten können auch ohne Code-Änderung kaputtgehen.

Breaking Changes stoppen

Ein Breaking Change im Kontrakt bricht die Konsumenten. Die Pipeline soll ihn vor dem Merge abfangen. Die Idee: Jeder Kontrakt im Pull Request wird mit seiner Version auf dem Base-Branch verglichen.

datacontract breaking lokal ausprobieren

Vergleiche zwei Kontrakte auf deinem Rechner:

datacontract breaking orders_v1.odcs.yaml orders_v2.odcs.yaml
Summary
[ 11 Warning ]  [ 6 Info ]
╭──────────┬─────────┬───────────────────────────────────────────────────────────╮
│ Severity │ Change  │ Field                                                     │
├──────────┼─────────┼───────────────────────────────────────────────────────────┤
│ INFO     │ Updated │ id                                                        │
│ WARNING  │ Updated │ schema.line_items.properties.lines_item_id.quality.[1]    │
│ INFO     │ Added   │ schema.line_items.properties.quantity                     │
│ WARNING  │ Updated │ schema.line_items.properties.sku.quality.[1]              │
│ …        │         │                                                           │
│ WARNING  │ Updated │ schema.orders.quality.[2]                                 │
│ INFO     │ Updated │ servers.postgres                                          │
│ INFO     │ Updated │ version                                                   │
╰──────────┴─────────┴───────────────────────────────────────────────────────────╯

Details
…

Das neue Feld quantity ist INFO. Die geänderten SQL-Quality-Queries sind WARNINGs: einen Blick wert, aber nicht brechend. Der Exit-Code ist 0. Vergleiche jetzt in die andere Richtung, als würdest du quantity wieder entfernen:

datacontract breaking orders_v2.odcs.yaml orders_v1.odcs.yaml
echo $?
Summary
[ 1 Error ]  [ 11 Warning ]  [ 5 Info ]
╭──────────┬─────────┬───────────────────────────────────────────────────────────╮
│ Severity │ Change  │ Field                                                     │
├──────────┼─────────┼───────────────────────────────────────────────────────────┤
│ INFO     │ Updated │ id                                                        │
│ WARNING  │ Updated │ schema.line_items.properties.lines_item_id.quality.[1]    │
│ ERROR    │ Removed │ schema.line_items.properties.quantity                     │
│ …        │         │                                                           │
│ INFO     │ Updated │ version                                                   │
╰──────────┴─────────┴───────────────────────────────────────────────────────────╯

Details
…
1

Das entfernte Feld ist ein ERROR, und der Exit-Code ist 1. Genau das lässt eine Pipeline fehlschlagen.

Breaking-Change-Check ergänzen

Ergänze einen zweiten Job im Workflow. Er läuft nur für Pull Requests:

YAML
  breaking-changes:
    name: Breaking changes
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with:
          fetch-depth: 0 # the base branch is needed for the comparison

      - uses: astral-sh/[email protected]

      - name: Install the CLI
        run: uv tool install --python 3.11 'datacontract-cli[postgres]==1.2.2'

      - name: Compare with the base branch
        env:
          BASE: origin/${{ github.base_ref }}
        run: |
          status=0
          # every contract on the base branch must stay compatible (new contracts have nothing to compare)
          for file in $(git ls-tree --name-only "$BASE" | grep '\.odcs\.yaml
#x27;); do
if [ ! -f "$file" ]; then echo "::error file=$file::Data contract $file was removed" status=1 continue fi git show "$BASE:$file" > "$RUNNER_TEMP/$file" if ! datacontract breaking "$RUNNER_TEMP/$file" "$file"; then echo "::error file=$file::Breaking change in $file. Release a new major version instead." status=1 fi done exit $status

Die ::error-Zeilen sind GitHub-Annotationen. Sie erscheinen im Pull Request. Committe und pushe auf main.

Pull Request mit Breaking Change öffnen

Jetzt bist du das Orders-Team und willst eine Spalte „aufräumen“. Lege einen Branch an und entferne das Feld customer_id aus dem Schema orders in orders_v2.odcs.yaml:

git switch -c remove-customer-id
# orders_v2.odcs.yaml bearbeiten: customer_id entfernen
git commit -am "Remove customer_id"
git push -u origin remove-customer-id
Switched to a new branch 'remove-customer-id'
[remove-customer-id 86465a7] Remove customer_id
 1 file changed, 10 deletions(-)
Enumerating objects: 5, done.
Counting objects: 100% (5/5), done.
Delta compression using up to 18 threads
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 305 bytes | 305.00 KiB/s, done.
Total 3 (delta 2), reused 0 (delta 0), pack-reused 0 (from 0)
remote:
remote: Create a pull request for 'remove-customer-id' on GitHub by visiting:
remote:      https://github.com/<your-username>/learn.datacontract.com/pull/new/remove-customer-id
remote:
To https://github.com/<your-username>/learn.datacontract.com.git
 * [new branch]      remove-customer-id -> remove-customer-id
branch 'remove-customer-id' set up to track 'origin/remove-customer-id'.

Öffne einen Pull Request auf GitHub.

Wähle deinen Fork als Base

Bei einem Fork schlägt GitHub das ursprüngliche Workshop-Repository als Base vor. Ändere base repository auf <dein-username>/learn.datacontract.com und die Base auf main.

Der Check Breaking changes schlägt fehl, mit einer Annotation an orders_v2.odcs.yaml.

Der Pull Request: Der Check Breaking changes schlägt fehl, Lint and test bleibt grün
Das Job-Log: datacontract breaking meldet das entfernte customer_id als ERROR

Stattdessen kompatibel ändern

Stelle customer_id wieder her. Ergänze stattdessen eine description oder einen neuen Tag. Pushe auf denselben Branch. Der Check wird grün, und du kannst mergen.

Eine Änderung, an die sich Konsumenten anpassen müssen, braucht eine neue Major-Version: einen neuen Kontrakt, wie orders_v2 für orders_v1 in Evolution von Datenkontrakten.

Kurzer Check
Ein Pull Request entfernt customer_id aus orders_v2, die Spalte existiert in der Datenbank aber weiterhin. Welcher Check lässt die Pipeline fehlschlagen?

Bonus

  • Checks verpflichtend machen: Gehe in deinem Fork zu Settings → Rules → Rulesets und fordere die Checks Lint and test und Breaking changes für main an. Jetzt lässt sich kein Breaking Change mehr mergen.
  • Consumer-driven Contracts in der Pipeline des Producers: Dein Consumer-Kontrakt orders_v2.consumer_sku_sales.odcs.yaml läuft in derselben Pipeline. In einem echten Setup testet das Orders-Team die Kontrakte aller seiner Konsumenten. Ein fehlschlagender Check zeigt dann genau, wen eine Änderung bricht.
  • Changelog lesen: datacontract changelog orders_v1.odcs.yaml orders_v2.odcs.yaml listet alle Änderungen, nicht nur die brechenden.
  • Testergebnisse veröffentlichen: In Teil D kannst du die Ergebnisse aus der Pipeline an Entropy Data senden. Der Referenz-Workflow in der Lösung enthält den Schritt als Kommentar.

Bonus: Tests mit Airflow einplanen

CI testet einen Kontrakt, wenn er sich ändert. Die Daten ändern sich aber täglich, also brauchen Produktionstests auch einen Zeitplan. Laufen deine Pipelines in Apache Airflow, bringt der Data Contract Provider für Airflow einen DataContractTestOperator mit: Er führt datacontract test als Quality Gate im DAG aus und lässt den Task fehlschlagen, wenn der Kontrakt verletzt ist. So stoppen schlechte Daten, bevor sie nachgelagerte Tasks erreichen.

dags/orders_contract.py
from datetime import datetime
from airflow.sdk import dag
from datacontract_provider.operators.datacontract import DataContractTestOperator


@dag(schedule="0 2 * * *", start_date=datetime(2026, 1, 1), catchup=False)
def orders_contract():
    DataContractTestOperator(
        task_id="test_orders_v2",
        data_contract_file="orders_v2.odcs.yaml",
        server="Orders",
        server_conn_id="workshop_postgres",  # Airflow-Connection mit den Datenbank-Zugangsdaten
    )


orders_contract()

Installation mit pip install "airflow-provider-datacontract[postgres]". Connections, das Veröffentlichen von Ergebnissen und die Ergebnisansicht beschreibt die Scheduling-Doku für Airflow der Data Contract CLI. Kein Airflow? Ein schedule:-Trigger in GitHub Actions macht dasselbe, siehe die Scheduling-Doku für GitHub Actions.