GitHub Actions ist das eingebaute CI/CD-System von GitHub. Du definierst Workflows als YAML-Dateien, die bei Push, Pull Request oder nach Zeitplan laufen. In diesem Artikel zeige ich dir alles: von deinem ersten Workflow über Matrix-Builds und Docker-Images bis hin zu Self-Hosted Runnern und wiederverwendbaren Workflows.
Stell dir vor, du pushst Code und alles passiert automatisch: Linting, Tests, Build, Deployment. Kein manuelles Rumgefummel mehr. Klingt gut? Genau das macht CI/CD, und GitHub Actions ist der einfachste Weg dahin.
🤔 Was ist CI/CD und warum sollte es dich interessieren?
CI (Continuous Integration) bedeutet, dass dein Code bei jedem Push automatisch getestet wird. CD (Continuous Deployment/Delivery) sorgt dafür, dass getesteter Code automatisch deployed wird. Zusammen bilden sie das Rückgrat moderner Softwareentwicklung.
Ohne CI/CD passiert Folgendes: Du vergisst Tests auszuführen. Ein Kollege merged kaputten Code. Das Deployment ist ein Freitagnachmittag-Albtraum. Mit CI/CD? Push, lehne dich zurück, fertig.
GitHub Actions ist direkt in GitHub integriert, kein externer Service, kein zusätzliches Setup. Du brauchst nur eine YAML-Datei in deinem Repository.
🧩 Die Grundkonzepte: Workflows, Jobs, Steps, Runner
Bevor wir loslegen, ein kurzer Überblick über die Bausteine:
- Workflow: Eine YAML-Datei unter
.github/workflows/. Definiert, wann und was passiert. - Job: Eine Gruppe von Schritten, die auf demselben Runner laufen. Jobs können parallel oder sequenziell laufen.
- Step: Ein einzelner Befehl oder eine Action. Steps laufen nacheinander innerhalb eines Jobs.
- Runner: Die Maschine, auf der dein Job ausgeführt wird. GitHub stellt kostenlose Runner bereit (Ubuntu, Windows, macOS), oder du nutzt deine eigenen.
- Action: Ein wiederverwendbarer Baustein. Tausende davon findest du im GitHub Marketplace.
🚀 Dein erster Workflow: Lint und Test bei Push/PR
Fangen wir einfach an. Erstelle die Datei .github/workflows/ci.yml in deinem Repository:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Test
run: npm testDas war's. Bei jedem Push auf main und bei jedem Pull Request werden Linting und Tests automatisch ausgeführt. Der grüne Haken oder das rote X siehst du direkt im PR.
🔢 Matrix-Builds: Teste auf mehreren Node.js-Versionen
Du willst sicherstellen, dass dein Code auf verschiedenen Node.js-Versionen läuft? Matrix-Builds machen das trivial:
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20, 22]
steps:
- uses: actions/checkout@v4
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm testGitHub erstellt automatisch drei parallele Jobs, einen für jede Version. Du kannst die Matrix auch für verschiedene Betriebssysteme erweitern:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node-version: [18, 20]
runs-on: ${{ matrix.os }}Das gibt dir 6 Kombinationen. Automatisch. Ohne Copy-Paste.
⚡ Caching: Schnellere Builds durch Zwischenspeicherung
Jedes Mal npm ci laufen zu lassen dauert. Mit Caching sparst du wertvolle Minuten:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ciDie setup-node Action hat Caching direkt eingebaut. Alternativ kannst du den generischen Cache verwenden:
- name: Cache node_modules
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-Erster Build? Normal langsam. Jeder weitere? Deutlich schneller. Bei großen Projekten spart das locker 2-3 Minuten pro Run.
🔐 Secrets und Umgebungsvariablen
API-Keys, Tokens, Passwörter, die gehören nicht in deinen Code. GitHub Secrets sind dafür da:
# Secrets über die GitHub CLI setzen
gh secret set MY_API_KEY --body "super-secret-value"
gh secret set DOCKER_PASSWORD --body "my-docker-password"Im Workflow greifst du so darauf zu:
env:
API_URL: https://api.example.com
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
env:
API_KEY: ${{ secrets.MY_API_KEY }}
run: |
curl -H "Authorization: Bearer $API_KEY" $API_URL/deployWichtig: Secrets werden in Logs automatisch maskiert. Du siehst nur *** statt des eigentlichen Werts. Und in Pull Requests von Forks sind Secrets nicht verfügbar, ein wichtiger Sicherheitsmechanismus.
🐳 Docker-Images bauen und pushen
Docker und GitHub Actions sind ein Dreamteam. So baust und pushst du ein Image zu Docker Hub oder GitHub Container Registry:
jobs:
docker:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}Das Schöne: GITHUB_TOKEN wird automatisch bereitgestellt. Kein extra Secret nötig für die GitHub Container Registry.
🌐 Deployment: Vercel, Netlify oder per SSH
Der Build ist durch, die Tests sind grün, jetzt ab auf den Server.
Vercel:
- name: Deploy to Vercel
uses: amondnet/vercel-action@v25
with:
vercel-token: ${{ secrets.VERCEL_TOKEN }}
vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
vercel-args: '--prod'SSH auf einen eigenen Server:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/myapp
git pull origin main
npm ci --production
pm2 restart myappEgal ob PaaS oder Bare Metal, für alles gibt es eine passende Action.
♻️ Wiederverwendbare Workflows und Composite Actions
Copy-Paste über mehrere Repos hinweg? Nein danke. Wiederverwendbare Workflows lösen das elegant:
# .github/workflows/reusable-test.yml
name: Reusable Test Workflow
on:
workflow_call:
inputs:
node-version:
required: false
type: string
default: '20'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- run: npm ci
- run: npm testAufrufen kannst du ihn dann so:
# .github/workflows/ci.yml
jobs:
test:
uses: ./.github/workflows/reusable-test.yml
with:
node-version: '20'Composite Actions sind noch granularer, perfekt für wiederverwendbare Step-Gruppen, die du über Repos hinweg teilen willst.
🏗️ Praxisbeispiel: Die komplette Pipeline
Jetzt setzen wir alles zusammen. Eine echte Pipeline: Lint, Test, Build, Deploy, mit Abhängigkeiten zwischen den Jobs:
name: Full Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- run: npm ci
- run: npm test
build:
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: build-output
path: dist/
deploy:
needs: build
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: build-output
path: dist/
- name: Deploy
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
echo "Deploying build artifacts..."
# Dein Deployment-Befehl hierBeachte die needs-Eigenschaft: Build wartet auf Lint und Test. Deploy wartet auf Build. Und Deploy läuft nur auf main bei einem Push, nicht bei Pull Requests. Sauber.
🏠 Self-Hosted Runner fürs Homelab
Die GitHub-Runner sind super für die meisten Projekte. Aber manchmal brauchst du mehr Kontrolle: spezielle Hardware, Zugriff aufs lokale Netzwerk oder einfach unbegrenzte Build-Minuten.
# Runner auf deinem Server einrichten
mkdir actions-runner && cd actions-runner
curl -o actions-runner-linux-x64.tar.gz -L https://github.com/actions/runner/releases/latest/download/actions-runner-linux-x64-2.311.0.tar.gz
tar xzf ./actions-runner-linux-x64.tar.gz
./config.sh --url https://github.com/DEIN-USER/DEIN-REPO --token DEIN-TOKEN
./run.shIm Workflow wechselst du einfach den Runner:
jobs:
build:
runs-on: self-hosted
steps:
- uses: actions/checkout@v4
- run: echo "Läuft auf meinem eigenen Server!"Für Homelab-Setups ist das Gold wert. Du kannst direkt auf lokale Dienste zugreifen, Docker-Images lokal bauen und alles in deinem Netzwerk deployen.
💰 Pricing: Was kostet der Spaß?
| Plan | Minuten/Monat | Speicher | Preis |
|---|---|---|---|
| Free | 2.000 | 500 MB | $0 |
| Team | 3.000 | 2 GB | $4/User/Monat |
| Enterprise | 50.000 | 50 GB | $21/User/Monat |
Wichtig: Die Minuten gelten nur für private Repos. Öffentliche Repos haben unlimitierte Minuten. Und Self-Hosted Runner verbrauchen keine Minuten, ein weiterer Grund fürs Homelab.
macOS-Runner kosten 10x so viel wie Linux-Runner. Windows-Runner das Doppelte. Wenn du sparen willst: Linux nehmen und macOS-Builds nur wenn wirklich nötig.
💡 Fazit
GitHub Actions ist ein mächtiges Werkzeug, das direkt in deinem Repository lebt. Kein externer Service, kein kompliziertes Setup. Du schreibst eine YAML-Datei und bekommst dafür automatisierte Tests, Builds und Deployments.
Fang klein an: Ein einfacher CI-Workflow mit Lint und Tests. Dann bau Schritt für Schritt aus, Caching, Matrix-Builds, Docker, Deployment. Und wenn du mehr Kontrolle brauchst, stell dir einen Self-Hosted Runner ins Homelab.
Die 2.000 kostenlosen Minuten pro Monat reichen für die meisten persönlichen Projekte locker aus. Und für Open-Source-Projekte? Komplett kostenlos. Keine Ausrede mehr, CI/CD nicht einzusetzen.
Happy automating! 🚀