Wer “GitLab CI vs GitHub Actions” sucht, findet Feature-Vergleichstabellen. Haekchen links, Haekchen rechts, am Ende steht überall “beide können alles”. Das stimmt — und ist trotzdem nutzlos. Die eigentliche Frage ist nicht, was die Systeme können, sondern wie sie dich dazu bringen, Pipelines zu denken. Und da unterscheiden sich GitLab CI und GitHub Actions fundamental.
Pipeline-Philosophie: Deklarativ vs. Event-Driven
GitLab CI denkt in Stages. Eine Pipeline ist eine geordnete Abfolge von Phasen: build, test, deploy. Jobs innerhalb einer Stage laufen parallel, Stages selbst sequenziell. Das Modell ist deklarativ — du beschreibst den Endzustand, nicht den Kontrollfluss.
GitHub Actions denkt in Events. Ein Workflow reagiert auf ein Ereignis: Push, Pull Request, Schedule, manueller Dispatch. Der Workflow definiert Jobs, Jobs definieren Steps. Die Beziehungen zwischen Jobs sind explizit über needs verdrahtet — es gibt kein implizites Stage-Modell.
Das klingt nach einem kleinen Unterschied. In der Praxis praegt es den gesamten Pipeline-Aufbau:
- GitLab: Du definierst einmal, was in welcher Reihenfolge passiert. Neue Jobs ordnen sich in bestehende Stages ein. Das funktioniert gut, solange die Pipeline linear ist.
- GitHub Actions: Du definierst Reaktionen auf Events. Komplexe Abhängigkeitsgraphen sind natürlich, aber du musst sie explizit modellieren.
Runner-Architektur im Vergleich
Beide Systeme trennen Orchestrierung von Ausführung. Die Plattform plant, der Runner führt aus. Aber die Details unterscheiden sich.
GitLab Runner ist ein einzelnes Binary, das verschiedene Executors unterstützt: Shell, Docker, Kubernetes, VirtualBox, SSH. Du registrierst einen Runner an einer GitLab-Instanz und weist ihm Tags zu. Jobs waehlen Runner über Tags aus. Das Tagging-System ist simpel und mächtig — ein Runner kann mehrere Tags haben, ein Job kann mehrere Tags fordern.
GitHub Actions Runner läuft entweder als GitHub-hosted Runner (vorkonfigurierte VMs) oder als Self-hosted Runner. Die Zuweisung erfolgt über Labels, konzeptionell aehnlich wie GitLab-Tags. GitHub-hosted Runner sind der Standardfall und für die meisten Projekte ausreichend. Der Self-hosted Runner ist ein .NET-Prozess, der sich als Listener an die GitHub API hängt.
Der größte praktische Unterschied: GitLab bietet mit dem Docker-Executor und dem Kubernetes-Executor zwei Optionen, die Container-basierte Builds ohne Docker-in-Docker ermöglichen. Bei GitHub Actions musst du entweder die vorkonfigurierten VMs nehmen oder dich um die Container-Runtime auf Self-hosted Runnern selbst kuemmern.
Pipeline-Definition im Vergleich
Schauen wir uns eine typische Pipeline an — Build, Test, Deploy — in beiden Systemen.
stages:
- build
- test
- deploy
variables:
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
build:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker build -t $DOCKER_IMAGE .
- docker push $DOCKER_IMAGE
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
test:
stage: test
image: $DOCKER_IMAGE
script:
- npm run test:unit
- npm run test:integration
coverage: '/Statements\s*:\s*(\d+\.?\d*)%/'
deploy:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl set image deployment/app app=$DOCKER_IMAGE
environment:
name: production
url: https://example.com
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: manual name: CI/CD Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
DOCKER_IMAGE: ghcr.io/${{ github.repository }}:${{ github.sha }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
push: true
tags: ${{ env.DOCKER_IMAGE }}
test:
needs: build
runs-on: ubuntu-latest
container:
image: ${{ env.DOCKER_IMAGE }}
steps:
- run: npm run test:unit
- run: npm run test:integration
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
steps:
- uses: azure/k8s-set-context@v3
with:
kubeconfig: ${{ secrets.KUBECONFIG }}
- run: kubectl set image deployment/app app=${{ env.DOCKER_IMAGE }} Auf den ersten Blick aehnlich. Aber beachte die Unterschiede: GitLab braucht services: [docker:dind] für Docker Builds. GitHub Actions delegiert an vorgefertigte Actions (docker/build-push-action). GitLab definiert Abhängigkeiten implizit über Stages, GitHub Actions explizit über needs.
Wiederverwendbarkeit: Templates vs. Composite Actions
Hier trennt sich die Spreu vom Weizen in größeren Organisationen.
GitLab bietet drei Mechanismen:
include: YAML-Dateien einbinden, lokal oder remoteextends: Jobs von Templates erben lassen!reference: Einzelne Keys aus anderen Jobs referenzieren
GitHub Actions bietet ebenfalls drei Wege:
- Reusable Workflows: Ganze Workflows als aufrufbare Einheiten (
workflow_call) - Composite Actions: Mehrere Steps als eine Action buendeln
- Starter Workflows: Org-weite Templates für neue Repositories
# templates/docker-build.yml (in einem zentralen Repository)
.docker-build:
image: docker:24
services:
- docker:24-dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
# .gitlab-ci.yml im Projekt
include:
- project: 'devops/ci-templates'
ref: main
file: '/templates/docker-build.yml'
build:
extends: .docker-build
variables:
DOCKER_BUILDKIT: "1" # .github/workflows/docker-build.yml (im Template-Repository)
name: Docker Build
on:
workflow_call:
inputs:
image-name:
required: true
type: string
secrets:
registry-password:
required: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/build-push-action@v5
with:
push: true
tags: ${{ inputs.image-name }}:${{ github.sha }}
# .github/workflows/ci.yml im Projekt
name: CI
on: [push]
jobs:
build:
uses: org/ci-templates/.github/workflows/docker-build.yml@main
with:
image-name: ghcr.io/${{ github.repository }}
secrets:
registry-password: ${{ secrets.GITHUB_TOKEN }} GitLabs extends-System ist flexibler — du kannst einzelne Keys überschreiben, Arrays mergen, beliebig tief verschachteln. Das ist mächtig, führt aber in großen Organisationen zu YAML-Konstrukten, die niemand mehr versteht. GitHub Actions’ Reusable Workflows sind eingeschraenkter, aber expliziter: Inputs und Outputs sind klar definiert.
Security: Secrets, OIDC und Protected Environments
Beide Systeme bieten verschlüsselte Secrets auf Projekt- und Organisationsebene. Die Unterschiede liegen im Detail:
GitLab trennt Variablen nach Umgebungen und schützt sie über Protected Branches und Protected Tags. Masked Variables werden in Logs geschwuerzt. Seit GitLab 16 gibt es native OIDC-Token für Cloud-Provider-Authentifizierung — keine langlebigen Credentials mehr für AWS, GCP oder Azure.
GitHub Actions bietet Environment Secrets, Repository Secrets und Organization Secrets. Environments können Reviewer-Approvals und Wait Timer haben. OIDC ist seit 2022 verfügbar und gut dokumentiert.
Mono-Repo Support und Pipeline-Performance
Bei Mono-Repos zeigt sich, wie gut ein CI-System skaliert.
GitLab bietet rules:changes — Jobs laufen nur, wenn sich Dateien in bestimmten Pfaden geändert haben. Mit needs und dem DAG-Modus (Directed Acyclic Graph) lassen sich Abhängigkeiten zwischen Jobs unabhängig von Stages definieren. Das Parent-Child-Pipeline-Feature erlaubt es, dynamisch Sub-Pipelines zu erzeugen.
GitHub Actions hat paths und paths-ignore auf Workflow-Ebene, aber nicht auf Job-Ebene. Für feinere Kontrolle brauchst du Workarounds: dorny/paths-filter oder manuelle Checks mit git diff. Dafuer ist die Parallelisierung über matrix elegant — du definierst eine Matrix von Parametern, und GitHub Actions erzeugt automatisch einen Job pro Kombination.
Die Pipeline-Performance wird bei beiden Systemen von Caching und Artefakt-Management dominiert. GitLab hat einen integrierten Cache pro Runner, GitHub Actions nutzt actions/cache mit einem zentralen Cache-Backend. In der Praxis sind beide aehnlich schnell, solange du dich um Caching kuemmerst.
Self-hosted Runner: Setup, Kosten, Wartung
Sobald GitHub-hosted Runner oder GitLab SaaS-Runner nicht mehr reichen — spezielle Hardware, Compliance-Anforderungen, Kostenoptimierung — musst du selbst Runner betreiben.
GitLab Runner ist ein Go-Binary. Installation auf einem Linux-Server dauert fuenf Minuten. Die Konfiguration erfolgt über config.toml, der Kubernetes-Executor macht Auto-Scaling trivial. Der Runner unterstützt mehrere parallele Jobs, Cache-Verzeichnisse und Custom-Executor-Plugins. Für größere Setups gibt es den GitLab Runner Operator für Kubernetes.
GitHub Actions Self-hosted Runner ist ein .NET-Prozess. Die Ersteinrichtung ist einfach (Anleitung direkt in der GitHub UI), aber Skalierung erfordert zusätzliche Tooling. Actions Runner Controller (ARC) ist die offizielle Kubernetes-Lösung für Auto-Scaling. Runner Groups ermöglichen die Organisation nach Teams oder Umgebungen.
Kostenvergleich: GitLab SaaS bietet 400 CI-Minuten im Free-Tier, GitHub Actions 2.000 Minuten. Bei Self-hosted Runnern zahlst du in beiden Faellen nur die Infrastruktur. Der Unterschied liegt im Verwaltungsaufwand: GitLab Runner ist ausgereifter und bietet mehr Executor-Optionen. ARC für GitHub Actions hat sich seit der offiziellen Übernahme durch GitHub aber deutlich verbessert.
Migration zwischen den Systemen
Migration ist möglich, aber nie ein reines Syntax-Mapping. Die Konzepte unterscheiden sich genug, dass du Pipeline-Logik neu denken musst.
Von GitLab zu GitHub Actions:
stageslösen sich auf — du modellierst Abhängigkeiten explizit mitneedsinclude/extendswerden zu Reusable Workflows und Composite Actionsservices(z.B. Postgres für Tests) werden zu Service Containersenvironmentmappt direkt auf GitHub Environments- CI/CD-Variablen müssen in GitHub Secrets und Variables uebertragen werden
Von GitHub Actions zu GitLab CI:
- Actions aus dem Marketplace haben kein Äquivalent — du musst die Logik in Scripts oder eigene CI-Templates portieren
matrixwird zuparallel:matrixin GitLab- Workflow-Trigger werden zu
rulesmit Pipeline-Source-Bedingungen - Composite Actions werden zu Hidden Jobs mit
extends
Entscheidungshilfe: Wann welches System
Die Entscheidung ist selten rein technisch. Aber hier sind die Szenarien, in denen ein System klar vorne liegt:
GitLab CI waehlen, wenn:
- Du GitLab bereits als Plattform nutzt (SCM, Issues, Registry — alles integriert)
- Komplexe Mono-Repo-Setups mit Parent-Child-Pipelines benötigt werden
- Self-hosted Runner mit Kubernetes-Executor im Einsatz sind
- Die Organisation ein zentrales Template-Repository mit
include/extendspflegt - Compliance-Anforderungen eine Single-Platform-Lösung erfordern
GitHub Actions waehlen, wenn:
- Die Codebasis auf GitHub liegt (offensichtlich, aber relevant)
- Das Team den Marketplace aktiv nutzt — vorgefertigte Actions sparen Tage
- Event-getriebene Workflows über CI/CD hinausgehen (Issue-Automatisierung, Releases, Dependabot)
- Open-Source-Projekte mit Community-Beiträgen verwaltet werden
- Die Organisation GitHub Enterprise mit GHES oder GHEC einsetzt
Egal welches System: Investiere in Pipeline-Templates, Caching-Strategien und Runner-Infrastruktur. Die Pipeline, die niemand pflegt, ist in beiden Systemen gleich schlecht.
Fazit
GitLab CI und GitHub Actions sind beide ausgereift und leistungsfähig. Der Unterschied liegt nicht in Features, sondern in der Architektur: deklarative Stages vs. event-getriebene Workflows. Beide Modelle haben Stärken, und das “bessere” System ist das, das zu eurer bestehenden Infrastruktur und eurem mentalen Modell passt.
Die wichtigste Erkenntnis nach Jahren mit beiden Systemen: Die Qualität einer CI/CD-Pipeline hängt weniger vom Tool ab als von der Disziplin des Teams. Schnelle Feedback-Loops, deterministische Builds, sauberes Caching, sinnvolle Parallelisierung — das sind die Hebel, die den Unterschied machen. Nicht das Logo im Tab.