CI/CD
Com configurar pipelines de CI/CD per a Laravel: GitHub Actions, GitLab CI, tests automatitzats, anàlisi de codi i desplegament continu.
Què és CI/CD?#
CI/CD són les sigles de Continuous Integration (Integració Contínua) i Continuous Deployment (Desplegament Continu). Són dues pràctiques que, combinades, automatitzen tot el procés des que un desenvolupador puja codi fins que aquest codi s'executa en producció. La idea fonamental és simple: cada canvi al codi ha de passar per una sèrie de verificacions automàtiques abans d'arribar a producció, i el desplegament en si també ha de ser automàtic.
La integració contínua s'encarrega de verificar que el codi nou no trenca res. Cada vegada que algú fa push a una branca o obre un pull request, el pipeline executa automàticament els tests, comprova l'estil del codi, fa anàlisi estàtica i verifica que l'aplicació es pot construir correctament. Si alguna d'aquestes verificacions falla, el desenvolupador rep una notificació immediata i pot corregir el problema abans que arribi a la branca principal.
El desplegament continu va un pas més enllà: quan el codi passa totes les verificacions i es fusiona a la branca principal, es desplega automàticament a producció. Això elimina el procés manual de desplegament, que és propens a errors humans, i permet desplegar amb confiança desenes de vegades al dia si cal.
Per què és important?#
Sense CI/CD, el procés típic és: un desenvolupador puja codi, algú el revisa manualment, algú altre recorda executar els tests al seu ordinador, i quan toca desplegar, algú es connecta al servidor per SSH i executa una sèrie de comandes que espera recordar correctament. Cada pas manual és una oportunitat perquè alguna cosa surti malament.
Amb CI/CD, tot això és automàtic i repetible. Els tests s'executen sempre, sense excepcions. L'estil del codi es verifica a cada push. L'anàlisi estàtica detecta bugs potencials abans que arribin a producció. I el desplegament es fa exactament de la mateixa manera cada vegada, sense dependre de la memòria o la disponibilitat de ningú.
Les etapes d'un pipeline#
Un pipeline de CI/CD típic per a una aplicació Laravel segueix aquestes etapes, cadascuna construint sobre l'anterior:
- Lint (estil de codi) - Verifica que el codi segueix les convencions d'estil definides per l'equip. Laravel Pint és l'eina estàndard.
- Anàlisi estàtica - Detecta bugs potencials sense executar el codi. PHPStan amb Larastan és l'opció recomanada.
- Tests - Executa els tests unitaris i funcionals per verificar que l'aplicació funciona correctament.
- Build - Compila els assets del frontend i verifica que l'aplicació es pot construir.
- Deploy - Desplega l'aplicació a producció (o a un entorn de staging per a pull requests).
GitHub Actions#
GitHub Actions és el sistema de CI/CD integrat a GitHub. Si el teu repositori està a GitHub, és l'opció més natural perquè no cal configurar cap servei extern: els workflows s'executen directament als servidors de GitHub. Els workflows es defineixen en fitxers YAML dins del directori .github/workflows/ del repositori.
Pipeline complet#
El workflow següent és un pipeline complet per a una aplicació Laravel. Executa Pint per verificar l'estil del codi, Larastan per a l'anàlisi estàtica, els tests amb una base de dades MySQL real i verifica que els assets es compilen correctament. Tot això en paral·lel quan és possible, per minimitzar el temps d'execució:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint:
name: Estil de codi
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2
coverage: none
- name: Cache Composer
uses: actions/cache@v4
with:
path: vendor
key: composer-${{ hashFiles('composer.lock') }}
restore-keys: composer-
- name: Instal·lar dependències
run: composer install --prefer-dist --no-progress --no-interaction
- name: Executar Pint
run: ./vendor/bin/pint --test
analysis:
name: Anàlisi estàtica
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2
coverage: none
- name: Cache Composer
uses: actions/cache@v4
with:
path: vendor
key: composer-${{ hashFiles('composer.lock') }}
restore-keys: composer-
- name: Instal·lar dependències
run: composer install --prefer-dist --no-progress --no-interaction
- name: Executar PHPStan / Larastan
run: ./vendor/bin/phpstan analyse --memory-limit=512M
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_DATABASE: testing
MYSQL_USER: laravel
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
ports:
- 3306:3306
options: >-
--health-cmd="mysqladmin ping -h localhost"
--health-interval=10s
--health-timeout=5s
--health-retries=5
redis:
image: redis:7-alpine
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: mbstring, pdo_mysql, redis, gd, intl, zip, bcmath
tools: composer:v2
coverage: xdebug
- name: Cache Composer
uses: actions/cache@v4
with:
path: vendor
key: composer-php${{ matrix.php }}-${{ hashFiles('composer.lock') }}
restore-keys: composer-php${{ matrix.php }}-
- name: Instal·lar dependències
run: composer install --prefer-dist --no-progress --no-interaction
- name: Preparar entorn
run: |
cp .env.example .env
php artisan key:generate
- name: Executar migracions
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: testing
DB_USERNAME: laravel
DB_PASSWORD: password
run: php artisan migrate --force
- name: Executar tests
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: testing
DB_USERNAME: laravel
DB_PASSWORD: password
REDIS_HOST: 127.0.0.1
REDIS_PORT: 6379
run: php artisan test --parallel --coverage-clover coverage.xml
- name: Pujar cobertura
if: matrix.php == '8.3'
uses: codecov/codecov-action@v4
with:
files: coverage.xml
token: ${{ secrets.CODECOV_TOKEN }}
assets:
name: Compilar assets
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Instal·lar dependències npm
run: npm ci --no-audit --no-fund
- name: Compilar assets
run: npm run build
- name: Verificar que no hi ha canvis sense cometre
run: |
if [ -n "$(git status --porcelain public/build)" ]; then
echo "Els assets compilats no estan actualitzats. Executa 'npm run build' i puja els canvis."
exit 1
fi
security:
name: Auditoria de seguretat
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer:v2
coverage: none
- name: Auditar dependències PHP
run: composer audit
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Auditar dependències npm
run: npm audit --audit-level=highAnalitzem els aspectes clau d'aquest workflow.
Matrix strategy#
La secció strategy.matrix permet executar els tests amb múltiples versions de PHP en paral·lel. Amb fail-fast: false, si els tests fallen amb PHP 8.2, els tests amb PHP 8.3 continuen executant-se. Això és útil per saber si un error és específic d'una versió de PHP o afecta totes les versions. En el nostre cas, testem amb PHP 8.2 i 8.3 per assegurar compatibilitat.
Service containers#
GitHub Actions permet arrencar serveis Docker (MySQL, Redis, PostgreSQL) com a contenidors al costat del runner. Aquests serveis són accessibles via 127.0.0.1 amb els ports definits. Els health checks garanteixen que el servei està llest abans que els tests comencin a executar-se. Fixeu-vos que les credencials del servei MySQL coincideixen amb les variables d'entorn dels passos que executen els tests.
Cache de dependències#
La cache de Composer i npm és fonamental per al rendiment del pipeline. Sense cache, cada execució ha de descarregar totes les dependències des de zero, cosa que pot trigar minuts. Amb la clau de cache basada en el hash de composer.lock, la cache s'invalida automàticament quan les dependències canvien. L'ús de restore-keys permet reutilitzar una cache parcial si el hash exacte no coincideix.
Tests en paral·lel#
El flag --parallel de php artisan test executa els tests en múltiples processos simultàniament, reduint significativament el temps d'execució. En un projecte amb centenars de tests, la diferència pot ser de minuts. Laravel gestiona automàticament la creació de bases de dades separades per a cada procés paral·lel.
Si els teus tests no necessiten una base de dades MySQL completa, pots fer servir SQLite en memòria per accelerar encara més el pipeline. Configura DB_CONNECTION=sqlite i DB_DATABASE=:memory: a les variables d'entorn. Alguns tests que depenen de funcionalitats específiques de MySQL (com JSON columns o fulltext search) poden fallar amb SQLite, així que avalua-ho segons el teu projecte.
Job de desplegament#
Un cop els tests passen, el pas de desplegament posa el codi en producció. Hi ha diverses estratègies de desplegament, cadascuna amb els seus avantatges. Afegeix un job de deploy al workflow anterior:
Desplegament via webhook (Laravel Forge)#
La manera més senzilla de desplegar amb Forge és disparar un webhook que activa l'script de desplegament configurat al panel de Forge:
deploy-forge:
name: Desplegar a Forge
runs-on: ubuntu-latest
needs: [lint, analysis, tests, assets, security]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Disparar desplegament a Forge
run: |
curl -s -X POST \
"${{ secrets.FORGE_DEPLOY_WEBHOOK }}" \
--max-time 30El camp needs assegura que el desplegament només s'executa si tots els jobs anteriors (lint, analysis, tests, assets, security) han passat. La condició if limita el desplegament a pushes directes a la branca main, excloent pull requests.
Desplegament via SSH#
Si no fas servir Forge, pots desplegar connectant-te directament al servidor via SSH. Necessites configurar una clau SSH privada com a secret de GitHub:
deploy-ssh:
name: Desplegar via SSH
runs-on: ubuntu-latest
needs: [lint, analysis, tests, assets, security]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Configurar SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
- name: Desplegar
run: |
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'DEPLOY'
cd /var/www/laravelandorra.com
php artisan down --retry=60
git pull origin main
composer install --no-interaction --prefer-dist --optimize-autoloader --no-dev
npm ci --no-audit --no-fund && npm run build
php artisan migrate --force
php artisan optimize
php artisan queue:restart
php artisan up
DEPLOYL'script de desplegament segueix un ordre molt concret i important. Primer posa l'aplicació en mode manteniment (artisan down) perquè els usuaris vegin una pàgina de manteniment en lloc d'errors. Després actualitza el codi, instal·la les dependències, executa les migracions, optimitza les caches i reinicia els workers. Finalment, torna a activar l'aplicació (artisan up). L'opció --retry=60 indica als clients que tornin a intentar la petició després de 60 segons.
Desplegament d'imatge Docker#
Si desplegues amb Docker, el pipeline construeix la imatge, la puja al registre i actualitza el servei en producció:
deploy-docker:
name: Desplegar imatge Docker
runs-on: ubuntu-latest
needs: [lint, analysis, tests, assets, security]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
permissions:
packages: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Iniciar sessió a GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Construir i pujar imatge
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
- name: Desplegar al servidor
run: |
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'DEPLOY'
docker pull ghcr.io/${{ github.repository }}:${{ github.sha }}
cd /opt/laravel
docker compose up -d --no-deps app worker scheduler
DEPLOYL'avantatge de desplegar amb Docker és que la imatge que s'executa en producció és exactament la mateixa que ha passat tots els tests. No hi ha diferències d'entorn, no hi ha dependències que s'instal·lin de manera diferent, no hi ha assets que es compilin amb versions de Node diferents. El que funciona al pipeline funciona en producció, punt.
Badges d'estat#
Afegeix badges al README.md del teu repositori perquè tothom pugui veure d'un cop d'ull si el pipeline passa correctament:
Quan el pipeline falla, el badge es torna vermell i qualsevol persona que visiti el repositori sabrà que hi ha un problema. Això crea una pressió positiva per mantenir el pipeline en verd.
GitLab CI#
Si el teu repositori està a GitLab, el sistema de CI/CD integrat es configura amb un fitxer .gitlab-ci.yml a l'arrel del projecte. GitLab CI té una filosofia lleugerament diferent de GitHub Actions: els jobs s'organitzen en stages (etapes) que s'executen seqüencialment, i tots els jobs dins d'un stage s'executen en paral·lel.
# .gitlab-ci.yml
stages:
- quality
- test
- build
- deploy
variables:
MYSQL_DATABASE: testing
MYSQL_USER: laravel
MYSQL_PASSWORD: password
MYSQL_ROOT_PASSWORD: password
DB_HOST: mysql
DB_DATABASE: testing
DB_USERNAME: laravel
DB_PASSWORD: password
# Cache global de Composer
cache:
key:
files:
- composer.lock
paths:
- vendor/
policy: pull
# Job base per a PHP
.php-setup:
image: php:8.3
before_script:
- apt-get update && apt-get install -y git unzip libzip-dev libpng-dev libicu-dev
- docker-php-ext-install pdo_mysql zip gd intl bcmath
- curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
- composer install --prefer-dist --no-progress --no-interaction
# ==========================================
# Stage: Quality
# ==========================================
lint:
extends: .php-setup
stage: quality
cache:
key:
files:
- composer.lock
paths:
- vendor/
policy: pull-push
script:
- ./vendor/bin/pint --test
phpstan:
extends: .php-setup
stage: quality
script:
- ./vendor/bin/phpstan analyse --memory-limit=512M
security-audit:
extends: .php-setup
stage: quality
script:
- composer audit
allow_failure: true
# ==========================================
# Stage: Test
# ==========================================
test:php83:
extends: .php-setup
stage: test
image: php:8.3
services:
- name: mysql:8.0
alias: mysql
- name: redis:7-alpine
alias: redis
variables:
REDIS_HOST: redis
REDIS_PORT: "6379"
script:
- cp .env.example .env
- php artisan key:generate
- php artisan migrate --force
- php artisan test --parallel --coverage-text
coverage: '/Lines:\s+(\d+\.\d+)%/'
test:php82:
extends: test:php83
image: php:8.2
# ==========================================
# Stage: Build
# ==========================================
build-assets:
stage: build
image: node:20-alpine
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
script:
- npm ci --no-audit --no-fund
- npm run build
artifacts:
paths:
- public/build/
expire_in: 1 hour
build-docker:
stage: build
image: docker:24
services:
- docker:24-dind
variables:
DOCKER_TLS_CERTDIR: "/certs"
only:
- main
- tags
script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA -t $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
# ==========================================
# Stage: Deploy
# ==========================================
deploy-staging:
stage: deploy
image: alpine:latest
only:
- develop
before_script:
- apk add --no-cache openssh-client
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H $STAGING_HOST >> ~/.ssh/known_hosts
script:
- ssh $SSH_USER@$STAGING_HOST "cd /var/www/staging && git pull && composer install --no-dev --optimize-autoloader && php artisan migrate --force && php artisan optimize"
environment:
name: staging
url: https://staging.laravelandorra.com
deploy-production:
stage: deploy
image: alpine:latest
only:
- main
when: manual
before_script:
- apk add --no-cache openssh-client
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa
- ssh-keyscan -H $PRODUCTION_HOST >> ~/.ssh/known_hosts
script:
- ssh $SSH_USER@$PRODUCTION_HOST "cd /var/www/production && git pull && composer install --no-dev --optimize-autoloader && php artisan migrate --force && php artisan optimize && php artisan queue:restart"
environment:
name: production
url: https://laravelandorra.comHi ha diverses diferències importants respecte a GitHub Actions que val la pena destacar.
Templates amb extends#
GitLab CI permet definir templates (prefixats amb un punt, com .php-setup) que altres jobs poden estendre. Això evita la duplicació de configuració entre jobs que comparteixen la mateixa configuració base. L'equivalent a GitHub Actions seria un workflow reutilitzable o un action personalitzat.
Cache global#
La cache de GitLab CI es pot definir globalment i aplicar a tots els jobs. La política pull descarrega la cache però no la puja (útil per a la majoria de jobs), mentre que pull-push descarrega i puja la cache actualitzada (útil per al primer job que instal·la les dependències). Això assegura que la cache es genera una sola vegada i es comparteix entre tots els jobs.
Desplegament manual vs automàtic#
El desplegament a staging es fa automàticament quan es puja codi a la branca develop. El desplegament a producció, en canvi, està configurat amb when: manual, cosa que significa que requereix que algú cliqui un botó al panel de GitLab per iniciar-lo. Això dóna un control addicional per a l'entorn de producció: el codi ha passat totes les verificacions automàtiques, però un humà pren la decisió final de desplegar.
Environments#
GitLab CI suporta environments, que permeten veure al panel de GitLab quina versió del codi s'executa a cada entorn (staging, producció) i accedir directament a la URL del servei.
Tasques comuns de CI#
Verificació d'estil de codi amb Pint#
Laravel Pint és el formatejador de codi oficial de Laravel. El flag --test verifica que el codi segueix les convencions sense modificar-lo. Si troba alguna desviació, surt amb un codi d'error i el pipeline falla:
./vendor/bin/pint --testSi vols que Pint corregeixi automàticament l'estil i pugui els canvis, pots afegir un pas addicional al pipeline:
- name: Corregir estil amb Pint
if: failure()
run: |
./vendor/bin/pint
git config user.name "GitHub Actions"
git config user.email "actions@github.com"
git add -A
git diff-index --quiet HEAD || git commit -m "style: corregir estil amb Pint"
git pushLa correcció automàtica d'estil al pipeline és convenient però pot ser problemàtica si no s'utilitza amb cura. Pot crear conflictes amb pull requests oberts i dificulta la revisió de codi perquè barreja canvis funcionals amb canvis d'estil. Molts equips prefereixen que els desenvolupadors executin Pint localment abans de fer push.
Anàlisi estàtica amb Larastan#
Larastan és una extensió de PHPStan específica per a Laravel. Entén les facades, les relacions d'Eloquent, les col·leccions i molts altres patrons de Laravel, i pot detectar errors que PHPStan sol no detectaria:
# Instal·lar Larastan
composer require --dev larastan/larastan
# Executar l'anàlisi
./vendor/bin/phpstan analyse --memory-limit=512MLa configuració de Larastan es defineix al fitxer phpstan.neon a l'arrel del projecte:
includes:
- vendor/larastan/larastan/extension.neon
parameters:
paths:
- app/
level: 6
ignoreErrors: []
excludePaths: []El nivell 6 és un bon equilibri entre rigor i pragmatisme. Els nivells més alts (7, 8, 9) detecten més errors potencials però també generen més falsos positius, cosa que pot ser frustrant i contraproduent. Comença amb un nivell baix i augmenta-ho gradualment a mesura que corregeixis els errors existents.
Auditoria de seguretat#
La comanda composer audit comprova si alguna de les dependències instal·lades té vulnerabilitats de seguretat conegudes. És una verificació ràpida i valuosa que hauria de formar part de tots els pipelines:
# Auditar dependències PHP
composer audit
# Auditar dependències npm
npm audit --audit-level=highEl flag --audit-level=high de npm ignora les vulnerabilitats de severitat baixa i mitjana, que sovint són falsos positius o afecten funcionalitats que l'aplicació no utilitza. Només les vulnerabilitats de severitat alta fan fallar el pipeline.
Cobertura de tests#
La cobertura de tests mesura quin percentatge del codi s'executa durant els tests. No és una mètrica perfecta (100% de cobertura no significa 100% de qualitat), però és un indicador útil per identificar parts del codi que no estan testejades:
- name: Executar tests amb cobertura
run: php artisan test --coverage-clover coverage.xml --coverage-text
- name: Verificar cobertura mínima
run: |
COVERAGE=$(php -r "
\$xml = simplexml_load_file('coverage.xml');
\$metrics = \$xml->project->directory->totals->lines;
echo round((\$metrics['covered'] / \$metrics['count']) * 100, 2);
")
echo "Cobertura: ${COVERAGE}%"
if (( $(echo "$COVERAGE < 80" | bc -l) )); then
echo "La cobertura és inferior al 80%"
exit 1
fiTests de navegador (Dusk) al CI#
Laravel Dusk permet executar tests de navegador amb Chrome. Configurar Dusk al CI requereix una mica més de treball perquè cal un navegador Chrome headless:
dusk:
name: Tests de navegador
runs-on: ubuntu-latest
needs: [tests]
services:
mysql:
image: mysql:8.0
env:
MYSQL_DATABASE: testing
MYSQL_ROOT_PASSWORD: password
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_mysql
- name: Instal·lar dependències
run: composer install --prefer-dist --no-progress
- name: Preparar entorn
run: |
cp .env.example .env
php artisan key:generate
npm ci && npm run build
- name: Instal·lar Chrome
run: php artisan dusk:chrome-driver --detect
- name: Arrencar servidor
run: php artisan serve --port=8000 &
- name: Executar Dusk
env:
APP_URL: http://127.0.0.1:8000
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_DATABASE: testing
DB_USERNAME: root
DB_PASSWORD: password
run: php artisan dusk
- name: Pujar captures en cas d'error
if: failure()
uses: actions/upload-artifact@v4
with:
name: dusk-screenshots
path: tests/Browser/screenshots/L'últim pas és especialment útil: si algun test de Dusk falla, Laravel fa una captura de pantalla del navegador en el moment de l'error. Pujant aquestes captures com a artifacts, pots veure exactament què ha fallat sense haver de reproduir l'error localment.
Estratègies de desplegament#
Desplegar amb push a main#
La estratègia més comuna. Cada push (o merge) a la branca main desplega automàticament a producció. La condició és simple:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'Aquesta estratègia requereix que la branca main estigui protegida amb regles que obliguin a passar tots els checks abans de poder fusionar. D'altra manera, un push directe a main amb codi trencat desplegaria immediatament a producció.
Desplegar amb tags o releases#
Per a equips que prefereixen un control més explícit, el desplegament es pot vincular a la creació d'un tag de Git o una release de GitHub:
on:
release:
types: [published]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Desplegar versió ${{ github.event.release.tag_name }}
run: |
echo "Desplegant la versió ${{ github.event.release.tag_name }}"
# Comandes de desplegamentAmb aquesta estratègia, el flux és: desenvolupar a branques, fusionar a main, i quan estàs llest per desplegar, crear una release amb un tag semàntic (v1.2.3). El pipeline es dispara automàticament amb la release.
Desplegament manual#
Per a entorns on el desplegament ha de ser aprovat manualment, GitHub Actions ofereix workflow_dispatch:
on:
workflow_dispatch:
inputs:
environment:
description: "Entorn de desplegament"
required: true
type: choice
options:
- staging
- production
confirmation:
description: "Escriu 'DEPLOY' per confirmar"
required: true
type: string
jobs:
deploy:
runs-on: ubuntu-latest
if: github.event.inputs.confirmation == 'DEPLOY'
environment: ${{ github.event.inputs.environment }}
steps:
- name: Desplegar a ${{ github.event.inputs.environment }}
run: echo "Desplegant a ${{ github.event.inputs.environment }}"El camp de confirmació afegeix una capa de protecció: l'operador ha d'escriure literalment "DEPLOY" per confirmar que vol desplegar. L'ús d'environment a GitHub Actions permet configurar regles d'aprovació addicionals i limitar qui pot desplegar a cada entorn.
Preview deployments per a pull requests#
Els preview deployments creen un entorn temporal per a cada pull request, permetent revisar els canvis en un entorn real abans de fusionar-los a la branca principal:
preview:
name: Preview deployment
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
needs: [lint, analysis, tests]
steps:
- uses: actions/checkout@v4
- name: Desplegar preview
run: |
# Desplegament a un entorn temporal
# Varia segons la plataforma (Vercel, Forge, etc.)
echo "Preview disponible a: https://pr-${{ github.event.number }}.staging.laravelandorra.com"
- name: Comentar al PR
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `Preview desplegat a: https://pr-${{ github.event.number }}.staging.laravelandorra.com`
})Gestió de secrets#
Els secrets (contrasenyes, claus API, tokens SSH) mai s'han d'incloure al codi font. Tant GitHub com GitLab proporcionen mecanismes segurs per emmagatzemar i accedir a secrets dins dels pipelines.
GitHub Secrets#
A GitHub, els secrets es configuren a Settings > Secrets and variables > Actions. Es poden definir a nivell de repositori, organització o environment:
# Accedir a un secret al workflow
steps:
- name: Desplegar
env:
API_KEY: ${{ secrets.API_KEY }}
SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: echo "Desplegant amb la clau API..."Els secrets de GitHub estan encriptats i mai es mostren als logs del workflow. Si un step intenta imprimir un secret, GitHub el substitueix per ***. A més, els secrets dels forks no estan disponibles als pull requests per motius de seguretat, cosa que impedeix que algú pugui crear un fork, modificar el workflow i accedir als teus secrets.
GitLab CI/CD Variables#
A GitLab, les variables es configuren a Settings > CI/CD > Variables. Pots marcar-les com "Protected" (només disponibles a branques protegides) i "Masked" (ocultes als logs):
# Les variables es converteixen en variables d'entorn automàticament
deploy:
script:
- ssh -i $SSH_PRIVATE_KEY user@$PRODUCTION_HOST "deploy.sh"Revisa periòdicament els secrets dels teus pipelines. Elimina els que ja no s'utilitzen, rota les claus regularment (almenys cada 90 dies) i limita l'abast de cada secret al mínim necessari. Un secret amb permisos excessius compromès pot tenir conseqüències devastadores.
Notificacions#
Rebre notificacions quan el pipeline falla és crític per reaccionar ràpidament. Si ningú se n'adona que el pipeline està en vermell, el codi trencat pot acumular-se i fer molt més difícil trobar i corregir el problema.
Notificació a Slack#
notify:
name: Notificar
runs-on: ubuntu-latest
needs: [lint, analysis, tests, assets]
if: failure()
steps:
- name: Notificar a Slack
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "Pipeline fallit a ${{ github.repository }}",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "*Pipeline fallit* :x:\n*Repositori:* ${{ github.repository }}\n*Branca:* ${{ github.ref_name }}\n*Commit:* ${{ github.event.head_commit.message }}\n*Autor:* ${{ github.actor }}\n<${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|Veure detalls>"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_TYPE: INCOMING_WEBHOOKLa condició if: failure() assegura que la notificació només s'envia quan algun job ha fallat. Això evita notificacions innecessàries per cada push exitós, que acabarien sent ignorades per l'equip.
Notificació per correu#
- name: Notificar per correu
if: failure()
uses: dawidd6/action-send-mail@v3
with:
server_address: smtp.gmail.com
server_port: 465
username: ${{ secrets.MAIL_USERNAME }}
password: ${{ secrets.MAIL_PASSWORD }}
subject: "Pipeline fallit: ${{ github.repository }}"
to: equip@laravelandorra.com
from: CI/CD <ci@laravelandorra.com>
body: |
El pipeline ha fallit a la branca ${{ github.ref_name }}.
Commit: ${{ github.event.head_commit.message }}
Autor: ${{ github.actor }}
Detalls: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}Consells pràctics#
Mantenir el pipeline ràpid#
Un pipeline lent és un pipeline que la gent intenta evitar. Si els tests triguen 20 minuts, els desenvolupadors faran push de grans blocs de canvis en lloc de commits petits i freqüents, cosa que derrota el propòsit de la integració contínua. Algunes estratègies per mantenir el pipeline ràpid:
- Executar jobs en paral·lel: Lint, anàlisi i tests poden executar-se simultàniament perquè són independents.
- Aprofitar la cache: Composer i npm no han de descarregar totes les dependències a cada execució.
- Tests en paral·lel: L'opció
--paralleldivideix els tests entre múltiples processos. - SQLite per a tests ràpids: Si els tests no necessiten funcionalitats específiques de MySQL, SQLite en memòria és molt més ràpid.
- Limitar els tests de Dusk: Els tests de navegador són lents. Reserva'ls per als fluxos crítics i no els executis a cada push, sinó només a la branca principal.
Checklist de desplegament al CI#
Afegeix un pas que verifiqui que tot està llest per al desplegament:
- name: Checklist de desplegament
run: |
# Verificar que les migracions es poden executar
php artisan migrate --pretend
# Verificar que les rutes estan correctes
php artisan route:list --json > /dev/null
# Verificar que la configuració es pot cachear
php artisan config:cache
php artisan config:clear
# Verificar que les vistes es poden compilar
php artisan view:cache
php artisan view:clearEstratègia de rollback#
Sempre has de tenir un pla per revertir un desplegament que ha anat malament. La manera més senzilla amb Git és crear un tag abans de cada desplegament i, si alguna cosa falla, desplegar el tag anterior:
- name: Crear tag de backup
run: |
TIMESTAMP=$(date +%Y%m%d%H%M%S)
git tag "deploy-${TIMESTAMP}"
git push origin "deploy-${TIMESTAMP}"
- name: Desplegar
id: deploy
run: |
# Comandes de desplegament
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "cd /var/www/app && ./deploy.sh"
- name: Rollback en cas d'error
if: failure() && steps.deploy.outcome == 'failure'
run: |
echo "Desplegament fallit. Fent rollback..."
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'ROLLBACK'
cd /var/www/app
git checkout $(git describe --tags --abbrev=0 HEAD~1)
composer install --no-dev --optimize-autoloader
php artisan migrate --force
php artisan optimize
php artisan queue:restart
ROLLBACKAmb Docker, el rollback és encara més senzill: simplement desplega la imatge anterior que ja existeix al registre de contenidors:
# Rollback a la versió anterior
docker compose pull --policy always
docker tag ghcr.io/el-teu-repo/app:sha-anterior ghcr.io/el-teu-repo/app:latest
docker compose up -dUn pipeline de CI/CD és un sistema viu que evoluciona amb el projecte. Comença amb quelcom senzill (tests + desplegament) i afegeix verificacions a mesura que el projecte creixi. No cal implementar tot el que s'ha descrit en aquest capítol des del primer dia. L'important és que cada canvi de codi passi per almenys alguna verificació automàtica abans d'arribar a producció.