diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..cd039dd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +# Arquivos que NÃO devem ser copiados para dentro da imagem Docker. +# Funciona como um .gitignore, mas para o "docker build". + +node_modules +npm-debug.log + +# Arquivos de teste (não precisamos em produção) +test/ + +# Arquivos de configuração do pipeline +Jenkinsfile +docker-compose*.yml + +# Git +.git +.gitignore + +# Docs +README.md +*.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e64acd6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +# ========================================================== +# Dockerfile — "Receita" para criar a imagem Docker da app +# ========================================================== +# Analogia: assim como você cria uma VM a partir de um template, +# este arquivo define o template (imagem) do container. +# +# O Jenkins executa "docker build" usando este arquivo para +# criar a imagem que será implantada no staging. +# ========================================================== + +# 1. Imagem base: Node.js 20 Alpine (versão enxuta do Linux) +# Alpine é uma distro minimalista (~5 MB), ideal para containers +FROM node:20-alpine + +# 2. Diretório de trabalho dentro do container +WORKDIR /app + +# 3. Copia APENAS os arquivos de dependências primeiro +# Isso aproveita o cache do Docker: se package.json não mudar, +# o "npm ci" não roda de novo (build mais rápido) +COPY package*.json ./ + +# 4. Instala dependências de produção +# "npm ci" é mais rápido e determinístico que "npm install" +# "--omit=dev" ignora dependências de teste (jest, supertest) +RUN npm ci --omit=dev + +# 5. Copia o código-fonte da aplicação +COPY src/ ./src/ + +# 6. Porta que a aplicação escuta +EXPOSE 3000 + +# 7. Roda como usuário não-root (segurança) +# A imagem node:alpine já inclui o usuário "node" +USER node + +# 8. Comando que inicia a aplicação quando o container sobe +CMD ["node", "src/index.js"] diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..255f664 --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,127 @@ +// ============================================================ +// JENKINSFILE — Definição do Pipeline CI/CD +// ============================================================ +// Este arquivo é o coração do CI/CD. Ele descreve, em código, +// TODAS as etapas que o Jenkins deve executar quando você faz +// um "git push". +// +// Conceito: "Pipeline as Code" — o pipeline é versionado junto +// com o código da aplicação. Se alguém perguntar "como o deploy +// funciona?", a resposta está aqui. +// +// SINTAXE: Groovy (linguagem do Jenkins). Não precisa saber +// Groovy — a estrutura é declarativa e autoexplicativa. +// ============================================================ + +pipeline { + + // "agent any" = executa em qualquer worker disponível. + // No nosso lab, só temos um (o próprio SRVJENKINS). + agent any + + // Variáveis de ambiente disponíveis em todos os stages + environment { + REGISTRY = '10.15.1.211:5000' // Docker Registry (roda no próprio SRVJENKINS) + APP_NAME = 'cicd-demo-app' // Nome da imagem Docker + STAGING_IP = '10.15.1.212' // IP do SRVSTAGING + } + + stages { + + // ================================================== + // STAGE 1: Instalar Dependências + // ================================================== + // O Jenkins já clonou o repositório automaticamente + // (configurado no Job). Agora instalamos as dependências + // Node.js listadas no package.json. + stage('Install') { + steps { + echo '📦 Instalando dependências (npm install)...' + sh 'npm install' + } + } + + // ================================================== + // STAGE 2: Executar Testes + // ================================================== + // Roda os testes automatizados (Jest). + // Se QUALQUER teste falhar, o pipeline PARA AQUI. + // Código quebrado NUNCA chega ao servidor de staging. + stage('Test') { + steps { + echo '🧪 Executando testes automatizados...' + sh 'npm test' + } + } + + // ================================================== + // STAGE 3: Construir Imagem Docker + // ================================================== + // Cria a imagem Docker da aplicação usando o Dockerfile. + // Gera duas tags: + // - BUILD_NUMBER: versão específica (ex: :14) + // - latest: sempre aponta para a versão mais recente + stage('Build Image') { + steps { + echo '🐳 Construindo imagem Docker...' + sh "docker build -t ${REGISTRY}/${APP_NAME}:${BUILD_NUMBER} ." + sh "docker tag ${REGISTRY}/${APP_NAME}:${BUILD_NUMBER} ${REGISTRY}/${APP_NAME}:latest" + } + } + + // ================================================== + // STAGE 4: Enviar Imagem para o Registry + // ================================================== + // "Push" da imagem para o Docker Registry local. + // O Registry é o "repositório de templates" — o staging + // vai buscar a imagem de lá. + stage('Push to Registry') { + steps { + echo '📤 Enviando imagem para o Registry local...' + sh "docker push ${REGISTRY}/${APP_NAME}:${BUILD_NUMBER}" + sh "docker push ${REGISTRY}/${APP_NAME}:latest" + } + } + + // ================================================== + // STAGE 5: Deploy no Staging + // ================================================== + // Conecta via SSH no SRVSTAGING e: + // 1. Puxa a imagem mais recente do Registry + // 2. Para e remove o container antigo (se existir) + // 3. Sobe um novo container com a versão nova + // + // PREREQUISITO: A credencial 'staging-ssh-key' precisa + // estar cadastrada no Jenkins (Manage Jenkins → Credentials) + stage('Deploy to Staging') { + steps { + echo '🚀 Fazendo deploy no SRVSTAGING...' + sshagent(credentials: ['staging-ssh-key']) { + sh """ + ssh -o StrictHostKeyChecking=no root@${STAGING_IP} ' + docker pull ${REGISTRY}/${APP_NAME}:latest && + (docker stop ${APP_NAME} || true) && + (docker rm ${APP_NAME} || true) && + docker run -d \ + --name ${APP_NAME} \ + --restart unless-stopped \ + -p 3001:3000 \ + ${REGISTRY}/${APP_NAME}:latest + ' + """ + } + } + } + } + + // Ações executadas APÓS o pipeline terminar + post { + success { + echo '✅ Pipeline executado com sucesso!' + echo '🌐 App disponível em: http://10.15.1.212:3001' + } + failure { + echo '❌ Pipeline falhou. Verifique os logs acima.' + } + } +} diff --git a/meri.cdr b/meri.cdr new file mode 100644 index 0000000..209abee Binary files /dev/null and b/meri.cdr differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..43d9370 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "cicd-demo-app", + "version": "1.0.0", + "description": "Aplicação de demonstração para pipeline CI/CD", + "main": "src/index.js", + "scripts": { + "start": "node src/index.js", + "test": "jest --verbose --forceExit" + }, + "dependencies": { + "express": "^4.21.0" + }, + "devDependencies": { + "jest": "^29.7.0", + "supertest": "^7.0.0" + } +} diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..0f3d341 --- /dev/null +++ b/src/index.js @@ -0,0 +1,47 @@ +const express = require('express'); +const app = express(); +const PORT = process.env.PORT || 3000; + +// ============================================================ +// Aplicação de demonstração CI/CD +// Cada vez que você alterar este arquivo e fizer "git push", +// o Jenkins vai automaticamente testar, construir e implantar. +// ============================================================ + +// Rota principal — retorna informações da aplicação +app.get('/', (req, res) => { + res.json({ + app: 'CI/CD Demo', + versao: '1.0.0', + status: 'rodando', + ambiente: process.env.NODE_ENV || 'development', + timestamp: new Date().toISOString(), + mensagem: 'Pipeline CI/CD funcionando! 🚀' + }); +}); + +// Health check — usado para verificar se a aplicação está saudável +// Em produção, load balancers e orquestradores usam este endpoint +app.get('/health', (req, res) => { + res.status(200).json({ status: 'healthy' }); +}); + +// Rota de informações do sistema +app.get('/info', (req, res) => { + res.json({ + node_version: process.version, + platform: process.platform, + uptime_seconds: Math.floor(process.uptime()), + memory_mb: Math.round(process.memoryUsage().rss / 1024 / 1024) + }); +}); + +// Inicia o servidor apenas se NÃO estiver em modo de teste. +// Nos testes, o supertest cria seu próprio servidor temporário. +if (process.env.NODE_ENV !== 'test') { + app.listen(PORT, '0.0.0.0', () => { + console.log(`✅ Servidor rodando em http://0.0.0.0:${PORT}`); + }); +} + +module.exports = app; diff --git a/test/app.test.js b/test/app.test.js new file mode 100644 index 0000000..d01a4cf --- /dev/null +++ b/test/app.test.js @@ -0,0 +1,60 @@ +// ============================================================ +// Testes automatizados da aplicação +// O Jenkins executa estes testes a cada push. +// Se QUALQUER teste falhar, o pipeline para e NÃO faz deploy. +// Isso é o "firewall de qualidade" do CI/CD. +// ============================================================ + +const request = require('supertest'); +const app = require('../src/index'); + +describe('Rota Principal GET /', () => { + + test('deve retornar status 200', async () => { + const res = await request(app).get('/'); + expect(res.statusCode).toBe(200); + }); + + test('deve retornar o nome da aplicação', async () => { + const res = await request(app).get('/'); + expect(res.body).toHaveProperty('app', 'CI/CD Demo'); + }); + + test('deve retornar status "rodando"', async () => { + const res = await request(app).get('/'); + expect(res.body).toHaveProperty('status', 'rodando'); + }); + + test('deve conter um timestamp', async () => { + const res = await request(app).get('/'); + expect(res.body).toHaveProperty('timestamp'); + }); +}); + +describe('Health Check GET /health', () => { + + test('deve retornar status 200', async () => { + const res = await request(app).get('/health'); + expect(res.statusCode).toBe(200); + }); + + test('deve retornar status "healthy"', async () => { + const res = await request(app).get('/health'); + expect(res.body).toHaveProperty('status', 'healthy'); + }); +}); + +describe('Informações do Sistema GET /info', () => { + + test('deve retornar versão do Node.js', async () => { + const res = await request(app).get('/info'); + expect(res.statusCode).toBe(200); + expect(res.body).toHaveProperty('node_version'); + }); + + test('deve retornar uptime em segundos', async () => { + const res = await request(app).get('/info'); + expect(res.body).toHaveProperty('uptime_seconds'); + expect(typeof res.body.uptime_seconds).toBe('number'); + }); +});