Módulo 12: GitHub Actions CI/CD
08. Mini-Proyecto: CI/CD Completo
Descripción del proyecto
En este mini-proyecto crearás un pipeline CI/CD production-ready con GitHub Actions: CI automático, matrix testing, deploy a staging/production, release automation, PR automation, y monitoring. Este setup es lo que equipos profesionales usan en producción.
Este proyecto integra todo lo aprendido en un pipeline completo.
Objetivos del proyecto
Al completar este proyecto, habrás:
✅ CI pipeline con lint, test, build
✅ Matrix testing (Node 18, 20 × Ubuntu, macOS)
✅ Deploy staging automático (develop branch)
✅ Deploy production con approval (main branch)
✅ Release automation con tags
✅ PR automation (labels, checks)
✅ Caching para performance
✅ Concurrency control
✅ Notifications en Slack/Discord
Tiempo estimado: 60-90 minutos
Estructura del proyecto
my-project/
├── .github/
│ ├── workflows/
│ │ ├── ci.yml # CI: lint, test, build
│ │ ├── deploy-staging.yml # Deploy to staging
│ │ ├── deploy-production.yml # Deploy to production
│ │ ├── release.yml # Release automation
│ │ ├── pr-automation.yml # PR labels, checks
│ │ └── stale.yml # Stale issues cleanup
│ ├── actions/
│ │ └── setup-project/
│ │ └── action.yml # Composite action
│ └── CODEOWNERS
├── src/
│ └── index.js
├── tests/
│ └── index.test.js
├── package.json
└── README.md
Fase 1: Project setup (10 min)
Create project:
mkdir cicd-project
cd cicd-project
npm init -y
git init
# Install dependencies
npm install --save-dev jest eslint prettier
# Create source file
mkdir src
cat > src/index.js << 'EOF'
function add(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('Arguments must be numbers');
}
return a + b;
}
function multiply(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new Error('Arguments must be numbers');
}
return a * b;
}
module.exports = { add, multiply };
EOF
# Create test file
mkdir tests
cat > tests/index.test.js << 'EOF'
const { add, multiply } = require('../src/index');
describe('add', () => {
test('adds two numbers', () => {
expect(add(1, 2)).toBe(3);
});
test('throws on non-numbers', () => {
expect(() => add('a', 2)).toThrow('Arguments must be numbers');
});
});
describe('multiply', () => {
test('multiplies two numbers', () => {
expect(multiply(3, 4)).toBe(12);
});
test('throws on non-numbers', () => {
expect(() => multiply(null, 2)).toThrow('Arguments must be numbers');
});
});
EOF
# Add scripts to package.json
npm set-script test "jest"
npm set-script lint "eslint src/ tests/"
npm set-script build "echo 'Build complete'"
# Create directories
mkdir -p .github/workflows
mkdir -p .github/actions/setup-project
Fase 2: Composite action (5 min)
Reusable setup action:
# .github/actions/setup-project/action.yml
name: 'Setup Project'
description: 'Checkout, setup Node, install deps'
inputs:
node-version:
description: 'Node.js version'
required: false
default: '18'
runs:
using: 'composite'
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
shell: bash
Fase 3: CI workflow (10 min)
.github/workflows/ci.yml:
name: CI
on:
push:
branches: [main, develop]
paths-ignore:
- '**/*.md'
- 'docs/**'
pull_request:
branches: [main]
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: ./.github/actions/setup-project
- name: Run ESLint
run: npm run lint
test:
name: Test (Node ${{ matrix.node }} on ${{ matrix.os }})
runs-on: ${{ matrix.os }}
needs: lint
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
node: ['18', '20']
include:
- os: macos-latest
node: '20'
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: 'npm'
- run: npm ci
- name: Run tests
run: npm test -- --coverage
- name: Upload coverage
if: matrix.os == 'ubuntu-latest' && matrix.node == '20'
uses: actions/upload-artifact@v3
with:
name: coverage
path: coverage/
build:
name: Build
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- name: Build
run: npm run build
- name: Upload build artifact
uses: actions/upload-artifact@v3
with:
name: build
path: dist/
retention-days: 7
Fase 4: Deploy workflows (15 min)
Staging deploy:
# .github/workflows/deploy-staging.yml
name: Deploy Staging
on:
push:
branches: [develop]
concurrency:
group: deploy-staging
cancel-in-progress: false
jobs:
test:
uses: ./.github/workflows/ci.yml
deploy:
name: Deploy to Staging
needs: test
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
- run: npm ci && npm run build
- name: Deploy to staging
run: |
echo "🚀 Deploying to staging..."
echo "Version: $(cat package.json | jq -r .version)"
echo "Commit: ${{ github.sha }}"
# Your actual deploy command here
env:
DEPLOY_TOKEN: ${{ secrets.STAGING_DEPLOY_TOKEN }}
- name: Verify deployment
run: |
echo "✅ Staging deployment verified"
# curl -f https://staging.example.com/health || exit 1
Production deploy:
# .github/workflows/deploy-production.yml
name: Deploy Production
on:
push:
branches: [main]
concurrency:
group: deploy-production
cancel-in-progress: false
jobs:
test:
name: Run Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm test
deploy:
name: Deploy to Production
needs: test
runs-on: ubuntu-latest
environment: production # Requires approval
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '20'
cache: 'npm'
- run: npm ci && npm run build
- name: Deploy to production
run: |
echo "🚀 Deploying to production..."
echo "Version: $(cat package.json | jq -r .version)"
# Your actual deploy command
env:
DEPLOY_TOKEN: ${{ secrets.PROD_DEPLOY_TOKEN }}
- name: Verify deployment
run: |
echo "✅ Production deployment verified"
# curl -f https://api.example.com/health || exit 1
- name: Notify success
if: success()
run: |
echo "✅ Production deploy successful"
# curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
# -d '{"text":"Production deployed successfully!"}'
- name: Notify failure
if: failure()
run: |
echo "❌ Production deploy failed"
# curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
# -d '{"text":"⚠️ Production deploy FAILED!"}'
Fase 5: Release automation (10 min)
Auto-release on tag:
# .github/workflows/release.yml
name: Release
on:
push:
tags:
- 'v*'
jobs:
release:
name: Create Release
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0 # Full history for changelog
- name: Generate changelog
id: changelog
run: |
# Get changes since last tag
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
if [ -n "$PREV_TAG" ]; then
CHANGES=$(git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" --no-merges)
else
CHANGES=$(git log --pretty=format:"- %s (%h)" --no-merges)
fi
echo "changes<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGES" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create GitHub Release
uses: actions/github-script@v6
with:
script: |
const tag = context.ref.replace('refs/tags/', '');
const changes = `${{ steps.changelog.outputs.changes }}`;
await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tag,
name: `Release ${tag}`,
body: `## Changes\n\n${changes}`,
draft: false,
prerelease: tag.includes('-')
});
Create release:
# Bump version
npm version patch # or minor, major
# Push tag
git push origin --tags
# Release workflow triggers automatically
Fase 6: PR automation (10 min)
Auto-label PRs:
# .github/workflows/pr-automation.yml
name: PR Automation
on:
pull_request:
types: [opened, synchronize]
jobs:
label:
name: Auto Label
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/github-script@v6
with:
script: |
const pr = context.payload.pull_request;
const labels = [];
// Size labels
const size = pr.additions + pr.deletions;
if (size < 50) labels.push('size/S');
else if (size < 200) labels.push('size/M');
else if (size < 500) labels.push('size/L');
else labels.push('size/XL');
// Type from title
const title = pr.title.toLowerCase();
if (title.startsWith('feat')) labels.push('feature');
else if (title.startsWith('fix')) labels.push('bugfix');
else if (title.startsWith('docs')) labels.push('documentation');
else if (title.startsWith('refactor')) labels.push('refactor');
if (labels.length > 0) {
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pr.number,
labels
});
}
Fase 7: Stale cleanup (5 min)
Auto-close stale issues:
# .github/workflows/stale.yml
name: Stale Issues
on:
schedule:
- cron: '0 0 * * 1' # Weekly on Monday
jobs:
stale:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v8
with:
stale-issue-message: |
This issue has been inactive for 30 days.
It will be closed in 7 days unless there's new activity.
close-issue-message: 'Closed due to inactivity.'
stale-pr-message: 'This PR has been inactive for 14 days.'
days-before-stale: 30
days-before-close: 7
stale-issue-label: 'stale'
exempt-issue-labels: 'pinned,priority/high'
Verificación final
Test checklist:
# 1. CI runs on PR
git checkout -b feature/test
echo "// test" >> src/index.js
git add . && git commit -m "feat: Test CI"
gh pr create --title "feat: Test CI" --body "Testing CI pipeline"
# Check: GitHub → Actions → CI should run
# 2. Matrix tests
# Check: CI runs on Node 18, 20, Ubuntu + macOS
# 3. Deploy staging
git checkout develop
git merge feature/test
git push
# Check: Deploy Staging workflow runs
# 4. Deploy production
git checkout main
git merge develop
git push
# Check: Deploy Production workflow (with approval)
# 5. Release
npm version patch
git push --tags
# Check: Release workflow creates GitHub Release
# 6. PR labels
# Check: PR has size/S and feature labels
# 7. Caching works
# Check: Second run of CI is faster
Workflow visualization
┌──────────┐
│ Push/PR │
└─────┬────┘
│
┌─────▼────┐
│ Lint │
└─────┬────┘
│
┌───────────┼───────────┐
│ │ │
┌─────▼──┐ ┌────▼───┐ ┌───▼─────┐
│Node 18 │ │Node 20 │ │Node 20 │
│Ubuntu │ │Ubuntu │ │macOS │
└────┬───┘ └────┬───┘ └────┬────┘
└───────────┼───────────┘
│
┌─────▼────┐
│ Build │
└─────┬────┘
│
┌──────────┴──────────┐
│ │
(develop branch) (main branch)
│ │
┌─────▼─────┐ ┌──────▼──────┐
│ Staging │ │ Production │
│ Deploy │ │ (approval) │
└────────────┘ └─────────────┘
Criterios de éxito
- CI runs on every PR (lint → test → build)
- Matrix testing works (3 combinations)
- Caching reduces install time
- Concurrency cancels duplicate runs
- Deploy staging on develop push
- Deploy production on main push (with approval)
- Release automation on tag push
- PR auto-labeling works
- Stale cleanup scheduled
- Composite action reusable
Resumen del proyecto
Completaste:
✅ 6 workflow files production-ready
✅ 1 composite action reusable
✅ CI pipeline (lint → matrix test → build)
✅ Staging deploy automático
✅ Production deploy con approval
✅ Release automation con changelog
✅ PR automation (labels, checks)
✅ Stale cleanup scheduled
✅ Performance optimized (cache, concurrency)
Resumen del Módulo 12
Completaste el módulo completo de GitHub Actions CI/CD:
✅ GitHub Actions fundamentals (YAML, triggers, jobs)
✅ Matrix testing (OS × versions)
✅ Deploy automation (staging + production)
✅ Secrets management (repo, env, org)
✅ Advanced workflows (reusable, composite, dispatch)
✅ Performance optimization (caching, artifacts, concurrency)
✅ Complete CI/CD pipeline production-ready
Siguiente módulo: Módulo 13: Git Internals (Opcional)
Recursos adicionales
- GitHub Actions Docs - Complete reference
- Actions Marketplace - Community actions
- GitHub Actions Examples - Starter templates
Tiempo total del proyecto: 60-90 minutos
¡Felicitaciones! Has completado el Módulo 12 - GitHub Actions CI/CD.