GitHub Actions CI/CD para proyectos Monorepo
Una canalización de CI/CD para un monorepo enfrenta un desafío fundamentalmente diferente al de una canalización de una sola aplicación: ¿cómo se ejecutan canalizaciones rápidas y confiables cuando solo cambió 1 de sus 5 aplicaciones? Construir ingenuamente todo en cada confirmación convierte una canalización de 2 minutos en un cuello de botella de 15 minutos que los desarrolladores solucionan agrupando confirmaciones por lotes, exactamente lo que estás tratando de evitar.
Esta guía cubre una configuración de producción de GitHub Actions para un monorepo de Turborepo: compilaciones solo afectadas, ejecución de trabajos paralelos, integración de almacenamiento en caché remoto de Turbo, puertas de implementación basadas en el entorno y patrones de seguridad que evitan que los secretos se filtren en registros o ejecuciones de relaciones públicas bifurcadas.
Conclusiones clave
- Utilice
fetch-depth: 2al finalizar la compra para habilitar la detección de paquetes afectados de Turbo- Ejecute lint, verifique tipos, pruebe y cree trabajos en paralelo; no los encadene secuencialmente
- Los trabajos Matrix le permiten realizar pruebas en múltiples versiones o sistemas operativos de Node.js sin duplicar YAML.
- Almacene las credenciales de caché remota de Turbo como GitHub Secrets, no como variables de entorno en YAML
- Utilice reglas de protección
environment:en trabajos de implementación de producción para requerir aprobación manual- La acción
paths-filterle permite omitir CI por completo cuando solo se cambian documentos o archivos sin código- Nunca imprima secretos en registros; use
::add-mask::para secretos generados en tiempo de ejecución- Los flujos de trabajo reutilizables (
workflow_call) eliminan la duplicación en múltiples flujos de trabajo del repositorio.
Estructura del repositorio para CI
Una buena configuración de CI refleja su flujo de trabajo de desarrollo. Aquí está la estructura que funciona para un monorepo Turborepo:
.github/
workflows/
ci.yml — Runs on every PR and push to main
deploy.yml — Runs on push to main (after CI passes)
security.yml — Weekly security scans
cleanup.yml — Scheduled: delete stale preview deployments
actions/
setup-pnpm/
action.yml — Reusable: install Node + pnpm + cache
Flujo de trabajo de CI central
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
# Cancel in-progress runs when a new run starts
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
NODE_VERSION: '22'
PNPM_VERSION: '10.28.0'
jobs:
# Fast path: skip if only docs/assets changed
changes:
runs-on: ubuntu-latest
outputs:
code: ${{ steps.filter.outputs.code }}
docs: ${{ steps.filter.outputs.docs }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
code:
- 'apps/**/*.ts'
- 'apps/**/*.tsx'
- 'packages/**/*.ts'
- 'turbo.json'
- 'package.json'
- 'pnpm-lock.yaml'
docs:
- 'apps/docs/**'
- '**/*.md'
lint:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # Required for turbo affected detection
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint
run: pnpm turbo run lint
- name: Type check
run: pnpm turbo run type-check
test:
needs: changes
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
services:
postgres:
image: postgres:17-alpine
env:
POSTGRES_DB: ecosire_test
POSTGRES_USER: ecosire
POSTGRES_PASSWORD: password
ports:
- 5433:5432
options: >-
--health-cmd pg_isready
--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 5
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run unit tests
run: pnpm turbo run test
env:
DATABASE_URL: postgresql://ecosire:password@localhost:5433/ecosire_test
REDIS_URL: redis://localhost:6379
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./apps/api/coverage/lcov.info
build:
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build all apps
run: pnpm turbo run build
env:
NEXT_PUBLIC_API_URL: ${{ vars.STAGING_API_URL }}
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: build-${{ github.sha }}
path: |
apps/web/.next/
apps/api/dist/
retention-days: 1 # Keep briefly for deploy job
Trabajo de prueba E2E con dramaturgo
e2e:
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
project: [chromium, firefox, mobile-chrome]
fail-fast: false # Don't cancel other browsers if one fails
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Install Playwright browsers
run: cd apps/web && npx playwright install ${{ matrix.project }} --with-deps
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: build-${{ github.sha }}
- name: Start test servers
run: |
cd apps/api && node dist/main.js &
cd apps/web && npx next start &
npx wait-on http://localhost:3000 http://localhost:3001/health
env:
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
- name: Run E2E tests
run: cd apps/web && npx playwright test --project=${{ matrix.project }}
env:
BASE_URL: http://localhost:3000
- name: Upload test results
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report-${{ matrix.project }}
path: apps/web/playwright-report/
retention-days: 7
Flujo de trabajo de escaneo de seguridad
# .github/workflows/security.yml
name: Security Scan
on:
schedule:
- cron: '0 2 * * 1' # Weekly Monday 2am UTC
push:
paths:
- '**/package.json'
- 'pnpm-lock.yaml'
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: '10.28.0'
- name: Run pnpm audit
run: pnpm audit --audit-level moderate
continue-on-error: true # Don't fail CI, just report
- name: Run Snyk security scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
command: test
args: --severity-threshold=high
secret-scanning:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history for secret scanning
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Flujo de trabajo de implementación
# .github/workflows/deploy.yml
name: Deploy to Production
on:
workflow_dispatch:
inputs:
environment:
description: 'Deploy environment'
required: true
default: 'production'
type: choice
options:
- production
- staging
# Only one production deploy at a time
concurrency:
group: deploy-${{ inputs.environment }}
cancel-in-progress: false # Don't cancel in-flight deploys
jobs:
quality-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: '10.28.0'
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
- run: pnpm install --frozen-lockfile
- name: Run quality checks
run: |
pnpm turbo run lint type-check test
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
deploy:
needs: quality-gate
runs-on: ubuntu-latest
environment: ${{ inputs.environment }} # Requires approval for production
steps:
- uses: actions/checkout@v4
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
set -e
cd /opt/ecosire/app
echo "Pulling latest changes..."
git pull origin main
echo "Installing dependencies..."
pnpm install --frozen-lockfile
echo "Building..."
TURBO_TOKEN=${{ secrets.TURBO_TOKEN }} \
TURBO_TEAM=${{ vars.TURBO_TEAM }} \
npx turbo run build
echo "Running migrations..."
pnpm --filter @ecosire/db db:migrate
echo "Restarting services..."
pm2 restart ecosystem.config.cjs --update-env
echo "Health check..."
sleep 10
curl -f https://ecosire.com/api/health || exit 1
curl -f https://api.ecosire.com/api/health || exit 1
echo "Deploy complete!"
- name: Notify on success
if: success()
uses: actions/github-script@v7
with:
script: |
github.rest.repos.createCommitComment({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: context.sha,
body: `Deployed to ${process.env.ENVIRONMENT} at ${new Date().toISOString()}`
})
env:
ENVIRONMENT: ${{ inputs.environment }}
- name: Notify on failure
if: failure()
uses: actions/github-script@v7
with:
script: |
github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: `Deploy failed: ${process.env.ENVIRONMENT}`,
body: `Deploy to ${process.env.ENVIRONMENT} failed at ${new Date().toISOString()}\n\nRun: ${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`
})
env:
ENVIRONMENT: ${{ inputs.environment }}
Estrategia de almacenamiento en caché
El almacenamiento en caché efectivo es la diferencia entre ejecuciones de CI de 2 y 8 minutos:
# Reusable setup step (extract to composite action)
# .github/actions/setup-pnpm/action.yml
name: Setup pnpm
runs:
using: composite
steps:
- uses: pnpm/action-setup@v4
with:
version: '10.28.0'
- uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'pnpm'
# Turbo cache (separate from pnpm cache)
- uses: actions/cache@v4
with:
path: .turbo
key: turbo-${{ runner.os }}-${{ github.sha }}
restore-keys: |
turbo-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
shell: bash
Mejores prácticas de seguridad
Nunca hagas eco de secretos:
# Wrong — secret appears in logs
- run: echo "API key is ${{ secrets.API_KEY }}"
# If you must generate and use runtime secrets
- run: |
echo "::add-mask::$GENERATED_SECRET"
GENERATED_SECRET=$(generate-secret)
echo "GENERATED_SECRET=$GENERATED_SECRET" >> $GITHUB_ENV
Restringir permisos:
# Minimum permissions at workflow level
permissions:
contents: read
actions: read
checks: write # Add only what you need
jobs:
test:
permissions:
contents: read # Override at job level if needed
Evitar la inyección de solicitud de extracción:
# For PRs from forks — never expose secrets
on:
pull_request:
jobs:
test:
# Secrets not available in fork PRs by default
# Use pull_request_target only for trusted actions
runs-on: ubuntu-latest
Preguntas frecuentes
¿Cómo activo un flujo de trabajo solo cuando cambian paquetes específicos?
Utilice dorny/paths-filter para detectar qué paquetes cambiaron y luego pase las salidas del filtro a las condiciones del trabajo. Para la detección de afectados incorporada de Turbo, asegúrese de fetch-depth: 2 en su paso de pago para que Turbo pueda compararse con la confirmación anterior. Utilice pnpm turbo run test --filter="...[HEAD^1]" para ejecutar pruebas solo para paquetes modificados y sus dependientes.
¿Cómo debo manejar las migraciones de bases de datos en CI?
Ejecute las migraciones en un trabajo independiente que se ejecute después de las pruebas pero antes de que se cree el artefacto de compilación final. Utilice una URL de base de datos de prueba dedicada en CI (separada de desarrollo y producción). Nunca ejecute migraciones contra producción desde CI automáticamente: use workflow_dispatch con aprobación manual a través de las reglas de protección del entorno de GitHub para cambios en la base de datos de producción.
¿Cuál es la mejor manera de administrar secretos en múltiples entornos?
Utilice entornos de GitHub (Configuración > Entornos) con reglas de protección. Cree entornos separados para staging y production. Almacene secretos específicos del entorno en cada entorno, no a nivel de repositorio. Para secretos compartidos (como el token de caché remoto Turbo), utilice secretos a nivel de repositorio. La clave environment: en la configuración del trabajo habilita secretos y reglas de protección específicos del entorno.
¿Cómo acelero la instalación de pnpm en CI?
El actions/setup-node@v4 con cache: 'pnpm' almacena en caché el almacén pnpm. Para monorepos grandes, use también pnpm install --prefer-offline después de restaurar el caché. La combinación del almacenamiento en caché del almacén pnpm y el almacenamiento en caché remoto Turbo generalmente reduce el tiempo de instalación y compilación entre un 60 % y un 80 % en los accesos al caché.
¿Cómo ejecuto diferentes flujos de trabajo para relaciones públicas y para envíos de rama principal?
Utilice las condiciones de activación on: on.push.branches para principal y on.pull_request.branches para PR. Puede ejecutar un subconjunto de pruebas más rápido para PR (lint, pruebas unitarias) y el conjunto completo en main. Utilice condiciones github.event_name == 'push' para ejecutar trabajos de implementación solo al enviar a principal, no en eventos de relaciones públicas.
Próximos pasos
Una canalización de CI/CD bien diseñada es la base de un equipo de ingeniería de alta velocidad: le brinda confianza para realizar envíos frecuentes y detectar regresiones automáticamente. ECOSIRE ejecuta GitHub Actions CI con lint, verificación de tipos, 1301 pruebas unitarias, Playwright E2E e implementaciones SSH sin tiempo de inactividad en cada envío a main.
Ya sea que necesite consultoría de arquitectura CI/CD, configuración de Turborepo monorepo o soporte completo de DevOps, explore nuestros servicios de ingeniería.
Escrito por
ECOSIRE Research and Development Team
Construyendo productos digitales de nivel empresarial en ECOSIRE. Compartiendo perspectivas sobre integraciones Odoo, automatización de eCommerce y soluciones empresariales impulsadas por IA.
Artículos relacionados
AI-Powered Accounting Automation: What Works in 2026
Discover which AI accounting automation tools deliver real ROI in 2026, from bank reconciliation to predictive cash flow, with implementation strategies.
Payroll Processing: Setup, Compliance, and Automation
Complete payroll processing guide covering employee classification, federal and state withholding, payroll taxes, garnishments, automation platforms, and year-end W-2 compliance.
AI Agents for Business Automation: The 2026 Landscape
Explore how AI agents are transforming business automation in 2026, from multi-agent orchestration to practical deployment strategies for enterprise teams.