| name | laravel-cicd |
| description | Designs and implements production-grade CI/CD pipelines for Laravel enterprise applications using GitHub Actions. Use when creating or refactoring GitHub Actions workflows for Laravel or PHP projects, configuring Composer and NPM caching, setting up PHPUnit or Pest test execution, implementing SAST security scanning, managing secrets with GitHub Secrets, configuring matrix builds for multiple PHP versions, structuring build/test/deploy stages, deploying to Laravel Forge, Envoyer, or custom servers, setting up Laravel Octane or queue workers in pipelines, or asked about PHP pipeline security, zero-downtime deployment, environment management, or automated quality gates for Laravel enterprise applications.
|
| license | MIT |
| metadata | {"author":"iamBrzDev","version":"1.0"} |
When to activate
- Creating a GitHub Actions workflow for a Laravel project from scratch
- Migrating a manual deployment process to automated CI/CD
- "How do I speed up my Laravel pipeline?" in a GitHub Actions context
- Adding Composer or NPM caching to an existing workflow
- Configuring PHPUnit or Pest to run in CI with a real database
- Setting up matrix builds for multiple PHP versions
- Adding security scanning (SAST) to PR validation
- Managing environment variables and secrets in GitHub Actions
- Configuring zero-downtime deployment to production
- Any mention of Laravel Forge, Envoyer, Octane, Horizon, or queues in a CI/CD context
Rules — Non-negotiable
-
Secrets never go in workflow .yml files. All sensitive values —
DB passwords, API keys, APP_KEY — must come from GitHub Secrets injected
as environment variables. Never hardcoded, never in plain env: at repo level.
-
Deploy only from main after all checks pass. No manual deploys that
bypass the pipeline. Deploy job must need: the test and security jobs.
-
Cache vendor/ and node_modules/ on every pipeline. A pipeline without
caching wastes 60–90 seconds per run on package downloads.
-
Tests run against a real database. SQLite in-memory is acceptable only
for unit tests. Feature tests must use MySQL or PostgreSQL via service containers.
-
.env files are never committed. Use .env.ci or inline env: blocks
with secrets. Never copy .env.example and commit real values.
Pipeline Structure
Push / PR → Quality Gates (parallel) → Build Assets → Deploy (gated)
├── PHPUnit / Pest
├── Static Analysis (Larastan)
└── Security Scan (SAST)
Canonical workflow skeleton
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
tests:
name: Tests
runs-on: ubuntu-latest
static-analysis:
name: Static Analysis
runs-on: ubuntu-latest
security-scan:
name: Security Scan
runs-on: ubuntu-latest
build-assets:
name: Build Assets
runs-on: ubuntu-latest
needs: [tests, static-analysis, security-scan]
deploy:
name: Deploy to Production
runs-on: ubuntu-latest
needs: [build-assets]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
Tests Job — Real Database via Service Container
tests:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: laravel_test
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping"
--health-interval=10s
--health-timeout=5s
--health-retries=5
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, pdo, pdo_mysql, redis
coverage: xdebug
- name: Cache Composer dependencies
uses: actions/cache@v4
with:
path: vendor
key: composer-${{ runner.os }}-${{ hashFiles('composer.lock') }}
restore-keys: composer-${{ runner.os }}-
- name: Install Composer dependencies
run: composer install --no-interaction --prefer-dist --optimize-autoloader
- name: Prepare Laravel environment
run: |
cp .env.ci .env
php artisan key:generate
php artisan migrate --force
php artisan db:seed --class=TestingSeeder
- name: Run tests with coverage
run: php artisan test --coverage --min=80
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: laravel_test
DB_USERNAME: root
DB_PASSWORD: secret
.env.ci — committed, no secrets
APP_NAME=Laravel
APP_ENV=testing
APP_KEY=
APP_DEBUG=false
APP_URL=http://localhost
LOG_CHANNEL=stderr
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_test
DB_USERNAME=root
DB_PASSWORD=secret
CACHE_DRIVER=array
QUEUE_CONNECTION=sync
SESSION_DRIVER=array
MAIL_MAILER=array
Caching — Composer + NPM/Vite
- name: Cache Composer
uses: actions/cache@v4
with:
path: vendor
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
composer-${{ runner.os }}-
- name: Cache NPM
uses: actions/cache@v4
with:
path: node_modules
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
npm-${{ runner.os }}-
- run: composer install --no-interaction --prefer-dist --optimize-autoloader
- run: npm ci
Matrix Builds — Multiple PHP Versions
tests:
strategy:
fail-fast: false
matrix:
php: ['8.2', '8.3']
laravel: ['11.*']
name: PHP ${{ matrix.php }} — Laravel ${{ matrix.laravel }}
runs-on: ubuntu-latest
steps:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
Static Analysis — Larastan
static-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- uses: actions/cache@v4
with:
path: vendor
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
- run: composer install --no-interaction --prefer-dist
- name: Run Larastan
run: ./vendor/bin/phpstan analyse --level=8 --no-progress
Security Scan — SAST
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
- uses: actions/cache@v4
with:
path: vendor
key: composer-${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
- run: composer install --no-interaction --prefer-dist
- name: Check for vulnerable dependencies
run: composer audit
- name: Run Psalm security analysis
run: ./vendor/bin/psalm --taint-analysis --no-progress
continue-on-error: false
Secrets — Injection Pattern
deploy:
steps:
- name: Deploy to server
env:
APP_KEY: ${{ secrets.APP_KEY }}
DB_PASSWORD: ${{ secrets.DB_PASSWORD }}
FORGE_API_TOKEN: ${{ secrets.FORGE_API_TOKEN }}
run: |
# Secrets are available as env vars — never printed in logs
deploy:
env:
DB_PASSWORD: 'MyProductionPassword123'
Required GitHub Secrets to configure:
| Secret | Purpose |
|---|
APP_KEY | Laravel application key for production |
DB_PASSWORD | Production database password |
DEPLOY_SSH_KEY | Private key for SSH deployment |
FORGE_API_TOKEN | If deploying via Laravel Forge |
Deploy Job — Zero-Downtime
deploy:
runs-on: ubuntu-latest
needs: [build-assets]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production
steps:
- uses: actions/checkout@v4
- name: Trigger Forge deployment
run: |
curl -X POST \
-H "Authorization: Bearer ${{ secrets.FORGE_API_TOKEN }}" \
https://forge.laravel.com/api/v1/servers/${{ secrets.FORGE_SERVER_ID }}/sites/${{ secrets.FORGE_SITE_ID }}/deployment/deploy
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_SSH_KEY }}
script: |
cd /var/www/app
php artisan down --retry=60
git pull origin main
composer install --no-dev --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
Build Assets Job
build-assets:
runs-on: ubuntu-latest
needs: [tests, static-analysis, security-scan]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- uses: actions/cache@v4
with:
path: node_modules
key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: built-assets
path: public/build/
retention-days: 1
Common mistakes
-
❌ DB_CONNECTION=sqlite for all tests — misses MySQL-specific constraints and queries
-
✅ Use MySQL service container for feature tests; SQLite only for pure unit tests
-
❌ composer install without --optimize-autoloader in production
-
✅ Always use --no-dev --optimize-autoloader for production deploys
-
❌ Running php artisan migrate:fresh in production deploy
-
✅ Use php artisan migrate --force — never fresh in production
-
❌ Deploying on every push to any branch
-
✅ Deploy only on push to main with if: github.ref == 'refs/heads/main'
-
❌ Forgetting php artisan config:cache && route:cache after deploy
-
✅ Always cache config, routes, and views post-deploy for performance
Definition of Done
A Laravel CI/CD pipeline built with this skill is complete only when:
Reference files
Load on demand:
references/laravel-octane-pipeline.md — pipeline adaptations for Octane (FrankenPHP/Swoole)
references/horizon-workers-deploy.md — restarting Horizon and queue workers in CI/CD
references/deployer-zero-downtime.md — full Deployer PHP configuration for zero-downtime