Wer Software entwickelt, kennt den klassischen Ablauf: Code ändern, Projekt kompilieren, Tests durchführen und anschließend die neue Version auf einen Server übertragen. Solange ein Projekt klein ist, kann man diese Schritte problemlos manuell durchführen. Mit zunehmender Größe und häufigeren Änderungen wird dieser Prozess jedoch schnell fehleranfällig.
Mit Visual Studio, GitHub und GitHub Actions lässt sich dieser Ablauf vollständig automatisieren. Nach einem Push prüft GitHub automatisch, ob die Anwendung kompiliert und alle Tests erfolgreich sind. Ist alles in Ordnung, kann die neue Version anschließend automatisch auf einem Server oder bei einem Cloud-Anbieter bereitgestellt werden.
Genau das bezeichnet man als CI/CD-Pipeline.
In diesem Beitrag erstellen wir beispielhaft eine Pipeline für eine ASP.NET-Core-Webanwendung.
Was bedeutet CI/CD?
CI/CD steht für:
Continuous Integration (CI)
und
Continuous Delivery beziehungsweise Continuous Deployment (CD).
Continuous Integration
Bei Continuous Integration wird jede Änderung automatisch geprüft.
Ein typischer Ablauf sieht so aus:
Entwickler ändert Code
↓
Commit
↓
Push zu GitHub
↓
Projekt kompilieren
↓
Tests ausführen
↓
Ergebnis anzeigen
Dadurch erkennt man beispielsweise sofort, wenn eine Änderung dazu führt, dass das Projekt nicht mehr kompiliert oder ein automatisierter Test fehlschlägt.
Continuous Deployment
Continuous Deployment geht noch einen Schritt weiter.
Nachdem Build und Tests erfolgreich abgeschlossen wurden, wird die neue Version automatisch veröffentlicht:
Push
↓
Build
↓
Tests
↓
Publish
↓
Deployment
↓
Neue Version läuft
Damit kann eine Änderung wenige Minuten nach dem Push automatisch auf dem Zielsystem verfügbar sein.
Was benötigen wir?
Für unser Beispiel benötigen wir:
- Visual Studio
- ein .NET-Projekt
- ein GitHub-Konto
- ein GitHub-Repository
- automatisierte Tests
- GitHub Actions
- ein Ziel für das Deployment
Als Beispiel für das Deployment verwenden wir einen Azure App Service. Das grundsätzliche Prinzip ist jedoch auch auf Linux-Server, Docker-Hosts oder andere Cloud-Plattformen übertragbar.
1. Das Projekt in Visual Studio erstellen
Zunächst benötigen wir ein normales .NET-Projekt.
Beispielsweise:
MyWebApp
│
├── MyWebApp
│ ├── Program.cs
│ ├── appsettings.json
│ └── ...
│
└── MyWebApp.Tests
└── ...
Zusätzlich zum eigentlichen Projekt sollte möglichst bereits ein Testprojekt vorhanden sein.
In Visual Studio könnte die Solution beispielsweise so aussehen:
Solution 'MyWebApp'
│
├── MyWebApp
│
└── MyWebApp.Tests
Damit können wir später automatisiert überprüfen, ob die Anwendung weiterhin korrekt funktioniert.
2. Das Projekt mit Git verbinden
Visual Studio besitzt eine integrierte Git-Unterstützung.
Ist das Projekt noch kein Git-Repository, kann über Visual Studio ein neues Repository angelegt werden.
Anschließend werden die Dateien über einen Commit in Git gespeichert.
Zum Beispiel:
Initial commit
Technisch entspricht das ungefähr:
git add .
git commit -m "Initial commit"
Von nun an werden Änderungen über Git versioniert.
3. Repository auf GitHub erstellen
Als Nächstes wird das lokale Repository mit GitHub verbunden.
Visual Studio kann direkt ein neues GitHub-Repository erstellen und das vorhandene Projekt hochladen.
Danach sieht die Struktur beispielsweise so aus:
Visual Studio
│
│ Commit
▼
lokales Git-Repository
│
│ Push
▼
GitHub Repository
GitHub wird damit zur zentralen Stelle für unseren Quellcode.
4. Den ersten CI-Workflow erstellen
Jetzt beginnt die eigentliche Automatisierung.
GitHub Actions sucht Workflow-Dateien im Verzeichnis:
.github/workflows/
Wir erstellen dort beispielsweise:
.github/workflows/ci.yml
Ein einfacher Workflow für ein aktuelles .NET-Projekt könnte folgendermaßen aussehen:
name: CI
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Repository herunterladen
uses: actions/checkout@v6
- name: .NET installieren
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Abhängigkeiten wiederherstellen
run: dotnet restore
- name: Projekt bauen
run: dotnet build --no-restore --configuration Release
- name: Tests ausführen
run: dotnet test --no-build --configuration Release
Damit haben wir bereits eine einfache CI-Pipeline.
Was passiert jetzt?
Sobald wir eine Änderung auf den Branch main pushen, startet GitHub automatisch den Workflow.
Push auf main
│
▼
GitHub Actions
│
▼
Repository herunterladen
│
▼
.NET installieren
│
▼
dotnet restore
│
▼
dotnet build
│
▼
dotnet test
│
▼
✅ erfolgreich
oder
❌ fehlgeschlagen
Der Entwickler muss Build und Tests damit nicht mehr ausschließlich lokal durchführen.
GitHub überprüft den Stand des Repositorys noch einmal unabhängig auf einem frischen Runner.
5. Was ist ein GitHub Runner?
Die Befehle des Workflows müssen irgendwo ausgeführt werden.
Dafür verwendet GitHub sogenannte Runner.
In unserem Workflow steht:
runs-on: ubuntu-latest
GitHub startet dafür eine Linux-Umgebung und führt dort unseren Workflow aus.
Alternativ stehen beispielsweise auch Windows und macOS zur Verfügung:
runs-on: windows-latest
oder:
runs-on: macos-latest
Für viele ASP.NET-Core-Anwendungen reicht ein Ubuntu-Runner vollkommen aus.
6. Pull Requests automatisch überprüfen
Die CI-Pipeline ist besonders praktisch in Verbindung mit Pull Requests.
Angenommen, wir entwickeln ein neues Feature auf einem eigenen Branch:
main
│
└── feature/login
Nach der Entwicklung erstellen wir einen Pull Request:
feature/login
↓
Pull Request
↓
main
Da unser Workflow auf pull_request reagiert, führt GitHub automatisch Build und Tests durch.
Pull Request
│
├── Build ✅
│
└── Tests ✅
│
▼
Merge möglich
Schlägt dagegen ein Test fehl:
Pull Request
│
├── Build ✅
│
└── Tests ❌
sollte die Änderung zunächst korrigiert werden.
7. Branch Protection einrichten
GitHub kann zusätzlich verhindern, dass fehlerhafter Code direkt in main übernommen wird.
Dazu können für den Hauptbranch Schutzregeln eingerichtet werden.
Beispielsweise:
- Pull Request erforderlich
- erfolgreicher CI-Workflow erforderlich
- bestimmte Reviews erforderlich
- direkte Pushes auf
mainverhindern
Damit entsteht ein kontrollierter Entwicklungsprozess:
Feature Branch
│
▼
Pull Request
│
▼
CI
┌────┴────┐
│ │
Build Tests
│ │
└────┬────┘
│
▼
Review
│
▼
Merge
│
▼
main
8. Aus CI wird CI/CD
Bis jetzt überprüft GitHub unsere Anwendung lediglich.
Nun soll sie nach einem erfolgreichen Merge auf main zusätzlich veröffentlicht werden.
Dazu erweitern wir die Pipeline um einen Deployment-Schritt.
Unser Ablauf sieht dann so aus:
Visual Studio
│
▼
Git Commit
│
▼
GitHub
│
▼
GitHub Actions
│
├── Restore
├── Build
├── Test
├── Publish
│
▼
Deployment
│
▼
Produktionsserver
9. Anwendung mit dotnet publish vorbereiten
Für das Deployment erzeugen wir zunächst eine veröffentlichbare Version unserer Anwendung.
Dafür verwenden wir:
dotnet publish --configuration Release --output ./publish
Der Inhalt des Ordners publish enthält anschließend die Dateien, die auf dem Zielsystem benötigt werden.
Im Workflow sieht das so aus:
- name: Anwendung veröffentlichen
run: dotnet publish ./MyWebApp/MyWebApp.csproj --configuration Release --output ./publish
10. Zugangsdaten nicht im Workflow speichern
Für das Deployment benötigt GitHub Zugang zum Zielsystem.
Passwörter, API-Schlüssel oder andere Zugangsdaten dürfen niemals direkt in der Workflow-Datei gespeichert werden.
Also nicht:
password: MeinGeheimesPasswort123
GitHub stellt dafür Secrets zur Verfügung.
Diese können in den Einstellungen des Repositorys hinterlegt und anschließend im Workflow verwendet werden.
Beispielsweise:
${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
Der eigentliche Inhalt des Secrets steht damit nicht im Repository.
11. Deployment nach Azure App Service
Für eine ASP.NET-Core-Webanwendung bietet sich beispielsweise Azure App Service an.
Nachdem dort eine Web App angelegt wurde, können die benötigten Deployment-Zugangsdaten als GitHub Secret gespeichert werden.
Der Deployment-Step könnte dann beispielsweise so aussehen:
- name: Anwendung deployen
uses: azure/webapps-deploy@v3
with:
app-name: meine-webapp
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
package: ./publish
GitHub überträgt damit die zuvor erzeugte Anwendung automatisch zu Azure.
12. Der komplette CI/CD-Workflow
Unsere Workflow-Datei könnte jetzt folgendermaßen aussehen:
name: CI-CD
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- name: Repository herunterladen
uses: actions/checkout@v6
- name: .NET installieren
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Abhängigkeiten wiederherstellen
run: dotnet restore
- name: Projekt bauen
run: dotnet build --no-restore --configuration Release
- name: Tests ausführen
run: dotnet test --no-build --configuration Release
deploy:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs:
- build-test
runs-on: ubuntu-latest
environment:
name: production
steps:
- name: Repository herunterladen
uses: actions/checkout@v6
- name: .NET installieren
uses: actions/setup-dotnet@v4
with:
dotnet-version: '10.0.x'
- name: Anwendung veröffentlichen
run: dotnet publish ./MyWebApp/MyWebApp.csproj --configuration Release --output ./publish
- name: Anwendung deployen
uses: azure/webapps-deploy@v3
with:
app-name: meine-webapp
publish-profile: ${{ secrets.AZURE_WEBAPP_PUBLISH_PROFILE }}
package: ./publish
Damit besitzen wir bereits eine vollständige CI/CD-Pipeline.
Warum gibt es zwei Jobs?
Der Workflow enthält:
build-test
und:
deploy
Das ist bewusst so gewählt.
Der Deployment-Job besitzt folgende Abhängigkeit:
needs:
- build-test
Das bedeutet:
Deployment darf erst beginnen, wenn Build und Tests erfolgreich abgeschlossen wurden.
Schlägt beispielsweise ein Test fehl:
Build ✅
Tests ❌
│
X
Deployment
wird keine neue Version veröffentlicht.
Erst bei:
Build ✅
Tests ✅
│
▼
Deployment
wird die Anwendung bereitgestellt.
13. Pull Requests dürfen nicht automatisch deployen
Unsere Pipeline wird sowohl bei Pushes als auch bei Pull Requests gestartet.
Wir möchten allerdings nicht jeden Pull Request sofort produktiv veröffentlichen.
Deshalb besitzt der Deployment-Job diese Bedingung:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
Damit läuft das Deployment ausschließlich bei einem echten Push auf main.
Der Ablauf eines Pull Requests sieht also so aus:
Pull Request
↓
Build
↓
Tests
↓
Ende
Nach dem Merge:
Merge nach main
↓
Push auf main
↓
Build
↓
Tests
↓
Publish
↓
Deploy
14. GitHub Environments verwenden
Für Deployments bietet GitHub sogenannte Environments an.
Typische Umgebungen wären:
development
staging
production
Unser Deployment verwendet beispielsweise:
environment:
name: production
Ein Environment kann zusätzliche Regeln besitzen.
Zum Beispiel:
- Deployment nur von
main - manuelle Freigabe erforderlich
- eigene Secrets
- Deployment-Historie
- Schutzregeln
Damit lässt sich ein wesentlich sichererer Deployment-Prozess aufbauen.
15. Manuelle Freigabe für Produktion
Eine interessante Variante besteht darin, Build und Tests vollständig automatisch auszuführen, das tatsächliche Produktionsdeployment aber zunächst freigeben zu lassen.
Der Ablauf wäre:
Push auf main
↓
Build
↓
Tests
↓
Publish
↓
Warte auf Freigabe
↓
[Approve]
↓
Deployment
Damit erhält man die Vorteile der Automatisierung, behält aber die Kontrolle darüber, wann eine neue Version tatsächlich produktiv geht.
16. Staging und Production
Noch professioneller ist eine zusätzliche Staging-Umgebung.
Der Ablauf könnte dann folgendermaßen aussehen:
Feature Branch
↓
Pull Request
↓
Build + Tests
↓
Merge nach main
↓
Build
↓
Tests
↓
Deploy Staging
↓
Integrationstest
↓
Freigabe
↓
Deploy Production
Dadurch kann eine Version zunächst in einer produktionsähnlichen Umgebung getestet werden.
Erst anschließend wird sie für die eigentlichen Benutzer veröffentlicht.
17. Build-Artefakte verwenden
Bei größeren Pipelines empfiehlt es sich, die Anwendung nur einmal zu bauen.
Das Ergebnis kann als sogenanntes Artifact gespeichert werden.
Beispielsweise:
- name: Build-Artefakt hochladen
uses: actions/upload-artifact@v4
with:
name: webapp
path: ./publish
Ein späterer Deployment-Job kann dieses Artefakt wieder herunterladen.
Damit wird exakt die Version deployt, die zuvor getestet wurde.
Der Ablauf sieht dann so aus:
Quellcode
↓
Build
↓
Tests
↓
Artifact
↓
Staging
↓
Production
Dieses Vorgehen ist für größere CI/CD-Pipelines empfehlenswert.
18. Wo endet Visual Studio und wo beginnt GitHub?
Visual Studio ist in diesem Prozess hauptsächlich die Entwicklungsumgebung.
Hier wird:
- programmiert
- getestet
- committed
- gepusht
- mit Branches gearbeitet
- ein Pull Request vorbereitet
GitHub übernimmt anschließend die zentrale Automatisierung.
┌───────────────────────┐
│ Visual Studio │
│ │
│ Code │
│ lokale Tests │
│ Git │
│ Commit │
│ Push │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ GitHub │
│ │
│ Repository │
│ Pull Requests │
│ Reviews │
│ GitHub Actions │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ CI/CD │
│ │
│ Restore │
│ Build │
│ Test │
│ Publish │
│ Deploy │
└───────────┬───────────┘
│
▼
┌───────────────────────┐
│ Produktionssystem │
└───────────────────────┘
19. Der tägliche Entwicklungsablauf
Ist die Pipeline einmal eingerichtet, verändert sie den normalen Entwicklungsprozess kaum.
Ein typischer Ablauf sieht dann so aus:
1. Feature-Branch erstellen
feature/neue-funktion
2. In Visual Studio entwickeln
Code schreiben und lokal testen.
3. Commit erstellen
Beispielsweise:
Add customer search
4. Zu GitHub pushen
Visual Studio
↓
GitHub
5. Pull Request erstellen
GitHub startet automatisch die CI-Pipeline.
6. Build und Tests überprüfen
Build ✅
Tests ✅
7. Pull Request mergen
Der Code landet auf main.
8. CD-Pipeline startet
Build
↓
Test
↓
Publish
↓
Deploy
9. Neue Version ist verfügbar
Damit ist der komplette Prozess abgeschlossen.
20. Was sollte nicht automatisch deployt werden?
Nicht jedes Projekt benötigt sofort Continuous Deployment.
Bei geschäftskritischer Software kann beispielsweise sinnvoll sein:
Continuous Integration
+
Continuous Delivery
statt:
Continuous Deployment
Der Unterschied liegt darin, dass bei Continuous Delivery die Anwendung automatisch bis zur Veröffentlichungsreife vorbereitet wird, das eigentliche Deployment aber bewusst freigegeben werden muss.
Gerade für Produktionssysteme ist das häufig eine sinnvolle Lösung.
21. Sicherheit nicht vergessen
Eine CI/CD-Pipeline besitzt Zugriff auf wichtige Systeme und sollte entsprechend abgesichert werden.
Einige grundlegende Regeln sind:
- Passwörter niemals im Repository speichern
- GitHub Secrets verwenden
- Produktionszugänge möglichst auf das Notwendige beschränken
- Branch Protection beziehungsweise Rulesets verwenden
- Pull Requests vor dem Merge prüfen
- Deployment-Environments einsetzen
- Produktionsdeployments gegebenenfalls manuell freigeben
- verwendete GitHub Actions regelmäßig aktualisieren
Für professionelle Azure-Deployments kann außerdem OpenID Connect (OIDC) verwendet werden. Dadurch lassen sich langlebige Zugangsdaten teilweise vermeiden.
22. CI/CD ist mehr als nur automatisches Deployment
Der größte Vorteil einer Pipeline besteht nicht darin, dass man sich das manuelle Kopieren von Dateien spart.
Der entscheidende Vorteil ist ein reproduzierbarer Prozess.
Jede Version durchläuft dieselben Schritte:
Restore
↓
Build
↓
Test
↓
Publish
↓
Deploy
Dadurch hängt die Qualität einer Veröffentlichung nicht mehr davon ab, ob ein Entwickler daran gedacht hat, bestimmte Schritte manuell auszuführen.
Fazit
Mit Visual Studio, GitHub und GitHub Actions lässt sich mit relativ wenig Aufwand eine vollständige CI/CD-Pipeline für .NET-Anwendungen aufbauen.
Visual Studio übernimmt dabei die lokale Entwicklung und Git-Verwaltung. GitHub speichert den zentralen Quellcode und organisiert Branches und Pull Requests. GitHub Actions führt anschließend automatisiert Build, Tests und Deployment durch.
Aus:
Code schreiben
↓
manuell bauen
↓
manuell testen
↓
Dateien manuell übertragen
wird damit:
Code schreiben
↓
Commit & Push
↓
GitHub Actions
↓
Build
↓
Tests
↓
Deployment
Für den Einstieg reicht bereits eine kleine CI-Pipeline aus, die bei jedem Pull Request automatisch dotnet build und dotnet test ausführt.
Im nächsten Schritt kann daraus eine vollständige Pipeline mit Artifacts, Staging-Umgebung, manueller Produktionsfreigabe und automatischem Deployment entstehen.
Damit wird GitHub von einer reinen Quellcodeverwaltung zu einem zentralen Bestandteil des gesamten Entwicklungs- und Veröffentlichungsprozesses.
