Continuous Integration transforms how we build Laravel applications. Automated testing, code quality checks, and deployment pipelines catch bugs before they reach production. At ZIRA Software, CI/CD has reduced our bug rate by 70% and deployment time from hours to minutes.
Why Continuous Integration?
Benefits:
- Catch bugs immediately
- Automated testing on every commit
- Consistent build environment
- Fast feedback loop
- Reduced integration problems
- Automated deployment
- Code quality enforcement
What CI does:
- Runs tests automatically
- Checks code style
- Analyzes code quality
- Builds assets
- Deploys to staging/production
- Notifies team of failures
Setting Up Travis CI
Configure .travis.yml
.travis.yml:
language: php
php:
- 7.0
- 5.6
services:
- mysql
- redis-server
before_script:
- cp .env.travis .env
- composer self-update
- composer install --no-interaction
- php artisan key:generate
- php artisan migrate --force
- npm install
- npm run production
script:
- vendor/bin/phpunit
- vendor/bin/phpcs --standard=PSR2 app/
after_success:
- bash <(curl -s https://codecov.io/bash)
notifications:
email: false
slack:
rooms:
- your-team:token#general
.env.travis:
APP_ENV=testing
APP_DEBUG=true
APP_KEY=base64:your-test-key
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=travis_test
DB_USERNAME=root
DB_PASSWORD=
CACHE_DRIVER=array
SESSION_DRIVER=array
QUEUE_DRIVER=sync
Database Setup
Before script:
before_script:
- mysql -e 'CREATE DATABASE IF NOT EXISTS travis_test;'
- php artisan migrate --seed
Jenkins Setup
Install Jenkins
wget -q -O - https://pkg.jenkins.io/debian/jenkins.io.key | sudo apt-key add -
echo deb http://pkg.jenkins.io/debian-stable binary/ | sudo tee /etc/apt/sources.list.d/jenkins.list
sudo apt-get update
sudo apt-get install jenkins
Configure Jenkins Job
Jenkinsfile:
pipeline {
agent any
stages {
stage('Checkout') {
steps {
git branch: 'main',
url: 'https://github.com/youruser/yourapp.git'
}
}
stage('Install Dependencies') {
steps {
sh 'composer install --no-interaction'
sh 'npm install'
}
}
stage('Prepare Environment') {
steps {
sh 'cp .env.jenkins .env'
sh 'php artisan key:generate'
sh 'php artisan migrate --force'
}
}
stage('Run Tests') {
parallel {
stage('Unit Tests') {
steps {
sh 'vendor/bin/phpunit --testsuite Unit'
}
}
stage('Feature Tests') {
steps {
sh 'vendor/bin/phpunit --testsuite Feature'
}
}
}
}
stage('Code Quality') {
parallel {
stage('Code Style') {
steps {
sh 'vendor/bin/phpcs --standard=PSR2 app/'
}
}
stage('Static Analysis') {
steps {
sh 'vendor/bin/phpstan analyse app/ --level=5'
}
}
}
}
stage('Build Assets') {
steps {
sh 'npm run production'
}
}
stage('Deploy to Staging') {
when {
branch 'develop'
}
steps {
sh './deploy-staging.sh'
}
}
stage('Deploy to Production') {
when {
branch 'main'
}
steps {
input message: 'Deploy to production?'
sh './deploy-production.sh'
}
}
}
post {
success {
slackSend color: 'good',
message: "Build #${env.BUILD_NUMBER} succeeded"
}
failure {
slackSend color: 'danger',
message: "Build #${env.BUILD_NUMBER} failed"
}
}
}
GitLab CI
.gitlab-ci.yml:
image: php:7.0
services:
- mysql:5.7
- redis:alpine
variables:
MYSQL_DATABASE: laravel_test
MYSQL_ROOT_PASSWORD: secret
cache:
paths:
- vendor/
- node_modules/
stages:
- test
- build
- deploy
before_script:
- apt-get update -yqq
- apt-get install -yqq git libpq-dev libcurl4-gnutls-dev
- docker-php-ext-install pdo_mysql curl
- curl -sS https://getcomposer.org/installer | php
- php composer.phar install --no-interaction
- cp .env.gitlab .env
- php artisan key:generate
- php artisan migrate --force
test:unit:
stage: test
script:
- vendor/bin/phpunit --testsuite Unit
test:feature:
stage: test
script:
- vendor/bin/phpunit --testsuite Feature
code_style:
stage: test
script:
- vendor/bin/phpcs --standard=PSR2 app/
build:assets:
stage: build
script:
- npm install
- npm run production
artifacts:
paths:
- public/css
- public/js
deploy:staging:
stage: deploy
script:
- apt-get install -yqq ssh
- ssh-keyscan -H $STAGING_SERVER >> ~/.ssh/known_hosts
- ssh $STAGING_USER@$STAGING_SERVER "cd /var/www/app && ./deploy.sh"
only:
- develop
environment:
name: staging
url: https://staging.example.com
deploy:production:
stage: deploy
script:
- ssh $PROD_USER@$PROD_SERVER "cd /var/www/app && ./deploy.sh"
only:
- main
when: manual
environment:
name: production
url: https://example.com
GitHub Actions
.github/workflows/laravel.yml:
name: Laravel CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
test:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:5.7
env:
MYSQL_DATABASE: laravel_test
MYSQL_ROOT_PASSWORD: password
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
redis:
image: redis:alpine
ports:
- 6379:6379
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 7.0
extensions: mbstring, pdo_mysql, redis
coverage: xdebug
- name: Copy .env
run: cp .env.testing .env
- name: Install Composer Dependencies
run: composer install --no-interaction --prefer-dist
- name: Generate Application Key
run: php artisan key:generate
- name: Run Migrations
run: php artisan migrate --force
env:
DB_PORT: ${{ job.services.mysql.ports[3306] }}
- name: Install NPM Dependencies
run: npm install
- name: Build Assets
run: npm run production
- name: Run Tests
run: vendor/bin/phpunit --coverage-clover coverage.xml
- name: Upload Coverage
uses: codecov/codecov-action@v1
with:
file: ./coverage.xml
- name: Code Style Check
run: vendor/bin/phpcs --standard=PSR2 app/
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v2
- name: Deploy to Production
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
script: |
cd /var/www/app
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
sudo supervisorctl restart laravel-worker:*
Code Quality Tools
PHP CodeSniffer
Install:
composer require --dev squizlabs/php_codesniffer
phpcs.xml:
<?xml version="1.0"?>
<ruleset name="Laravel">
<description>Laravel coding standard</description>
<rule ref="PSR2"/>
<file>app</file>
<file>config</file>
<file>routes</file>
<file>tests</file>
<exclude-pattern>*/migrations/*</exclude-pattern>
<exclude-pattern>*/cache/*</exclude-pattern>
<exclude-pattern>*.blade.php</exclude-pattern>
<arg name="colors"/>
<arg value="sp"/>
</ruleset>
Run:
vendor/bin/phpcs
vendor/bin/phpcbf # Auto-fix issues
PHPStan
Install:
composer require --dev phpstan/phpstan
phpstan.neon:
parameters:
level: 5
paths:
- app
excludes_analyse:
- app/Http/Middleware/TrustProxies.php
ignoreErrors:
- '#Unsafe usage of new static#'
Run:
vendor/bin/phpstan analyse
PHP Mess Detector
Install:
composer require --dev phpmd/phpmd
Run:
vendor/bin/phpmd app text cleancode,codesize,controversial,design,naming,unusedcode
Test Coverage
phpunit.xml:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="vendor/autoload.php"
colors="true">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
</whitelist>
</filter>
<logging>
<log type="coverage-html" target="coverage"/>
<log type="coverage-clover" target="coverage.xml"/>
</logging>
</phpunit>
Generate coverage:
vendor/bin/phpunit --coverage-html coverage
Deployment Scripts
deploy.sh:
#!/bin/bash
set -e
echo "Starting deployment..."
# Maintenance mode
php artisan down || true
# Pull latest code
git fetch origin
git reset --hard origin/main
# Install dependencies
composer install --no-dev --optimize-autoloader --no-interaction
# Clear caches
php artisan config:clear
php artisan cache:clear
# Run migrations
php artisan migrate --force
# Optimize
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Build assets
npm install --production
npm run production
# Restart services
sudo supervisorctl restart laravel-worker:*
# Exit maintenance mode
php artisan up
echo "Deployment complete!"
Make executable:
chmod +x deploy.sh
Zero-Downtime Deployment
deploy-zero-downtime.sh:
#!/bin/bash
set -e
RELEASE_DIR="/var/www/releases/$(date +%Y%m%d%H%M%S)"
CURRENT_DIR="/var/www/current"
SHARED_DIR="/var/www/shared"
echo "Creating release directory: $RELEASE_DIR"
mkdir -p $RELEASE_DIR
# Clone repository
git clone --depth 1 -b main https://github.com/youruser/yourapp.git $RELEASE_DIR
cd $RELEASE_DIR
# Link shared directories
ln -s $SHARED_DIR/.env .env
ln -s $SHARED_DIR/storage storage
# Install dependencies
composer install --no-dev --optimize-autoloader
# Build assets
npm install --production
npm run production
# Run migrations (on shared database)
php artisan migrate --force
# Cache config
php artisan config:cache
php artisan route:cache
php artisan view:cache
# Swap symlink
ln -nfs $RELEASE_DIR $CURRENT_DIR
# Restart services
sudo systemctl reload php7.0-fpm
sudo supervisorctl restart laravel-worker:*
# Cleanup old releases (keep last 5)
cd /var/www/releases
ls -t | tail -n +6 | xargs rm -rf
echo "Deployment complete!"
Environment Variables in CI
Travis CI:
env:
global:
- APP_KEY=base64:your-key
- secure: "encrypted-value"
Jenkins: Use Credentials Plugin to store secrets
GitLab CI: Settings → CI/CD → Variables
GitHub Actions: Settings → Secrets
Database Seeding in CI
DatabaseSeeder.php:
<?php
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
public function run()
{
if (app()->environment('testing')) {
$this->call(TestDataSeeder::class);
} elseif (app()->environment('local')) {
$this->call(DevelopmentSeeder::class);
}
}
}
CI configuration:
before_script:
- php artisan migrate:fresh --seed
Parallel Testing
Run tests in parallel:
# Install parallel runner
composer require --dev brianium/paratest
# Run tests
vendor/bin/paratest --processes 4
.travis.yml:
script:
- vendor/bin/paratest --processes 4
Browser Testing
Install Laravel Dusk:
composer require --dev laravel/dusk
php artisan dusk:install
CI configuration:
before_script:
- google-chrome --version
- ./vendor/laravel/dusk/bin/chromedriver-linux &
script:
- php artisan dusk
DuskTestCase.php:
<?php
namespace Tests;
use Laravel\Dusk\TestCase as BaseTestCase;
use Facebook\WebDriver\Chrome\ChromeOptions;
use Facebook\WebDriver\Remote\RemoteWebDriver;
use Facebook\WebDriver\Remote\DesiredCapabilities;
abstract class DuskTestCase extends BaseTestCase
{
public static function prepare()
{
static::startChromeDriver();
}
protected function driver()
{
$options = (new ChromeOptions)->addArguments([
'--disable-gpu',
'--headless',
'--no-sandbox',
'--window-size=1920,1080',
]);
return RemoteWebDriver::create(
'http://localhost:9515',
DesiredCapabilities::chrome()->setCapability(
ChromeOptions::CAPABILITY, $options
)
);
}
}
Notifications
Slack notifications:
# Install Slack integration
composer require maknz/slack
# Configure webhook in .env
SLACK_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK/URL
Notify on deployment:
<?php
use Maknz\Slack\Facades\Slack;
Slack::to('#deployments')
->attach([
'title' => 'Deployment Successful',
'text' => 'Version 2.1.0 deployed to production',
'color' => 'good'
])
->send();
Best Practices
- Run tests on every commit
- Use consistent environments (Docker helps)
- Keep builds fast (< 10 minutes)
- Cache dependencies
- Fail fast (run unit tests first)
- Monitor deployment success
- Automate everything
- Use feature flags for risky changes
Monitoring Build Health
Track metrics:
- Build success rate
- Average build time
- Test coverage percentage
- Code quality scores
- Deployment frequency
Tools:
- Codecov for coverage
- Code Climate for quality
- Grafana for metrics
Conclusion
Continuous Integration transforms Laravel development from error-prone manual processes to reliable automated pipelines. At ZIRA Software, CI/CD has eliminated entire classes of bugs and enabled us to deploy multiple times daily with confidence.
Start simple—automate your tests first. Add deployment automation when comfortable. Your future self will thank you.
Ready to implement CI/CD for your Laravel application? Contact ZIRA Software to discuss how we can set up robust automated testing and deployment pipelines for your team.