Übung 7 · CI/CD mit GitHub Actions
Jede Änderung automatisch testen und Breaking Changes im Pull Request stoppen.
Jede Änderung automatisch testen und Breaking Changes im Pull Request stoppen.
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.
datacontract breaking stoppenDas 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 -> mainGitHub 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.
Lege die Datei .github/workflows/datacontract.yml an:
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"
doneCommitte und pushe die Datei, dann öffne den Tab Actions. Nach etwa einer Minute ist der Run grün.
Ä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.
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.
Ergänze diese Schritte am Ende des Jobs test:
- 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.yamldatacontract 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.
Ä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.
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.
Vergleiche zwei Kontrakte auf deinem Rechner:
datacontract breaking orders_v1.odcs.yaml orders_v2.odcs.yamlSummary
[ 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
…
1Das entfernte Feld ist ein ERROR, und der Exit-Code ist 1. Genau das lässt eine Pipeline fehlschlagen.
Ergänze einen zweiten Job im Workflow. Er läuft nur für Pull Requests:
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 $statusDie ::error-Zeilen sind GitHub-Annotationen. Sie erscheinen im Pull Request.
Committe und pushe auf main.
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-idSwitched 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.
Der Check Breaking changes schlägt fehl, mit einer Annotation an orders_v2.odcs.yaml.
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.
main an. Jetzt lässt sich kein Breaking Change mehr mergen.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.datacontract changelog orders_v1.odcs.yaml orders_v2.odcs.yaml listet alle Änderungen, nicht nur die brechenden.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.
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.