Zum Inhalt springen
Abstrakte Formen aus Mattglas: ein Würfel fährt durch vier Tore und kommt am Ende geglättet und orange heraus

GitHub Actions CI für Laravel + Statamic: 5-Minuten-Setup

Eine ci.yml, die Pint, PHPUnit oder Pest, Stache-Warmup und ein Build-Artefakt in unter drei Minuten durchzieht. Bereit für Laravel-Cloud-Deploys.

7 min Lesezeit Performance & DevOps

Warum CI von Tag 1

Die Pipeline ist kein Optional, sondern Teil des Stacks

In jedem Projekt, das wir aufsetzen, ist die CI vor dem ersten Feature da. Nicht aus Dogma, sondern weil sich Pint, PHPUnit und ein erfolgreich gebauter Statamic-Stache schon beim ersten Pull-Request lohnen. Wer die Pipeline später nachzieht, schiebt eine wachsende Schuldenliste vor sich her.

Eine Strecke mit drei Arbeitsschritten, der orange Fortschritt endet deutlich vor der gestrichelten Zeitgrenze.

Schnell genug, um nicht zu nerven

Eine CI, die fünf Minuten braucht, wird bei jedem Commit gepusht. Eine CI, die zwanzig Minuten braucht, wird umgangen. Wir zielen auf unter drei Minuten Gesamtlaufzeit - mit Composer-Cache, NPM-Cache und parallelem Lint-/Test-Job ist das in den meisten Projekten realistisch.

Stapel von Inhaltseinträgen läuft durch einen orangen Prüfring, ein Eintrag ist dunkel als fehlerhaft erkennbar.

Statamic-Stache als zusätzlicher Smoke-Test

Statamic 6 hält den Index aller Entries im Stache. Wenn ein Blueprint kaputt ist oder ein Entry verwaisten YAML-Code enthält, fällt das spätestens beim Warmup auf. Wir lassen den Stache deshalb in CI bauen - das ist der schnellste Content-Smoke-Test, den es für Statamic gibt.

Workflow aufbauen

Eine Datei, drei klare Phasen

Die Pipeline besteht aus drei Phasen: Lint mit Pint, Test mit PHPUnit oder Pest plus Stache-Warmup, und Build des Frontends mit Vite. Jede Phase ist ein eigener Job, damit Logs sauber getrennt sind und parallele Ausführung möglich bleibt.

Drei Spuren für Branches, auf der mittleren sind ältere Läufe gestrichelt abgebrochen, nur der neueste läuft orange.

Trigger und Concurrency

Wir laufen auf push und pull_request, beschränkt auf main, develop und Feature-Branches mit feature/-Prefix. Concurrency-Gruppe pro Branch verhindert, dass alte Runs neue blockieren - wer fünfmal hintereinander pusht, lässt nur den letzten Run weiterlaufen.

Raster aus gestrichelten Feldern für viele Versionen, nur ein oranger Block ist fest gesetzt.

PHP-Version pinnen, nicht matrixen

Bibliotheken testen über mehrere PHP-Versionen - Applikationen nicht. Wir pinnen auf PHP 8.5, exakt wie Production. Wenn Laravel 14 erscheint und 8.6 bringt, hebt ein einziger Edit die Version. Das spart pro Run wertvolle Minuten und macht Debugging deterministisch.

.github/workflows/ci.yml yaml
name: CI

on:
  push:
    branches: [main, develop, 'feature/**']
  pull_request:
    branches: [main, develop]

concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  lint:
    name: Pint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.5'
          coverage: none
          tools: composer:v2
      - name: Cache Composer
        uses: actions/cache@v4
        with:
          path: ~/.composer/cache
          key: composer-${{ hashFiles('composer.lock') }}
      - run: composer install --prefer-dist --no-interaction --no-progress
      - run: vendor/bin/pint --test --format=github

  test:
    name: PHPUnit + Stache
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16-alpine
        env:
          POSTGRES_DB: testing
          POSTGRES_USER: testing
          POSTGRES_PASSWORD: testing
        ports: ['5432:5432']
        options: >-
          --health-cmd pg_isready
          --health-interval 5s
          --health-retries 10
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.5'
          extensions: intl, pdo_pgsql, redis
          coverage: none
      - name: Cache Composer
        uses: actions/cache@v4
        with:
          path: ~/.composer/cache
          key: composer-${{ hashFiles('composer.lock') }}
      - run: composer install --prefer-dist --no-interaction --no-progress
      - run: cp .env.ci .env
      - run: php artisan key:generate --ansi
      - run: php artisan migrate --force --no-interaction
      - run: php artisan stache:warm
      - run: php artisan test --compact --parallel

  build:
    name: Vite Build
    runs-on: ubuntu-latest
    needs: [lint, test]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: vite-build
          path: public/build
          retention-days: 7

CI im Bestandsprojekt nachrüsten?

Wir analysieren Ihr Repo, schreiben Pint-Regeln passend zum Code-Stand und liefern die Pipeline lauffähig - in der Regel an einem Tag.

CI-Audit anfragen

Jobs im Detail

Lint, Test und Build genauer angesehen

Drei Jobs, drei Aufgaben. Jeder hat einen klaren Fail-Fast-Pfad, damit Sie nicht acht Minuten auf einen Build warten, bevor Sie sehen, dass Pint sich beschwert hat.

Codeblatt mit Zeilen, eine Zeile ist orange markiert, daneben steht ein oranger Hinweis.

Pint mit GitHub-Format

vendor/bin/pint --test --format=github gibt Annotations direkt im Pull-Request aus - Reviewer sehen die fehlerhafte Zeile inline, ohne Logs zu öffnen. Lokal läuft Pint im Pre-Commit-Hook, in CI nur als Sicherheitsnetz. Wer den Hook umgeht, wird auf GitHub geblockt.

Zwei kurze Stapel von Tests nebeneinander auf einer orangen Grundplatte, daneben gestrichelt der doppelt so hohe Stapel bei serieller Ausführung.

Tests parallel, Stache als Smoke

php artisan test --parallel halbiert in den meisten Projekten die Test-Laufzeit. Davor läuft php artisan stache:warm gegen die committeten Statamic-Inhalte - wenn ein Blueprint-Handle fehlt oder ein Entry kaputtes YAML hat, scheitert dieser Schritt schneller als jeder Feature-Test es könnte.

.env.ci bash
APP_ENV=testing
APP_DEBUG=true
APP_URL=http://localhost

DB_CONNECTION=pgsql
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DATABASE=testing
DB_USERNAME=testing
DB_PASSWORD=testing

CACHE_STORE=array
QUEUE_CONNECTION=sync
SESSION_DRIVER=array
MAIL_MAILER=log

STATAMIC_STACHE_WATCHER=false
STATAMIC_LICENSE_KEY=${{ secrets.STATAMIC_LICENSE_KEY }}

Caching und Deploy

Schnell halten, sauber deployen

Eine grüne CI ist die Voraussetzung für einen automatisierten Deploy. Auf Laravel Cloud reicht ein Webhook nach erfolgreichem Build, auf Hetzner triggern wir per SSH-Action - beide Wege brauchen denselben Vorlauf: zuverlässige, schnelle Pipeline.

Schrank mit Fächern, ein oranges Fach ist herausgezogen und gestrichelt mit dem Blatt der Lock-Datei verbunden.

Composer- und NPM-Cache richtig schlüsseln

Cache-Keys hängen am Hash der jeweiligen Lock-Datei - composer.lock für Composer, package-lock.json für npm. So wird der Cache invalidiert, sobald Dependencies sich ändern, und bleibt sonst stabil. Sub-Minute-Builds bei reinen Code-Changes sind damit normal, nicht außergewöhnlich.

Auf der Hauptspur folgt dem Build ein oranger Deploy-Schritt bis zum Server, die Nebenspur endet ohne Deploy.

Deploy als nachgelagerter Job

Den Deploy fügen wir als separaten Job mit needs: [build] hinzu, der nur auf main läuft. Auf Laravel Cloud reicht ein curl auf den Deploy-Webhook - die Cloud zieht sich den Build selbst. Secrets gehören in die Repository-Secrets, nicht in die Datei.

.github/workflows/ci.yml (deploy-Job) yaml
deploy:
  name: Deploy to Laravel Cloud
  runs-on: ubuntu-latest
  needs: [build]
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
  steps:
    - name: Trigger Laravel Cloud deploy hook
      run: |
        curl -fsSL -X POST \
          -H "Authorization: Bearer ${{ secrets.LARAVEL_CLOUD_TOKEN }}" \
          "${{ secrets.LARAVEL_CLOUD_DEPLOY_HOOK }}"

    - name: Smoke-Test nach Deploy
      run: |
        for i in 1 2 3 4 5; do
          if curl -fsSL "${{ secrets.APP_URL }}/health" >/dev/null; then
            echo "Deploy ok"
            exit 0
          fi
          sleep 10
        done
        echo "Health check failed"
        exit 1

Hat das geholfen? Wir machen Audit, Workshop oder Coaching auch live

Pakete direkt im Shop. Unser Standard-Tagessatz liegt bei 1.500 € netto.

CI-Setup anfragen