Flujos CI/CD Completos
Ejemplos end-to-end de pipelines reales para los stacks mΓ‘s comunes. Copia, adapta y ΓΊsalos en tus proyectos.
Flujo 1: Next.js + Supabase β Vercelβ
El stack mΓ‘s comΓΊn para proyectos web modernos.
Estructura del proyecto:
mi-app/
βββ .github/workflows/
β βββ ci.yml
β βββ deploy.yml
βββ src/
βββ tests/
βββ package.json
βββ .env.example
Paso 1: Configurar secrets en GitHubβ
Repo β Settings β Secrets and variables β Actions
Agregar:
- VERCEL_TOKEN
- VERCEL_ORG_ID
- VERCEL_PROJECT_ID
- SUPABASE_URL (para tests de integraciΓ³n)
- SUPABASE_ANON_KEY (para tests de integraciΓ³n)
Paso 2: Workflow de CI (en cada PR)β
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
push:
branches: [develop]
jobs:
quality:
name: Calidad de cΓ³digo
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Instalar dependencias
run: npm ci
- name: Lint
run: npm run lint
- name: Verificar tipos TypeScript
run: npx tsc --noEmit
- name: Tests unitarios
run: npm run test:unit -- --coverage
- name: Tests de integraciΓ³n
run: npm run test:integration
env:
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
- name: Build de verificaciΓ³n
run: npm run build
env:
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
preview-deploy:
name: Deploy Preview
runs-on: ubuntu-latest
needs: quality
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm install -g vercel@latest
- run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }}
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
- run: vercel build --token=${{ secrets.VERCEL_TOKEN }}
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
- name: Deploy a preview
id: deploy
run: |
url=$(vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }})
echo "preview_url=$url" >> $GITHUB_OUTPUT
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
- name: Comentar URL en el PR
uses: actions/github-script@v7
with:
script: |
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(c =>
c.user.type === 'Bot' && c.body.includes('Preview desplegado')
);
const body = `## π Preview desplegado\n\n**URL:** ${{ steps.deploy.outputs.preview_url }}\n\n**Commit:** \`${{ github.sha }}\``;
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body,
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body,
});
}
Paso 3: Workflow de Deploy (merge a main)β
# .github/workflows/deploy.yml
name: Deploy a ProducciΓ³n
on:
push:
branches: [main]
jobs:
deploy-production:
name: Deploy β Vercel ProducciΓ³n
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm install -g vercel@latest
- run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }}
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
- run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }}
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
- name: Deploy a producciΓ³n
run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }}
env:
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
Resultado visual en GitHubβ
Pull Request #42: "feat: agregar perfil de usuario"
β
CI / Calidad de cΓ³digo (1m 45s)
β
CI / Deploy Preview (2m 10s)
π€ GitHub Actions comentΓ³:
π Preview desplegado
URL: https://mi-app-git-feat-perfil.vercel.app
Commit: a3f9b2c
Flujo 2: API Node.js β Fly.io + Neonβ
Para APIs backend con Docker.
# .github/workflows/api-pipeline.yml
name: API Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
# βββββββββββββββββββββββββββββββββββββββββ
# JOB 1: Tests (en cada push y PR)
# βββββββββββββββββββββββββββββββββββββββββ
test:
name: Tests
runs-on: ubuntu-latest
# Base de datos temporal para los tests
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- name: Correr migraciones en DB de test
run: npx drizzle-kit migrate
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
- name: Tests
run: npm test
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
JWT_SECRET: test-secret-para-ci
NODE_ENV: test
# βββββββββββββββββββββββββββββββββββββββββ
# JOB 2: Build Docker (solo en main)
# βββββββββββββββββββββββββββββββββββββββββ
build:
name: Build Docker
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main'
permissions:
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
# βββββββββββββββββββββββββββββββββββββββββ
# JOB 3: Deploy a Fly.io (solo en main)
# βββββββββββββββββββββββββββββββββββββββββ
deploy:
name: Deploy β Fly.io
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@master
- name: Migrar base de datos de producciΓ³n
run: fly ssh console -C "node dist/migrate.js" --app mi-api
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
- name: Deploy
run: flyctl deploy --remote-only --image ghcr.io/${{ github.repository }}:${{ github.sha }}
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
Flujo 3: Monorepo (Frontend + Backend)β
Cuando tienes el frontend y el backend en el mismo repositorio.
mi-monorepo/
βββ apps/
β βββ web/ β Next.js
β βββ api/ β Node.js/Express
βββ packages/
β βββ shared/ β tipos y utils compartidos
βββ .github/workflows/
# .github/workflows/monorepo-ci.yml
name: Monorepo CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
changes:
name: Detectar cambios
runs-on: ubuntu-latest
outputs:
web: ${{ steps.filter.outputs.web }}
api: ${{ steps.filter.outputs.api }}
shared: ${{ steps.filter.outputs.shared }}
steps:
- uses: actions/checkout@v4
- uses: dorny/paths-filter@v3
id: filter
with:
filters: |
web:
- 'apps/web/**'
- 'packages/shared/**'
api:
- 'apps/api/**'
- 'packages/shared/**'
shared:
- 'packages/shared/**'
test-web:
name: Test Frontend
needs: changes
if: needs.changes.outputs.web == 'true' # β Solo si hubo cambios en web
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/web
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm test
- run: npm run build
test-api:
name: Test Backend
needs: changes
if: needs.changes.outputs.api == 'true' # β Solo si hubo cambios en api
runs-on: ubuntu-latest
defaults:
run:
working-directory: apps/api
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm test
deploy-web:
name: Deploy Frontend
needs: [test-web]
if: github.ref == 'refs/heads/main' && needs.changes.outputs.web == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "Deploy web aquΓ..."
deploy-api:
name: Deploy Backend
needs: [test-api]
if: github.ref == 'refs/heads/main' && needs.changes.outputs.api == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: echo "Deploy api aquΓ..."
ProtecciΓ³n de ramas: el complemento perfecto del CIβ
Configura que nadie (ni tΓΊ) pueda hacer push directo a main sin que el CI pase:
Repo β Settings β Branches β Add rule
Branch name pattern: main
β
Require a pull request before merging
β
Require approvals: 1 (o 0 si trabajas solo)
β
Require status checks to pass before merging
β
Require branches to be up to date before merging
Status checks:
β Buscar y agregar: "CI / Calidad de cΓ³digo"
β Buscar y agregar: "Tests"
β
Include administrators β para que aplique a todos
Con esta configuraciΓ³n:
Nadie puede hacer merge si:
β Los tests fallan
β El linting falla
β El type check falla
β La rama estΓ‘ desactualizada respecto a main
Diagrama del flujo completoβ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β FLUJO COMPLETO DE CI/CD β
β β
β 1. Developer crea rama: git checkout -b feat/nueva-funcionalidad β
β β
β 2. Desarrolla y hace commits localmente β
β β
β 3. git push origin feat/nueva-funcionalidad β
β β β
β βΌ β
β 4. GitHub Actions: CI workflow β
β ββ Lint β
β
β ββ TypeScript β
β
β ββ Tests β
β
β ββ Build β
β
β β
β 5. Abre Pull Request β Preview deploy automΓ‘tico β
β URL: https://mi-app-git-feat-nueva.vercel.app β
β β
β 6. Review + aprobaciΓ³n (si trabajan en equipo) β
β β
β 7. Merge a main β
β β β
β βΌ β
β 8. GitHub Actions: Deploy workflow β
β ββ Build producciΓ³n β
β
β ββ Migraciones DB β
β
β ββ Deploy a prod β
β
β β
β 9. β
CΓ³digo en producciΓ³n en ~3 minutos β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ