Ein Test, der nur auf deinem Laptop läuft, schützt den nächsten Commit nicht. Trotzdem würde ich als Juniorentwickler nicht mit einer komplexen Deployment-Pipeline anfangen: Solange lokale Befehle nicht reproduzierbar sind, automatisiert CI lediglich Überraschungen. Der bessere Einstieg ist ein kleines Python-Projekt, in dem derselbe Prüfablauf zuerst auf deinem Rechner und danach bei jedem Pull Request läuft.
Ein funktionierender lokaler Befehl ist wichtiger als die erste Pipeline
Starte mit einem neuen Repository, damit du jeden Schritt nachvollziehen kannst. Du brauchst Git, Python und uv als Paket- und Umgebungsmanager. Installiere uv über dessen offizielle Installationsanleitung und prüfe mit uv –version, ob es erreichbar ist. Lege dann den Ordner mini-api an, wechsle hinein und führe uv init –lib aus. Das erzeugt ein Python-Paket samt pyproject.toml. Mit uv python pin 3.12 hältst du die gewünschte Python-Version in .python-version fest. Die Versionsangabe ist hier eine Projektentscheidung, kein Versprechen, dass jede andere Version fehlschlägt.
Installiere die Prüfwerkzeuge mit uv add –dev pytest ruff. uv schreibt die Abhängigkeiten in pyproject.toml und löst sie in uv.lock auf. Committe beide Dateien: Die Projektdatei beschreibt, was du verlangst; das Lockfile hält fest, welche konkreten Pakete dazu aufgelöst wurden. Eine lokal erzeugte virtuelle Umgebung wie .venv gehört dagegen nicht ins Repository, weil sie Dateien für genau einen Rechner enthält. Prüfe vor dem ersten Commit mit git status, was Git tatsächlich erfassen würde.
Jetzt braucht der Ablauf einen echten Test. Erstelle unter src/mini_api/__init__.py eine Funktion total(values: list[int]) -> int, die sum(values) zurückgibt. Lege tests/test_total.py an und prüfe dort mit pytest, dass total([2, 3]) == 5 gilt und total([]) == 0. Der zweite Fall ist nützlich, weil er das Verhalten bei leerer Eingabe ausdrücklich festlegt. Führe anschließend uv run ruff check . und uv run pytest -q aus. Beide Befehle müssen ohne Fehler enden, bevor du CI hinzufügst.
Automatisierung und Skripting in der Softwareentwicklung bietet den größeren Rahmen; für diesen Einstieg würde ich ihn bewusst auf zwei ausführbare Prüfungen verengen, weil du einen kaputten Test so ohne zusätzliche Infrastruktur untersuchen kannst. Ich würde auch nicht zuerst ein Skript bauen, das Installation, Formatierung, Tests und Git-Commit gleichzeitig erledigt: Bei einem Fehlschlag verschleiert ein solcher Sammelbefehl, welcher Schritt korrigiert werden muss.
CI sollte dieselben Prüfungen ausführen, nicht neue Regeln erfinden
Wenn beide lokalen Befehle funktionieren, erstelle das Repository auf GitHub und lege die Datei .github/workflows/checks.yml an. GitHub Actions startet den folgenden Workflow bei Pushes und Pull Requests. actions/checkout@v4 holt den Quellcode, astral-sh/setup-uv@v6 stellt uv bereit, und uv sync –locked bricht ab, falls Projektdatei und Lockfile nicht zusammenpassen. Die letzten beiden Prüfungen sind dieselben Befehle, die du bereits lokal ausgeführt hast.
name: Checks
on: [push, pull_request]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- run: uv sync --locked
- run: uv run ruff check . && uv run pytest -q
Die Datei ist ein vollständiger Workflow für das zuvor angelegte Projekt. ubuntu-24.04 wählt das Runner-Image ausdrücklich, statt stillschweigend dem jeweils voreingestellten Image zu folgen. Die beiden Action-Versionen sind Tags; bei strengeren Anforderungen an die Lieferkette kannst du sie nach Prüfung durch vollständige Commit-SHAs ersetzen, weil ein Tag grundsätzlich verschoben werden kann. Für diesen reinen Prüfjob reichen Leserechte auf den Repository-Inhalt. Gib ihm keine Deployment-Secrets: Pull Requests sollen Tests ausführen können, ohne Zugang zu produktiven Systemen zu erhalten.
GitHub dokumentiert für einen einzelnen Job auf einem GitHub-gehosteten Runner eine maximale Laufzeit von 6 Stunden; diese vom Anbieter veröffentlichte Grenze ist für zwei kleine Prüfungen kein sinnvolles Ziel. Setze nach den ersten erfolgreichen Läufen bei Bedarf ein eigenes timeout-minutes am Job: 10 Minuten sind ein anpassbarer Anfangswert, keine gemessene Dauer dieses Projekts. Damit hängt ein versehentlich wartender Test nicht bis zur Plattformgrenze. Für ein so kleines Repository würde ich noch keinen Cache ergänzen, weil ein zusätzlicher Cache-Schlüssel eine weitere Fehlerquelle wäre, bevor du überhaupt weißt, ob die Installation spürbar Zeit kostet.
Teste die Pipeline mit einer absichtlichen Änderung: Ersetze im Test kurz die erwartete 5 durch 6, öffne einen Pull Request und kontrolliere, ob pytest im Actions-Log fehlschlägt. Stelle danach die richtige Erwartung wieder her. Dieser kontrollierte Fehler belegt mehr als ein grünes Symbol, weil er zeigt, dass Änderungen am Test tatsächlich bis zum Runner gelangen.
Ein Lockfile lohnt sich hier mehr als ein zusätzlicher Werkzeugwechsel
Für ein neues Python-Repository würde ich uv wählen, weil uv.lock und uv sync –locked den Abgleich zwischen lokalem Rechner und CI zu einem überprüfbaren Schritt machen. pip mit venv gewinnt dagegen, wenn du in ein bestehendes Projekt mit gepflegter requirements.txt einsteigst oder deine Organisation zusätzliche Paketmanager nicht zulässt. Der Preis von uv ist die zusätzliche Installation und ein Workflow, den das Team lernen muss; der Preis von pip mit venv ist hier, dass du das Festschreiben und Prüfen der Abhängigkeiten selbst konsistent organisieren musst. Ein Wechsel eines laufenden Teams allein für diesen kleinen Vorteil wäre keine gute erste Automatisierungsaufgabe.
Auch innerhalb des uv-Setups brauchst du eine klare Grenze: Ruff prüft in diesem Beispiel den Code, pytest das Verhalten. Ein grüner Ruff-Lauf sagt nicht, dass total([]) richtig implementiert ist; ein grüner pytest-Lauf sagt nicht, dass andere Dateien frei von den durch Ruff erkannten Problemen sind. Behalte deshalb beide Befehle getrennt im Kopf, selbst wenn die YAML-Zeile sie mit && verbindet. Dieser Operator startet pytest nur, wenn Ruff erfolgreich war; im Log erkennst du damit, welche Prüfung zuerst scheiterte.
Miss die Laufzeit erst an deinem eigenen Projekt, bevor du optimierst. Zeigt deine Stoppuhr für beide lokalen Befehle zusammen beispielsweise 18 Sekunden, ist das ein Messwert auf deinem Rechner und keine allgemeine Leistungszusage für uv, Ruff oder pytest. Liegen Installation und Prüfungen auf dem Runner deutlich auseinander, lies zuerst die Zeitstempel im Actions-Log. Nur wenn wiederholte Läufe zeigen, dass das Herunterladen der Pakete den Job dominiert, lohnt sich ein Cache als nächster Versuch. Ändere dann genau eine Sache und vergleiche erneut, statt gleichzeitig Cache, Teststruktur und Abhängigkeiten umzubauen.
Automatisierung und Skripting: Effiziente DevOps Workflows ist ein sinnvoller nächster Schritt; mit Deployment würde ich trotzdem warten, bis dieser Prüfjob verlässlich scheitert und wieder grün wird, weil eine automatisch ausgelieferte Änderung sonst dieselben unbekannten Fehler nur schneller verteilt. Die Grenze ist praktisch: Der Job darf Code lesen und prüfen, aber noch nichts veröffentlichen.
Ein roter Test ist erst nützlich, wenn du ihn lokal nachstellen kannst
Wenn CI fehlschlägt, beginne beim ersten fehlgeschlagenen Schritt im Log, nicht beim letzten roten Statussymbol. Scheitert uv sync –locked, prüfe, ob nach einer Änderung an pyproject.toml auch uv.lock aktualisiert und committet wurde. Scheitert Ruff, führe lokal uv run ruff check . aus und korrigiere die gemeldete Datei. Scheitert pytest, starte uv run pytest -q und lies die konkrete Assertion. So behandelst du den Fehler als reproduzierbaren Befund statt als Eigenheit von GitHub.
Falls der Fehler nur in CI auftritt, vergleiche zuerst die Umgebung: Steht .python-version im Commit? Läuft lokal tatsächlich Python 3.12? Verwendet ein Test Dateipfade, eine Zeitzone oder Umgebungsvariablen, die nur auf deinem Rechner vorhanden sind? Ein Test sollte seine benötigten Beispieldateien aus dem Repository lesen oder sie mit pytest-Fixtures selbst anlegen. Deinen persönlichen Download-Ordner als Testeingabe würde ich nicht verwenden, weil ein frischer Runner ihn nicht besitzt.
Halte den nächsten Ausbau an einem konkreten Fehler fest. Wenn wiederholt Formatabweichungen auftauchen, prüfe zusätzlich uv run ruff format –check . und nimm denselben Befehl in CI auf. Wenn Tests wegen externer Dienste schwanken, ersetze diese Abhängigkeit im betreffenden Test durch eine kontrollierte Fixture oder trenne einen Integrationstest ab. Installiere dagegen nicht vorsorglich weitere Prüfer: Jeder zusätzliche Check erzeugt Konfiguration und Wartungsarbeit, ohne schon ein beobachtetes Problem zu lösen.
Erst wenn der Job auf Pull Requests zuverlässig reagiert, richte in GitHub eine Branch-Schutzregel ein, die Checks / test vor dem Merge verlangt. Der genaue Name muss zum angezeigten Statuscheck passen; kontrolliere ihn in einem echten Pull Request, bevor du die Regel für andere verbindlich machst. Die Regel verhindert keinen fehlerhaften Test, aber sie verhindert, dass ein bereits erkannter Fehler durch einen gewöhnlichen Merge übergangen wird.
Der nächste Commit sollte einen absichtlichen Fehler enthalten
Lege heute das kleine Paket, einen Test und die beiden lokalen Prüfbefehle an. Committe danach den Workflow und ändere in einem separaten Pull Request kurz eine erwartete Zahl, bis CI sichtbar rot wird. Korrigiere sie erst dann. Wenn du diesen Wechsel von grün zu rot und zurück selbst auslösen kannst, hast du ein funktionierendes Setup – und einen klaren Ausgangspunkt für den nächsten echten Test.


