laravelci-cddevopsgithub-actionsautomatización

CI/CD en Laravel: Pipelines automatizados con GitHub Actions

Introducción

En el desarrollo moderno de aplicaciones Laravel, la integración continua y el despliegue continuo (CI/CD) se han convertido en una práctica indispensable. Sin embargo, muchos desarrolladores todavía despliegan manualmente sus aplicaciones, ejecutan tests localmente y confían en procesos manuales que son propensos a errores.

Los pipelines CI/CD automatizan estos procesos, garantizando que cada cambio en tu código sea probado, validado y desplegado de manera consistente. GitHub Actions es la solución perfecta para esto: es gratuita, está integrada en GitHub y es relativamente sencilla de configurar.

En este artículo, te mostraré cómo configurar un pipeline CI/CD completo para tus proyectos Laravel, desde la ejecución de tests hasta el despliegue automático en producción.

¿Qué es un pipeline CI/CD?

Un pipeline CI/CD es un conjunto de automatizaciones que se ejecutan cuando realizas cambios en tu código. Típicamente incluye:

  • Integración Continua (CI): Ejecuta tests y valida la calidad del código automáticamente
  • Despliegue Continuo (CD): Despliega tu aplicación en servidores de producción de manera automática

Beneficios principales

  • Detectar errores antes de llegar a producción
  • Reducir errores humanos en el despliegue
  • Acelerar el ciclo de desarrollo
  • Mantener estándares de calidad consistentes
  • Despliegues más frecuentes y confiables

Configuración inicial de GitHub Actions

GitHub Actions utiliza archivos YAML ubicados en .github/workflows/ para definir los pipelines. Vamos a crear nuestro primer workflow.

Crear el archivo de configuración

Crea el archivo .github/workflows/laravel.yml:

name: Laravel CI/CD

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  laravel-tests:
    runs-on: ubuntu-latest
    
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: laravel_test
        options: >-
          --health-cmd="mysqladmin ping"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=3
        ports:
          - 3306:3306

    steps:
    - uses: actions/checkout@v4
    
    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.3'
        extensions: dom, curl, libxml, mbstring, zip
        coverage: pcov

    - 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@v3
      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 --no-interaction --prefer-dist

    - name: Create Environment File
      run: cp .env.example .env.testing

    - name: Generate Application Key
      run: php artisan key:generate --env=testing

    - name: Run Migrations
      run: php artisan migrate --env=testing
      env:
        DB_CONNECTION: mysql
        DB_HOST: 127.0.0.1
        DB_PORT: 3306
        DB_DATABASE: laravel_test
        DB_USERNAME: root
        DB_PASSWORD: root

    - name: Run Tests
      run: php artisan test --coverage
      env:
        DB_CONNECTION: mysql
        DB_HOST: 127.0.0.1
        DB_PORT: 3306
        DB_DATABASE: laravel_test
        DB_USERNAME: root
        DB_PASSWORD: root

    - name: Run PHP Code Sniffer
      run: ./vendor/bin/phpcs app --standard=PSR12
      continue-on-error: true

    - name: Run Laravel Pint
      run: ./vendor/bin/pint --test
      continue-on-error: true

    - name: Upload Coverage Reports
      uses: codecov/codecov-action@v3
      if: always()
      with:
        files: ./coverage.xml

Explicación detallada del workflow

Triggers del workflow

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

El workflow se ejecuta cuando:

  • Haces push a las ramas main o develop
  • Se crea un pull request a esas ramas

Configuración del entorno

services:
  mysql:
    image: mysql:8.0
    env:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: laravel_test

Iniciamos un servicio MySQL en paralelo para que los tests puedan interactuar con la base de datos.

Instalación de dependencias

- name: Cache Composer Dependencies
  uses: actions/cache@v3
  with:
    path: ${{ steps.composer-cache.outputs.dir }}
    key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}

Cachea las dependencias de Composer para acelerar las ejecuciones futuras.

Agregar validación de código

Configurar Laravel Pint

Laravel Pint es el formateador de código oficial de Laravel. Asegúrate de que esté instalado:

composer require --dev laravel/pint

Crea un archivo pint.json en la raíz de tu proyecto:

{
    "preset": "laravel",
    "exclude": [
        "vendor",
        "node_modules"
    ]
}

Agregar Static Analysis

Agrega PHPStan para análisis estático de código:

composer require --dev phpstan/phpstan

Crea phpstan.neon:

includes:
    - phpstan-baseline.neon

parameters:
    level: 5
    paths:
        - app
    excludePaths:
        - app/Providers
    tmpDir: build/phpstan

Actualiza tu workflow para incluir PHPStan:

- name: Run PHPStan
  run: ./vendor/bin/phpstan analyse
  continue-on-error: true

Despliegue automático a producción

Una vez que tus tests pasen, puedes desplegar automáticamente. Aquí está una configuración para desplegar a un servidor VPS:

deploy:
  needs: laravel-tests
  runs-on: ubuntu-latest
  if: github.ref == 'refs/heads/main' && github.event_name == 'push'
  
  steps:
  - uses: actions/checkout@v4
  
  - name: Deploy to Production
    uses: appleboy/ssh-action@master
    with:
      host: ${{ secrets.HOST }}
      username: ${{ secrets.USERNAME }}
      key: ${{ secrets.SSH_PRIVATE_KEY }}
      script: |
        cd /var/www/app
        git pull origin main
        composer install --no-interaction --prefer-dist --optimize-autoloader
        php artisan migrate --force
        php artisan cache:clear
        php artisan config:clear
        sudo systemctl restart php-fpm

Configurar secretos en GitHub

Ve a tu repositorio → Settings → Secrets and variables → Actions y agrega:

  • HOST: IP de tu servidor
  • USERNAME: Usuario SSH
  • SSH_PRIVATE_KEY: Tu clave privada SSH

Ejemplo práctico: Pipeline completo

Aquí está un workflow más robusto que combina todo:

name: Complete Laravel Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main, develop ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: laravel_test
        options: >-
          --health-cmd="mysqladmin ping"
          --health-interval=10s
        ports:
          - 3306:3306

    strategy:
      matrix:
        php-version: ['8.2', '8.3']

    steps:
    - uses: actions/checkout@v4

    - name: Setup PHP ${{ matrix.php-version }}
      uses: shivammathur/setup-php@v2
      with:
        php-version: ${{ matrix.php-version }}
        extensions: dom, curl, libxml, mbstring, zip, pdo, mysql
        coverage: pcov

    - name: Install Composer dependencies
      run: composer install --prefer-dist --no-progress

    - name: Copy environment file
      run: cp .env.example .env.testing

    - name: Generate app key
      run: php artisan key:generate --env=testing

    - name: Run migrations
      run: php artisan migrate --env=testing
      env:
        DB_HOST: 127.0.0.1

    - name: Execute tests
      run: php artisan test --coverage --coverage-clover=coverage.xml
      env:
        DB_HOST: 127.0.0.1

    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        files: ./coverage.xml

  code-quality:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v4

    - name: Setup PHP
      uses: shivammathur/setup-php@v2
      with:
        php-version: '8.3'

    - name: Install dependencies
      run: composer install --prefer-dist

    - name: Run Pint
      run: ./vendor/bin/pint --test

    - name: Run PHPStan
      run: ./vendor/bin/phpstan analyse
      continue-on-error: true

  deploy:
    needs: [test, code-quality]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    
    steps:
    - uses: actions/checkout@v4

    - name: Deploy to server
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.DEPLOY_HOST }}
        username: ${{ secrets.DEPLOY_USER }}
        key: ${{ secrets.DEPLOY_KEY }}
        script: |
          cd /var/www/laravel-app
          git fetch origin
          git checkout main
          git pull
          composer install --no-dev --optimize-autoloader
          php artisan migrate --force
          php artisan cache:clear
          php artisan config:clear
          php artisan queue:restart

Mejores prácticas para CI/CD en Laravel

1. Usa variables de entorno específicas para testing

// .env.testing
APP_ENV=testing
DB_CONNECTION=testing
CACHE_DRIVER=array
QUEUE_CONNECTION=sync

2. Mantén tests rápidos

// tests/Unit/ExampleTest.php
class ExampleTest extends TestCase
{
    #[Test]
    public function it_returns_expected_result()
    {
        $result = calculateSomething();
        
        $this->assertEquals(expected: true, actual: $result);
    }
}

3. Usa matrizas para probar múltiples versiones

strategy:
  matrix:
    php-version: ['8.2', '8.3']
    laravel-version: ['11', '13']

4. Cachea dependencias agresivamente

- name: Cache dependencies
  uses: actions/cache@v3
  with:
    path: ~/.composer/cache
    key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}

5. Notifica sobre fallos

- name: Slack Notification
  if: failure()
  uses: slackapi/slack-github-action@v1
  with:
    payload: |
      {
        "text": "Pipeline failed for ${{ github.repository }}"
      }
  env:
    SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}

Troubleshooting común

Problema: Tests fallan por base de datos no lista

Solución: Asegúrate de que MySQL esté completamente iniciado:

services:
  mysql:
    options: >-
      --health-cmd="mysqladmin ping"
      --health-interval=10s
      --health-timeout=5s
      --health-retries=3

Problema: Composer cache no funciona

Solución: Obtén correctamente la ruta del cache:

- name: Get Composer Cache
  id: composer-cache
  run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT

- uses: actions/cache@v3
  with:
    path: ${{ steps.composer-cache.outputs.dir }}

Problema: Despliegue falla por permisos

Solución: Verifica que tu usuario SSH tenga permisos adecuados en el servidor:

# En tu servidor
sudo usermod -aG www-data deploy-user
sudo chown -R deploy-user:www-data /var/www/app

Conclusión

Implementar CI/CD en tus proyectos Laravel con GitHub Actions te proporciona automatización, confiabilidad y tranquilidad. Con la configuración que te mostré en este artículo, ahora puedes:

  • Ejecutar tests automáticamente en cada push
  • Validar la calidad del código
  • Desplegar automáticamente a producción
  • Detectar errores antes de que lleguen a usuarios

La clave es comenzar simple y escalar gradualmente. Empieza con tests, luego agrega análisis de código, y finalmente automatiza tus despliegues.

Puntos clave

  • GitHub Actions es gratuito y está integrado en GitHub, ideal para CI/CD en Laravel
  • Cachea dependencias de Composer para acelerar los workflows
  • Usa servicios como MySQL para testear con bases de datos reales
  • Configura múltiples jobs (test, análisis, despliegue) que se ejecutan en paralelo
  • Valida código con Laravel Pint y PHPStan antes del despliegue
  • Automatiza despliegues solo en ramas principales (main/production)
  • Usa secretos de GitHub para credenciales SSH y tokens
  • Monitorea el estado con notificaciones en Slack o correo
  • Prueba múltiples versiones de PHP usando matrices
  • Mantén workflows simples al inicio y evoluciona según necesidades