Hola mundo en Node.js
A continuación se muestra un ejemplo de integración y despliegue continuos en Jenkins de un proyecto NodeJs. Los pasos a realizar son similares al ejemplo anterior con Java, el decir, el pipeline tendrá las mismas fases; eso si, adaptaremos las ordenes o comandos a ejecutar a la tecnología Node.js.
Al igual con el ejemplo anterior en Java, en primer lugar trabajaremos con la aplicación Node.js sin dockerizar, y después dockerizaremos la aplicación. La mayoría de los pasos siempre los ejecutaremos primero en local, y tras comprobar que funcionan correctamente, los automatizaremos en Jenkins.
Construcción y ejecución en local
Nos vamos a basar en el proyecto HelloWorld en NodeJs, disponible en https://github.com/ualcnsa/nodeapp. Necesitarás poder hacer cambios sobre el mismo, así que crea un fork y trabaja con tu fork a partir de ahora.
Tras clonar tu fork a local, haz checkout del tag v0.1 en una nueva rama cuyo nombre sea tu usuario de la UAL, para que tus archivos estén en el estado inicial de este tutorial:
git checkout tags/v0.1 -b <branch> (1)
| 1 | Usa tu nombre de usuario de la UAL como nombre de la rama. |
Veamos los archivos que componen la aplicación:
-
El archivo
package.jsoncontienen información básica de la aplicación y las dependencias:-
express: Node framework
-
jest: framework de testing para NodeJs (existen numerosos framework de testing en NodeJs, como Jasmine, Mocha, Tape, etc.)
-
supertest: proporciona abstracción a alto nivel para testing HTTP
-
{
"name": "nodeapp",
"version": "1.0.0",
"description": "",
"main": "src/main.js",
"scripts": {
"start": "node src/main",
"test": "jest"
},
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.17.3"
},
"devDependencies": {
"jest": "^27.5.1",
"supertest": "^6.2.2"
}
}
|
Comprueba que los archivos
Fig. 1. Archivos y carpetas en el estado inicial
|
Para instalar las dependencias ejecuta npm install.
-
El archivo principal del proyecto
src/main.jsse encarga de arrancar la aplicación en el puerto 3000.
const app = require("./app");
const port = 3000
app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})
-
El archivo
src/app.jses un sencillo hola mundo con dos rutas:-
/devuelve"Hello World!" -
/:nameToSalutedevuelve"Hello " + nameToSalute + "!"mediante el servicioHelloWordService
-
const express = require('express')
const HelloWordService = require( "./services/hello-world" );
const app = express()
app.get('/', (req, res) => {
res.send('Hello World!')
})
app.get('/:nameToSalute', (req, res) => {
res.send(new HelloWordService().greet(req.params.nameToSalute));
})
module.exports = app
-
El archivo
src/services/hello-world.jses un servicio de hola mundo.
class HelloWordService {
/**
* @description Create an instance of HelloWordService
*/
constructor () {
}
/**
* @description Says Hello to a given name
* @param nameToHello {string} Name to greet
* greet name
* @returns a string that starts with Hello
*/
greet ( nameToHello ) {
return "Hello " + nameToHello+"!";
}
}
module.exports = HelloWordService;
Para ejecutar la aplicación, ejecuta: npm start
Puedes ver la aplicación en el navegador accediendo a http://localhost:3000 o a http://localhost:3000/nombre
Test unitarios y end2end
En primer lugar tenemos un test unitario para probar el servicio HelloWorldService que comprueba que la salida sea la esperada.
Se guardará en la carpeta src/services/ con el nombre hello-world.test.js.
const HelloWordService = require("./hello-world");
describe("HelloWordService Test", () => {
const helloWordService = new HelloWordService();
it("says 'Hello John!' to greet John", () => {
expect(helloWordService.greet("John")).toBe("Hello John!");
});
});
En segundo lugar tenemos varios test end2end. El primer test va a navegar a la raiz de la aplicación (/) y verificar que la página responde con el texto esperado Hello World!. El segundo test navega a /John y comprueba que la página responde con Hello John!.
const request = require("supertest");
const app = require("./app");
describe("GET /", () => {
//navigate to root and check the the response is "Hello World!"
it('responds with "Hello World!"', (done) => {
request(app).get('/').expect('Hello World!', done);
});
});
describe("GET /John", () => {
//navigate to /John and check the the response is "Hello John!"
it('responds with "Hello John!"', (done) => {
request(app).get('/John').expect('Hello John!', done);
});
});
Para ejecutar los tests: npm test
Si todo funciona correctamenente, haz commit y push de tu rama.
Creación del pipeline en Jenkins
Definimos un nuevo proyecto tipo Pipeline. Añadimos la descripción del pipeline:
pipeline {
agent any
tools {
// In Global tools configuration, install Node configured as "nodejs"
nodejs "nodejs"
}
stages {
stage('Cloning Git') {
steps {
git branch: 'MI_RAMA', url: 'https://github.com/MI_USUARIO/nodeapp' (1)
}
}
stage('Install dependencies') {
steps {
sh 'npm install'
}
}
stage('Test') {
steps {
sh 'npm test'
}
}
}
}
| 1 | Cambia el nombre de la rama y la URL del repositorio por las tuyas. |
El resultado sera:
La evolución de las métricas del proyecto es uno de los indicadores que habitualmente muestra Jenkins como feedback para los desarrolladores. Vamos a publicar los resultados de los test en un gráfico.
-
Editamos
package.jsony añadimos el scripttest-jenkinspara generar los resultados de los test en formato xml que usará Jenkins para generar el gráfico, y la dependencia necesaria para ello:
...
"scripts": {
"start": "node src/main",
"test": "jest",
"test-jenkins": "jest --reporters=default --reporters=jest-junit", (1)
},
"jest-junit": { (2)
"outputDirectory": "./coverage/",
"outputName": "test.results.xml",
"usePathForSuiteName": "true"
},
...
"devDependencies": {
"jest": "^27.5.1",
"jest-junit": "^13.0.0", (3)
"supertest": "^6.2.2"
}
| 1 | Añadimos el script test-jenkins que define los formatos de salida de los test: el normal y usando el plugin jest-junit para formato xml. |
| 2 | Configuración para jest-junit que genera los resultados de los test en el archivo ./coverage/test.results.xml |
| 3 | Dependencia a jest-junit que permite generar los resultados de los test en xml. |
Podemos probar en local, llamamos a la ejecución de los test y generación del xml: npm run test-jenkins.
|
Añade al |
Guarda los cambios en el repositorio, para que estén actualizados cuando los lea Jenkins.
-
Actualizamos el pipeline, la fase
Test:
stage('Test') {
steps {
sh 'npm run test-jenkins'
}
post {
success {
junit '**/test*.xml'
}
}
}
Guardamos los cambios. Tras un par de ejecuciones del build, se visualiza el gráfico Test Result Trend:
Webhook para construcción automática
Configura en GitHub un nuevo Webhook para que tras cada cambio de código en el repositorio, Jenkins sea notificado y lance automáticamente la construcción del pipeline:
-
En GitHub, seleccionamos el repositorio sobre el que queremos activar la construcción en Jenkins y hacemos clic en: Settings > WebHooks > Add webhook
-
En Payload URL:
http://{YOUR_JENKINS_URL}/github-webhook/
-
Finalmente, en la configuración del proyecto en Jenkins, en la sección Build Trigers, marca la opción GitHub hook tirigger from GITScm polling
A partir de ahora, cuando el repositorio en GitHub reciba un push notificará a Jenkins para que lance la construcción automáticamente.
Informe de cobertura
Como ya sabemos, la cobertura de código nos va a ofrecer un valor directamente relacionado con la calidad de los juegos de prueba. Para obtener la cobertura y publicarla en Jenkins, debemos hacer:
-
Añadir a
package.jsonun script para cobertura que permite obtener la cobertura con Jest. -
Modificar la fase Test de Jenkins para que llame al script de cobertura y publique, en el bloque
post, el informe de cobertura generado.
1.Modifica package.json, añadiendo el nuevo script y la dependencia:
...
"scripts": {
...
"coverage-jenkins": "jest --reporters=default --reporters=jest-junit --coverage --coverageReporters=text --coverageReporters=html --coverageDirectory=./coverage/"
},
...
Podemos probar en local, llamamos a la ejecución del script: npm run coverage-jenkins.
Como resultado, en la carpeta coverage del proyecto se ha generado el informe de cobertura.
-
Modifica el pipeline de Jenkins, la fase
Test:
stage('Test') {
steps {
sh 'npm run coverage-jenkins' (1)
}
post {
success {
junit '**/test*.xml'
publishHTML target: [ (2)
allowMissing : false,
alwaysLinkToLastBuild : false,
keepAll : true,
reportDir : './coverage/',
reportFiles : 'index.html',
reportName : 'Coverage Report'
]
}
}
}
| 1 | Llama al nuevo script que calcula la cobertura |
| 2 | Publica el informe de cobertura |
|
Instala el HTML Publisher plugin en Jenkins |
El resultado en Jenkins, debe aparece un enlace nuevo en el menú de la izquierda:
-
Para poder visualizar correctamente el Coverage Report, hay que cambiar la configuración de seguridad de Jenkins predeterminada, que es muy restrictiva para prevenir de archivos HTML/JS maliciosos que podrían instalarse como parte de un Plugin. Para modificar la configuración, abre la consola de scritps (Manage Jenkins / Script Console), y ejecuta estas líneas:
System.setProperty("hudson.model.DirectoryBrowserSupport.CSP", "sandbox; default-src 'none'; img-src 'self'; style-src 'self' 'unsafe-inline'; ")
System.getProperty("hudson.model.DirectoryBrowserSupport.CSP")
Tras ello ya podrás visualizar correctamente el informe de cobertura. Pero ten en cuenta que cada vez que reinicies Jenkins esta configuración se pierde y vuelve a la configuración predeterminada.
Análisis estático de código
El código JavaScript es dinámicamente tipado, por lo que en lugar de usar el compilador para realizar el análisis estático de código, como ocurre en lenguajes como Java, las formas más comunes de análisis estático en JavaScript son formatters y linters.
-
Formatters o formateadores, escanean y reformatean rápidamente los archivos de código. Uno de los más populares es Prettier, que como cualquier buen formateador, corregirá automaticamente las inconsistencias que encuentre.
-
Linters pueden trabajar en aspectos de formato pero también otros problemas más complejos. Se basan en una serie de reglas para escanear el código, o descripciones de comportamientos a vigilar, y muestran todas las violaciones que encuentran. El más popular para JavaScript es ESLint.
Vamos a probar ESLint.
-
Instala con npm:
npm install eslint eslint-config-prettier eslint-plugin-prettier --save-dev
-
A continuación, inicializa un archivo de configuración:
npx eslint --init
Y responde a las preguntas:
Se habrá creado un archivo .eslintrc.json, que incluirá esta línea:
{
"extends": "eslint:recommended" (1)
}
| 1 | Habilita las reglas predeterminadas |
En lugar del anterior fichero, puedes utilizar un fichero .eslintrc.js como el siguiente, que contiene recomendaciones para express:
module.exports = {
env: {
es6: true,
node: true
},
extends: ['prettier'],
plugins: ['prettier'],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly'
},
parserOptions: {
ecmaVersion: 2018,
sourceType: 'module'
},
rules: {
'prettier/prettier': 'error',
'class-methods-use-this': 'off',
'no-param-reassign': 'off',
camelcase: 'off',
'no-unused-vars': ['error', { argsIgnorePattern: 'next' }]
}
};
-
Añade a
package.jsonun script paralinty la dependencia a ESLint
"scripts": {
...
"lint": "eslint src/**/*.js -f checkstyle -o coverage/eslint-result.xml"
},
...
"devDependencies": {
...
"eslint": "^8.10.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"prettier": "^2.5.1",
}
...
-
Lánzalo en local:
npm run lint -s
El parámetro -s se utiliza para que no muestre mensajes de error. Habrá generado el archivo coverage/eslint-result.xml en formato similar al informe de CheckStyle para poder importarlo correctamente en Jenkins.
-
En Jenkins, añade una nueva fase
Analysisen el pipeline, en la que llames alinty publiques el informe generado por ESLint con el formato CheckStyle.
stage('Analysis'){
steps{
sh 'npm run lint -s'
}
post {
always{
// record lint issues found, also, fail the build if there are ANY NEW issues found
recordIssues enabledForFailure: true,
blameDisabled: true,
tools: [esLint(pattern: '**/eslint-result.xml')],
qualityGates: [[threshold: 1, type: 'NEW']]
}
}
}
-
El enlace al informe de ESLint no aparece en la página principal del proyecto, en el menú de enlaces, sino que tienes que hacer clic en el número del último build, y en la nueva página ya aparece el enlace:
-
No te preocupes si la fase de análisis que acabas de añadir falla (está en rojo). Es así porque cuando ESLint detecta un error, finaliza con error (
EXIT 1). Si te fijas en el informe, los 2 errores detectados han sido en el archivotest.js(y pueden ser falsos positivos). Para evitarlo, eliminatest/*.jsdel scriptlintenpackage.json.
Tras ello, la nueva ejecución del pipeline se ejecutará correctamente.
Despliegue en la VM
Para desplegar la aplicación hello world en la instancia de despliegue vamos a clonar el repositorio y a continuación ejecutaremos en ella la orden de Node para ponerla en marcha.
Recuerda que ya he hemos realizado una configuración previa sobre la instancia de despliegue, que constituyen los prerrequisitos para esta sección:
-
Con anterioridad ya instalamos NodeJS en la instancia de despliegue.
-
También habíamos copiado la clave pública de despliegue para que Jenkins, que tiene la clave privada asociada, pueda hacer
sshy ejecutar comandos sobre ella. -
Como requisito adicional, para ayudarnos a lanzar
npm startdesde Jenkins, como un proceso demonio en background, usaremos forever. Debes instalarforeveren la instancia de despliegue:sudo npm install forever -g
Una vez revisados los prerrequisitos, añade la fase de despliegue al pipeline en Jenkins:
-
Copia este nueva fase en tu pipeline, sustituyendo DEPLOY_MACHINE por el nombre DNS de tu instancia, y usa el nombre del repositorio git adecuado:
stage('Deploy'){
steps {
sh '''
ssh -i ~/.ssh/id_rsa_deploy ubuntu@DEPLOY_MACHINE "if [ ! -d 'nodeapp' ] ; then
git clone https://github.com/ualcnsa/nodeapp.git
else
cd nodeapp
git pull origin master
fi" (1)
ssh -i ~/.ssh/id_rsa_deploy ubuntu@DEPLOY_MACHINE "if pgrep node; then forever stopall; fi" (2)
ssh -i ~/.ssh/id_rsa_deploy ubuntu@DEPLOY_MACHINE "cd nodeapp && npm install" (3)
ssh -i ~/.ssh/id_rsa_deploy ubuntu@DEPLOY_MACHINE "cd nodeapp && PORT=8080 forever start index.js" (4)
'''
}
}
| 1 | Clona el repositorio si no existe en la máquina de despliegue, si existe hace un pull |
| 2 | Detiene la ejecución de forever si existe de un despliegue anterior, usando forever stop. |
| 3 | Instala las dependencias |
| 4 | Ejecuta la aplicación con forever start en el puerto 8080, que ejecuta el proceso en background como demonio. |