TL;DR
This article provides a complete GitHub Actions workflow for Laravel: parallel testing with MySQL and Redis, PHP CS Fixer for code style, PHPStan for static analysis, security scanning, and zero-downtime deployment. Copy, adapt, deploy.
Dit artikel biedt een complete GitHub Actions workflow voor Laravel: parallel testen met MySQL en Redis, PHP CS Fixer voor code stijl, PHPStan voor statische analyse, beveiligingsscans en zero-downtime deployment. Kopieer, pas aan, deploy.
Este artículo proporciona un flujo de trabajo completo de GitHub Actions para Laravel: pruebas paralelas con MySQL y Redis, PHP CS Fixer para estilo de código, PHPStan para análisis estático, escaneo de seguridad y despliegue sin tiempo de inactividad. Copia, adapta, despliega.
Why GitHub Actions?
I've used Jenkins, GitLab CI, CircleCI, and Travis CI over the years. GitHub Actions isn't necessarily the "best" - but for projects already hosted on GitHub, it's the most practical choice. Tight integration, no additional service to manage, and generous free tier for private repos.
What follows is a workflow I've refined over dozens of Laravel projects. It's not theoretical - this runs in production every day, catching bugs before they reach users.
The complete workflow
Let's start with the full workflow file, then break down each section:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
tests:
name: Tests (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3']
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: testing
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
redis:
image: redis:7
ports:
- 6379:6379
options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, bcmath, redis
coverage: xdebug
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --prefer-dist --no-interaction --no-progress
- name: Copy environment file
run: cp .env.ci .env
- name: Generate application key
run: php artisan key:generate
- name: Run migrations
run: php artisan migrate --force
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: testing
DB_USERNAME: root
DB_PASSWORD: password
- name: Run tests
run: php artisan test --parallel --coverage-clover coverage.xml
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: testing
DB_USERNAME: root
DB_PASSWORD: password
REDIS_HOST: 127.0.0.1
REDIS_PORT: 6379
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: coverage.xml
fail_ci_if_error: false
code-style:
name: Code Style
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: php-cs-fixer
- name: Run PHP CS Fixer
run: php-cs-fixer fix --dry-run --diff --config=.php-cs-fixer.php
static-analysis:
name: Static Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --prefer-dist --no-interaction --no-progress
- name: Run PHPStan
run: vendor/bin/phpstan analyse --memory-limit=2G
security:
name: Security Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Security check for PHP dependencies
uses: symfonycorp/security-checker-action@v5
deploy:
name: Deploy
runs-on: ubuntu-latest
needs: [tests, code-style, static-analysis, security]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- name: Deploy to production
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
script: |
cd /var/www/myapp
php artisan down --retry=60
git pull origin main
composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan queue:restart
php artisan up
Breaking it down
Matrix testing
The test job runs on multiple PHP versions simultaneously:
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3']
Setting fail-fast: false ensures all matrix jobs complete even if one fails. This is useful for seeing the full picture - maybe your code works on 8.3 but breaks on 8.2.
Service containers
GitHub Actions makes it easy to spin up MySQL and Redis as service containers:
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: testing
options: --health-cmd="mysqladmin ping" --health-interval=10s
The health check ensures MySQL is ready before tests run. Without it, you'll see intermittent "connection refused" failures.
Dependency caching
Composer installation is one of the slowest steps. Caching cuts this from 2 minutes to seconds:
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
The cache key includes the composer.lock hash, so the cache invalidates automatically when dependencies change.
Parallel testing
Laravel's parallel testing feature can dramatically speed up your test suite:
- name: Run tests
run: php artisan test --parallel --coverage-clover coverage.xml
On a suite with 500+ tests, parallel testing typically reduces runtime by 60-70%.
The environment file
Create a dedicated CI environment file at .env.ci:
# .env.ci
APP_NAME=Laravel
APP_ENV=testing
APP_DEBUG=true
LOG_CHANNEL=stderr
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=testing
DB_USERNAME=root
DB_PASSWORD=password
CACHE_DRIVER=redis
QUEUE_CONNECTION=sync
SESSION_DRIVER=array
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
Key settings:
LOG_CHANNEL=stderr- Logs appear in GitHub Actions outputQUEUE_CONNECTION=sync- Jobs run immediately in testsSESSION_DRIVER=array- No session storage needed
PHP CS Fixer Configuration
Create .php-cs-fixer.php in your project root:
<?php
$finder = PhpCsFixer\Finder::create()
->in([
__DIR__ . '/app',
__DIR__ . '/config',
__DIR__ . '/database',
__DIR__ . '/routes',
__DIR__ . '/tests',
])
->name('*.php')
->notName('*.blade.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
return (new PhpCsFixer\Config())
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'not_operator_with_successor_space' => true,
'trailing_comma_in_multiline' => true,
'phpdoc_scalar' => true,
'unary_operator_spaces' => true,
'binary_operator_spaces' => true,
'blank_line_before_statement' => [
'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
],
'phpdoc_single_line_var_spacing' => true,
'phpdoc_var_without_name' => true,
'class_attributes_separation' => [
'elements' => [
'method' => 'one',
],
],
'method_argument_space' => [
'on_multiline' => 'ensure_fully_multiline',
'keep_multiple_spaces_after_comma' => true,
],
'single_trait_insert_per_statement' => true,
])
->setFinder($finder);
PHPStan Configuration
Create phpstan.neon:
includes:
- ./vendor/larastan/larastan/extension.neon
parameters:
paths:
- app
- config
- database
- routes
level: 6
ignoreErrors:
# Add patterns for known false positives
excludePaths:
- app/Console/Kernel.php
checkMissingIterableValueType: false
Start with level 5 or 6. Going straight to level 9 on an existing project will generate thousands of errors.
Larastan
Install Larastan (composer require --dev larastan/larastan) for Laravel-specific PHPStan rules. It understands Eloquent, facades, and other Laravel patterns that would otherwise confuse PHPStan.
Zero-downtime deployment
The deployment script in the workflow uses php artisan down for simplicity. For true zero-downtime deployments, consider this alternative approach:
#!/bin/bash
# deploy.sh - Zero-downtime deployment script
set -e
DEPLOY_PATH="/var/www/myapp"
RELEASES_PATH="$DEPLOY_PATH/releases"
SHARED_PATH="$DEPLOY_PATH/shared"
CURRENT_PATH="$DEPLOY_PATH/current"
RELEASE=$(date +%Y%m%d%H%M%S)
echo "Creating release directory..."
mkdir -p "$RELEASES_PATH/$RELEASE"
echo "Cloning repository..."
git clone --depth 1 git@github.com:user/repo.git "$RELEASES_PATH/$RELEASE"
echo "Linking shared files..."
ln -nfs "$SHARED_PATH/.env" "$RELEASES_PATH/$RELEASE/.env"
ln -nfs "$SHARED_PATH/storage" "$RELEASES_PATH/$RELEASE/storage"
echo "Installing dependencies..."
cd "$RELEASES_PATH/$RELEASE"
composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
echo "Running migrations..."
php artisan migrate --force
echo "Caching configuration..."
php artisan config:cache
php artisan route:cache
php artisan view:cache
echo "Switching to new release..."
ln -nfs "$RELEASES_PATH/$RELEASE" "$CURRENT_PATH"
echo "Restarting queue workers..."
php artisan queue:restart
echo "Cleaning old releases..."
ls -1dt "$RELEASES_PATH"/* | tail -n +6 | xargs rm -rf
echo "Deployment complete!"
Secrets configuration
Configure these secrets in your GitHub repository settings:
PRODUCTION_HOST- Your server's IP or hostnamePRODUCTION_USER- SSH usernamePRODUCTION_SSH_KEY- Private SSH key (without passphrase)
Generate a dedicated deployment key:
ssh-keygen -t ed25519 -C "github-actions-deploy" -f deploy_key -N ""
Add the public key to ~/.ssh/authorized_keys on your server, and the private key as a GitHub secret.
Workflow optimisation tips
1. Run jobs in parallel
The code-style, static-analysis, and security jobs don't depend on each other. They run in parallel automatically, reducing total CI time.
2. Skip unnecessary runs
Add path filters to skip CI for documentation-only changes:
on:
push:
branches: [main]
paths-ignore:
- '**.md'
- 'docs/**'
3. Use concurrency controls
Prevent multiple deployments from running simultaneously:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
What this catches
With this workflow in place, every pull request is automatically checked for:
- Test failures across PHP versions
- Code style violations
- Type errors and potential bugs (PHPStan)
- Known security vulnerabilities in dependencies
Problems are caught before code review even begins. Reviewers can focus on architecture and logic instead of spotting typos and style issues.
Waarom GitHub Actions?
Ik heb door de jaren heen Jenkins, GitLab CI, CircleCI en Travis CI gebruikt. GitHub Actions is niet per se de "beste" - maar voor projecten die al op GitHub gehost worden, is het de meest praktische keuze. Strakke integratie, geen extra service om te beheren, en een royale gratis tier voor private repos.
Wat volgt is een workflow die ik heb verfijnd over tientallen Laravel projecten. Het is niet theoretisch - dit draait elke dag in productie en vangt bugs voordat ze gebruikers bereiken.
De complete workflow
Laten we beginnen met het volledige workflow bestand, en vervolgens elke sectie uitleggen:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
tests:
name: Tests (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3']
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: testing
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
redis:
image: redis:7
ports:
- 6379:6379
options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, bcmath, redis
coverage: xdebug
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --prefer-dist --no-interaction --no-progress
- name: Copy environment file
run: cp .env.ci .env
- name: Generate application key
run: php artisan key:generate
- name: Run migrations
run: php artisan migrate --force
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: testing
DB_USERNAME: root
DB_PASSWORD: password
- name: Run tests
run: php artisan test --parallel --coverage-clover coverage.xml
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: testing
DB_USERNAME: root
DB_PASSWORD: password
REDIS_HOST: 127.0.0.1
REDIS_PORT: 6379
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: coverage.xml
fail_ci_if_error: false
code-style:
name: Code Style
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: php-cs-fixer
- name: Run PHP CS Fixer
run: php-cs-fixer fix --dry-run --diff --config=.php-cs-fixer.php
static-analysis:
name: Static Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --prefer-dist --no-interaction --no-progress
- name: Run PHPStan
run: vendor/bin/phpstan analyse --memory-limit=2G
security:
name: Security Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Security check for PHP dependencies
uses: symfonycorp/security-checker-action@v5
deploy:
name: Deploy
runs-on: ubuntu-latest
needs: [tests, code-style, static-analysis, security]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- name: Deploy to production
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
script: |
cd /var/www/myapp
php artisan down --retry=60
git pull origin main
composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan queue:restart
php artisan up
Uitleg per onderdeel
Matrix testen
De test job draait op meerdere PHP versies tegelijkertijd:
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3']
Het instellen van fail-fast: false zorgt ervoor dat alle matrix jobs compleet worden, zelfs als een job faalt. Dit is nuttig om het complete plaatje te zien - misschien werkt je code op 8.3 maar breekt het op 8.2.
Service containers
GitHub Actions maakt het eenvoudig om MySQL en Redis op te starten als service containers:
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: testing
options: --health-cmd="mysqladmin ping" --health-interval=10s
De health check zorgt ervoor dat MySQL klaar is voordat tests beginnen. Zonder deze check zie je intermittent "connection refused" fouten.
Dependency caching
Composer installatie is een van de traagste stappen. Caching vermindert dit van 2 minuten naar seconden:
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
De cache sleutel bevat de composer.lock hash, dus de cache wordt automatisch ongeldig gemaakt wanneer dependencies veranderen.
Parallel testen
Laravel's parallel testing feature kan je test suite dramatisch versnellen:
- name: Run tests
run: php artisan test --parallel --coverage-clover coverage.xml
Op een test suite met 500+ tests vermindert parallel testen de runtime typisch met 60-70%.
Het environment bestand
Maak een dedicated CI environment bestand aan op .env.ci:
# .env.ci
APP_NAME=Laravel
APP_ENV=testing
APP_DEBUG=true
LOG_CHANNEL=stderr
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=testing
DB_USERNAME=root
DB_PASSWORD=password
CACHE_DRIVER=redis
QUEUE_CONNECTION=sync
SESSION_DRIVER=array
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
Belangrijke instellingen:
LOG_CHANNEL=stderr- Logs verschijnen in GitHub Actions outputQUEUE_CONNECTION=sync- Jobs worden direct uitgevoerd in testsSESSION_DRIVER=array- Geen session opslag nodig
PHP CS Fixer configuratie
Maak .php-cs-fixer.php aan in je project root:
<?php
$finder = PhpCsFixer\Finder::create()
->in([
__DIR__ . '/app',
__DIR__ . '/config',
__DIR__ . '/database',
__DIR__ . '/routes',
__DIR__ . '/tests',
])
->name('*.php')
->notName('*.blade.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
return (new PhpCsFixer\Config())
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'not_operator_with_successor_space' => true,
'trailing_comma_in_multiline' => true,
'phpdoc_scalar' => true,
'unary_operator_spaces' => true,
'binary_operator_spaces' => true,
'blank_line_before_statement' => [
'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
],
'phpdoc_single_line_var_spacing' => true,
'phpdoc_var_without_name' => true,
'class_attributes_separation' => [
'elements' => [
'method' => 'one',
],
],
'method_argument_space' => [
'on_multiline' => 'ensure_fully_multiline',
'keep_multiple_spaces_after_comma' => true,
],
'single_trait_insert_per_statement' => true,
])
->setFinder($finder);
PHPStan configuratie
Maak phpstan.neon aan:
includes:
- ./vendor/larastan/larastan/extension.neon
parameters:
paths:
- app
- config
- database
- routes
level: 6
ignoreErrors:
# Add patterns for known false positives
excludePaths:
- app/Console/Kernel.php
checkMissingIterableValueType: false
Begin met level 5 of 6. Direct naar level 9 gaan op een bestaand project genereert duizenden fouten.
Larastan
Installeer Larastan (composer require --dev larastan/larastan) voor Laravel-specifieke PHPStan regels. Het begrijpt Eloquent, facades en andere Laravel patronen die anders PHPStan zouden verwarren.
Zero-downtime deployment
Het deployment script in de workflow gebruikt php artisan down voor eenvoud. Voor echte zero-downtime deployments, overweeg deze alternatieve aanpak:
#!/bin/bash
# deploy.sh - Zero-downtime deployment script
set -e
DEPLOY_PATH="/var/www/myapp"
RELEASES_PATH="$DEPLOY_PATH/releases"
SHARED_PATH="$DEPLOY_PATH/shared"
CURRENT_PATH="$DEPLOY_PATH/current"
RELEASE=$(date +%Y%m%d%H%M%S)
echo "Creating release directory..."
mkdir -p "$RELEASES_PATH/$RELEASE"
echo "Cloning repository..."
git clone --depth 1 git@github.com:user/repo.git "$RELEASES_PATH/$RELEASE"
echo "Linking shared files..."
ln -nfs "$SHARED_PATH/.env" "$RELEASES_PATH/$RELEASE/.env"
ln -nfs "$SHARED_PATH/storage" "$RELEASES_PATH/$RELEASE/storage"
echo "Installing dependencies..."
cd "$RELEASES_PATH/$RELEASE"
composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
echo "Running migrations..."
php artisan migrate --force
echo "Caching configuration..."
php artisan config:cache
php artisan route:cache
php artisan view:cache
echo "Switching to new release..."
ln -nfs "$RELEASES_PATH/$RELEASE" "$CURRENT_PATH"
echo "Restarting queue workers..."
php artisan queue:restart
echo "Cleaning old releases..."
ls -1dt "$RELEASES_PATH"/* | tail -n +6 | xargs rm -rf
echo "Deployment complete!"
Secrets configuratie
Configureer deze secrets in je GitHub repository instellingen:
PRODUCTION_HOST- Het IP-adres of hostname van je serverPRODUCTION_USER- SSH gebruikersnaamPRODUCTION_SSH_KEY- Private SSH key (zonder wachtwoordzin)
Genereer een dedicated deployment key:
ssh-keygen -t ed25519 -C "github-actions-deploy" -f deploy_key -N ""
Voeg de public key toe aan ~/.ssh/authorized_keys op je server, en de private key als GitHub secret.
Workflow optimalisatie tips
1. Voer jobs parallel uit
De code-style, static-analysis en security jobs zijn niet afhankelijk van elkaar. Ze draaien automatisch parallel, wat de totale CI tijd vermindert.
2. Sla onnodige runs over
Voeg path filters toe om CI over te slaan voor documentatie-only wijzigingen:
on:
push:
branches: [main]
paths-ignore:
- '**.md'
- 'docs/**'
3. Gebruik concurrency controls
Voorkom dat meerdere deployments gelijktijdig draaien:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Wat dit vangt
Met deze workflow worden bij elke pull request automatisch de volgende zaken gecontroleerd:
- Test failures over PHP versies
- Code stijl overtredingen
- Type fouten en potentiële bugs (PHPStan)
- Bekende beveiligingsproblemen in dependencies
Problemen worden gevangen voordat code review zelfs maar begint. Reviewers kunnen zich richten op architectuur en logica in plaats van het opsporen van typefouten en stijlproblemen.
¿Por qué GitHub Actions?
He usado Jenkins, GitLab CI, CircleCI y Travis CI a lo largo de los años. GitHub Actions no es necesariamente el "mejor", pero para proyectos ya alojados en GitHub, es la opción más práctica. Integración estrecha, sin servicios adicionales que gestionar y un generoso nivel gratuito para repos privados.
Lo que sigue es un flujo de trabajo que he refinado en docenas de proyectos Laravel. No es teórico: esto se ejecuta en producción todos los días, detectando errores antes de que lleguen a los usuarios.
El flujo de trabajo completo
Comencemos con el archivo de flujo de trabajo completo, luego desglosaremos cada sección:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
jobs:
tests:
name: Tests (PHP ${{ matrix.php }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3']
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: testing
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
redis:
image: redis:7
ports:
- 6379:6379
options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, pdo_mysql, bcmath, redis
coverage: xdebug
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --prefer-dist --no-interaction --no-progress
- name: Copy environment file
run: cp .env.ci .env
- name: Generate application key
run: php artisan key:generate
- name: Run migrations
run: php artisan migrate --force
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: testing
DB_USERNAME: root
DB_PASSWORD: password
- name: Run tests
run: php artisan test --parallel --coverage-clover coverage.xml
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: testing
DB_USERNAME: root
DB_PASSWORD: password
REDIS_HOST: 127.0.0.1
REDIS_PORT: 6379
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: coverage.xml
fail_ci_if_error: false
code-style:
name: Code Style
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: php-cs-fixer
- name: Run PHP CS Fixer
run: php-cs-fixer fix --dry-run --diff --config=.php-cs-fixer.php
static-analysis:
name: Static Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
- name: Install dependencies
run: composer install --prefer-dist --no-interaction --no-progress
- name: Run PHPStan
run: vendor/bin/phpstan analyse --memory-limit=2G
security:
name: Security Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Security check for PHP dependencies
uses: symfonycorp/security-checker-action@v5
deploy:
name: Deploy
runs-on: ubuntu-latest
needs: [tests, code-style, static-analysis, security]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- name: Deploy to production
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_SSH_KEY }}
script: |
cd /var/www/myapp
php artisan down --retry=60
git pull origin main
composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan queue:restart
php artisan up
Desglose
Pruebas matriciales
El trabajo de prueba se ejecuta en múltiples versiones de PHP simultáneamente:
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3']
Configurar fail-fast: false asegura que todos los trabajos de matriz se completen incluso si uno falla. Esto es útil para ver el panorama completo: tal vez tu código funcione en 8.3 pero falle en 8.2.
Contenedores de servicios
GitHub Actions facilita iniciar MySQL y Redis como contenedores de servicios:
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: testing
options: --health-cmd="mysqladmin ping" --health-interval=10s
La verificación de salud asegura que MySQL esté listo antes de que se ejecuten las pruebas. Sin ella, verás fallos intermitentes de "conexión rechazada".
Caché de dependencias
La instalación de Composer es uno de los pasos más lentos. El caché reduce esto de 2 minutos a segundos:
- name: Get Composer cache directory
id: composer-cache
run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: ${{ runner.os }}-composer-
La clave del caché incluye el hash de composer.lock, por lo que el caché se invalida automáticamente cuando cambian las dependencias.
Pruebas paralelas
La función de pruebas paralelas de Laravel puede acelerar dramáticamente tu suite de pruebas:
- name: Run tests
run: php artisan test --parallel --coverage-clover coverage.xml
En una suite con más de 500 pruebas, las pruebas paralelas típicamente reducen el tiempo de ejecución en un 60-70%.
El archivo de entorno
Crea un archivo de entorno CI dedicado en .env.ci:
# .env.ci
APP_NAME=Laravel
APP_ENV=testing
APP_DEBUG=true
LOG_CHANNEL=stderr
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=testing
DB_USERNAME=root
DB_PASSWORD=password
CACHE_DRIVER=redis
QUEUE_CONNECTION=sync
SESSION_DRIVER=array
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
Configuraciones clave:
LOG_CHANNEL=stderr- Los logs aparecen en la salida de GitHub ActionsQUEUE_CONNECTION=sync- Los trabajos se ejecutan inmediatamente en las pruebasSESSION_DRIVER=array- No se necesita almacenamiento de sesión
Configuración de PHP CS Fixer
Crea .php-cs-fixer.php en la raíz de tu proyecto:
<?php
$finder = PhpCsFixer\Finder::create()
->in([
__DIR__ . '/app',
__DIR__ . '/config',
__DIR__ . '/database',
__DIR__ . '/routes',
__DIR__ . '/tests',
])
->name('*.php')
->notName('*.blade.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
return (new PhpCsFixer\Config())
->setRules([
'@PSR12' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sort_algorithm' => 'alpha'],
'no_unused_imports' => true,
'not_operator_with_successor_space' => true,
'trailing_comma_in_multiline' => true,
'phpdoc_scalar' => true,
'unary_operator_spaces' => true,
'binary_operator_spaces' => true,
'blank_line_before_statement' => [
'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
],
'phpdoc_single_line_var_spacing' => true,
'phpdoc_var_without_name' => true,
'class_attributes_separation' => [
'elements' => [
'method' => 'one',
],
],
'method_argument_space' => [
'on_multiline' => 'ensure_fully_multiline',
'keep_multiple_spaces_after_comma' => true,
],
'single_trait_insert_per_statement' => true,
])
->setFinder($finder);
Configuración de PHPStan
Crea phpstan.neon:
includes:
- ./vendor/larastan/larastan/extension.neon
parameters:
paths:
- app
- config
- database
- routes
level: 6
ignoreErrors:
# Add patterns for known false positives
excludePaths:
- app/Console/Kernel.php
checkMissingIterableValueType: false
Comienza con el nivel 5 o 6. Ir directamente al nivel 9 en un proyecto existente generará miles de errores.
Larastan
Instala Larastan (composer require --dev larastan/larastan) para reglas PHPStan específicas de Laravel. Entiende Eloquent, facades y otros patrones de Laravel que de otro modo confundirían a PHPStan.
Despliegue sin tiempo de inactividad
El script de despliegue en el flujo de trabajo usa php artisan down por simplicidad. Para despliegues realmente sin tiempo de inactividad, considera este enfoque alternativo:
#!/bin/bash
# deploy.sh - Script de despliegue sin tiempo de inactividad
set -e
DEPLOY_PATH="/var/www/myapp"
RELEASES_PATH="$DEPLOY_PATH/releases"
SHARED_PATH="$DEPLOY_PATH/shared"
CURRENT_PATH="$DEPLOY_PATH/current"
RELEASE=$(date +%Y%m%d%H%M%S)
echo "Creating release directory..."
mkdir -p "$RELEASES_PATH/$RELEASE"
echo "Cloning repository..."
git clone --depth 1 git@github.com:user/repo.git "$RELEASES_PATH/$RELEASE"
echo "Linking shared files..."
ln -nfs "$SHARED_PATH/.env" "$RELEASES_PATH/$RELEASE/.env"
ln -nfs "$SHARED_PATH/storage" "$RELEASES_PATH/$RELEASE/storage"
echo "Installing dependencies..."
cd "$RELEASES_PATH/$RELEASE"
composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
echo "Running migrations..."
php artisan migrate --force
echo "Caching configuration..."
php artisan config:cache
php artisan route:cache
php artisan view:cache
echo "Switching to new release..."
ln -nfs "$RELEASES_PATH/$RELEASE" "$CURRENT_PATH"
echo "Restarting queue workers..."
php artisan queue:restart
echo "Cleaning old releases..."
ls -1dt "$RELEASES_PATH"/* | tail -n +6 | xargs rm -rf
echo "Deployment complete!"
Configuración de secretos
Configura estos secretos en la configuración de tu repositorio de GitHub:
PRODUCTION_HOST- La IP o nombre de host de tu servidorPRODUCTION_USER- Nombre de usuario SSHPRODUCTION_SSH_KEY- Clave SSH privada (sin frase de contraseña)
Genera una clave de despliegue dedicada:
ssh-keygen -t ed25519 -C "github-actions-deploy" -f deploy_key -N ""
Añade la clave pública a ~/.ssh/authorized_keys en tu servidor, y la clave privada como un secreto de GitHub.
Consejos de optimización del flujo de trabajo
1. Ejecuta trabajos en paralelo
Los trabajos code-style, static-analysis y security no dependen entre sí. Se ejecutan en paralelo automáticamente, reduciendo el tiempo total de CI.
2. Omite ejecuciones innecesarias
Añade filtros de ruta para omitir CI en cambios solo de documentación:
on:
push:
branches: [main]
paths-ignore:
- '**.md'
- 'docs/**'
3. Usa controles de concurrencia
Evita que múltiples despliegues se ejecuten simultáneamente:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
Lo que esto detecta
Con este flujo de trabajo implementado, cada pull request se verifica automáticamente para:
- Fallos de pruebas en diferentes versiones de PHP
- Violaciones de estilo de código
- Errores de tipo y bugs potenciales (PHPStan)
- Vulnerabilidades de seguridad conocidas en dependencias
Los problemas se detectan antes de que comience la revisión de código. Los revisores pueden centrarse en la arquitectura y la lógica en lugar de detectar errores tipográficos y problemas de estilo.
Need help setting up CI/CD for your Laravel project? I've implemented these workflows for teams of all sizes. Let's talk.