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.

La implementación de la integración y despliegue continuos permitirá que, para cada cambio de código en el repositorio, Jenkins será notificado y descargará los cambios, instalará las dependencias y ejecutará los tests. Si los tests pasan correctamente, Jenkins desplegará la aplicación en el servidor de despliegue. Y si fallan, se notificará al desarrollador.

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:

  1. El archivo package.json contienen 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

package.json
{
  "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 main.js, app.js y app.test.js, así como la carpeta services, estén dentro de una carpeta src. Si no es así, crea la carpeta src y muevelos dentro. Revisa el conetenido de package.json para que sea idéntico al mostrado aquí.

node files initial point
Fig. 1. Archivos y carpetas en el estado inicial

Para instalar las dependencias ejecuta npm install.

  1. El archivo principal del proyecto src/main.js se encarga de arrancar la aplicación en el puerto 3000.

src/main.js
const app = require("./app");
const port = 3000
app.listen(port, () => {
  console.log(`Example app listening on port ${port}`)
})
  1. El archivo src/app.js es un sencillo hola mundo con dos rutas:

    • / devuelve "Hello World!"

    • /:nameToSalute devuelve "Hello " + nameToSalute + "!" mediante el servicio HelloWordService

src/app.js
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
  1. El archivo src/services/hello-world.js es un servicio de hola mundo.

src/services/hello-world.js
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.

src/services/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!.

src/app.test.js
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

node jest passed
Fig. 2. 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:

jenkins node pipeline1
Fig. 3. Nodeapp pipeline

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.

  1. Editamos package.json y añadimos el script test-jenkins para generar los resultados de los test en formato xml que usará Jenkins para generar el gráfico, y la dependencia necesaria para ello:

package.json: jenkins-test y dependencia mocha-junit-reporter
  ...
  "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 .gitignore la carpeta /coverage, ya que su contenido se generará al lanzar los tests y no se debe guardar en el repositorio.

Guarda los cambios en el repositorio, para que estén actualizados cuando los lea Jenkins.

  1. 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:

jenkins nodeapp pipeline test result trend
Fig. 4. Publicado el gráfico de tendencia de los test

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:

  1. En GitHub, seleccionamos el repositorio sobre el que queremos activar la construcción en Jenkins y hacemos clic en: Settings > WebHooks > Add webhook

  2. En Payload URL:

    http://{YOUR_JENKINS_URL}/github-webhook/
jenkins webhook github
Fig. 5. Nuevo Webhook
  1. 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

jenkins webhook build triger
Fig. 6. Activar el Webhook en build trigers

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.json un 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:

package.json: cobertura
   ...
   "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.

node jest coverage jenkins ok
Fig. 7. Ejecución de cobertura

Como resultado, en la carpeta coverage del proyecto se ha generado el informe de cobertura.

node mocha coverage results
Fig. 8. Informe de cobertura
node jest coverage index
Fig. 9. Informe de cobertura en html
  1. 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:

jenkins node coverage report link
Fig. 10. Enlace al informe de cobertura en html
  1. 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")
maven script console site
Fig. 11. Script Console: permisos para visualizar el informe de cobertura

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.

  1. Instala con npm:

    npm install eslint eslint-config-prettier eslint-plugin-prettier --save-dev
  2. A continuación, inicializa un archivo de configuración:

    npx eslint --init

Y responde a las preguntas:

eslint init
Fig. 12. ESLint init

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' }]
    }
};
  1. Añade a package.json un script para lint y la dependencia a ESLint

package.json: lint y 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",
   }
   ...
  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.

  1. En Jenkins, añade una nueva fase Analysis en el pipeline, en la que llames a lint y 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']]
        }
      }
    }
  1. 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:

eslint jenkins link
Fig. 13. Enlace al informe ESLint
  1. 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 archivo test.js (y pueden ser falsos positivos). Para evitarlo, elimina test/*.js del script lint en package.json.

Tras ello, la nueva ejecución del pipeline se ejecutará correctamente.

eslint jenkins pass grapth
Fig. 14. Fase ESLint passed

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 ssh y ejecutar comandos sobre ella.

  • Como requisito adicional, para ayudarnos a lanzar npm start desde Jenkins, como un proceso demonio en background, usaremos forever. Debes instalar forever en la instancia de despliegue:

    sudo npm install forever -g

Una vez revisados los prerrequisitos, añade la fase de despliegue al pipeline en Jenkins:

  1. 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.