5 min read

Shared Libraries en Jenkins


Imagínate tener más de 5 proyectos frontend que desplegar en los ambientes de qa y producción, para automatizar el despliegue con Jenkins y para evitar tener la lógica de los stages en cada proyecto y se vuelvan grandes y difícil de mantener, tendremos que utilizar los Shared Libraries.

Los shared libraries sirven para reutilizar código de pipelines entre varios proyectos, además hacen que estos pipelines estén limpios y sean fáciles de reutilizar.

Lo que primero debemos hacer es crear un proyecto en un repositorio de código como GitHub o GitLab y crear la siguiente estructura:

https://gitlab.com/repo/shared-library

(root del repo de la librería)
├── vars/
│   ├── gitVersioning.groovy        # Lógica de versionamiento semántico
│   ├── dockerBuildPush.groovy      # Lógica de construcción y subida de Docker
│   ├── deployDocker.groovy         # Lógica de desplegar imagen con docker run
│   └── otros...
└── src/                            # (Opcional, para otros archivos de infra)

Configuración en Jenkins

Agregamos el repositorio como una librería en Jenkins en la configuracion global, para hacer esto vamos a:

Administrar Jenkins → System → Global Trusted Pipeline Libraries

Una vez dentro completamos con los siguientes datos:

  • Name: my-shared-library (Nombre clave que se usara para llamar al shared library)
  • Default version: main (Nombre de la rama que se usará por defecto)

Dejamos checked las opciones:

  • Allow default version to be overridden
  • Include @Library changes in job recents changes

Continuamos completando:

  • Retrieval method ponemos: Modern SCM
  • Source code Management: Git
  • Project repository: https://gitlab.com/repo/shared-library (nuestro repositorio GitLab)
  • Credentials: Seleccionamos la que da acceso al código del repositorio

En behaivours no completamos nada y en library path dejamos el valor por defecto ./

Luego le damos en guardar.

Usar esta shared library:

Tener definido el código o función que deseamos reutilizar, ejemplo de deployDocker.groovy

def call(Map config = [:]) {
  def sshUser = config.sshUser ?: config.sshUser
  def sshHost = config.sshHost ?: config.sshHost
  def credentialsId = config.credentialsId
  def registryUrl = config.registryUrl
  def imageName = config.imageName
  def fullImage = "${registryUrl}/${imageName}"
  def port = config.port

  if (!credentialsId) {
    error "Error: 'credentialsId' is needed for SSH authentication."
  }

  script {
    sshagent(credentials: [credentialsId]) {
      echo "--- Deploying using Docker Run to ${sshHost} ---"
      sh """
        ssh -o StrictHostKeyChecking=no ${sshUser}@${sshHost} '
            set -e
            echo "Pulling image ${fullImage}:latest..."
            docker pull ${fullImage}:latest

            echo "Recreating container ${imageName}..."
            docker stop ${imageName} || true
            docker rm ${imageName} || true

            docker run -d --restart unless-stopped \
              --name ${imageName} \
              -p ${port}:80 \
              ${fullImage}:latest
        '
      """
    }
  }
}

Para reutilizar este código en un repositorio (en este caso, un proyecto frontend en Angular), creamos un archivo Jenkinsfile.

Estructura del proyecto angular:

angular.json
CHANGELOG.md
deploy.ps1
Dockerfile
Jenkinsfile
karma.conf.js
nginx.conf
node_modules/
package.json
README.md
src/
tsconfig.app.json
tsconfig.json
tsconfig.spec.json
yarn.lock

En el Jenkinsfile completamos:

@Library('my-shared-library') _

pipeline {
  agent {
    node {
      label "${params.NODE_LABEL ?: 'main'}"
    }
  }

  // Avoid checking out code before 'Prepare' stage
  options {
    skipDefaultCheckout()
    timeout(time: 1, unit: 'HOURS')
  }

  parameters {
    choice(name: 'NODE_LABEL', choices: ['main', 'children'], description: 'Selecciona el nodo/label para el build')
    choice(name: 'ENV_TYPE', choices: ['stage', 'production'], description: 'Entorno de despliegue')
    choice(name: 'RELEASE_TYPE', choices: ['auto', 'patch', 'minor', 'major'], description: 'Tipo de incremento')
  }

  environment {
    // variables
    ARTIFACTORY_PATH = 'myapp'
    IMAGE_NAME       = 'repo-myapp'
    GIT_REPO_URL     = 'gitlab.com/repo/myapp.git'
    ARTIFACTORY_REPO = 'repo-dist'
    CONTAINER_NAME   = 'repo_myapp'
    DEPLOY_PATH_PROD = 'D:\\app\\myapp'
    PORT             = '8010'
    //constants
    REGISTRY_URL     = 'docker.myregistry.com'
    DOCKER_CREDS     = 'docker-registry-credentials'
    GIT_CREDS        = 'gitlab-credentials'
    ARTIFACTORY_URL  = 'https://artifactory.myregistry.com/artifactory/'
    ARTIFACTORY_ACCESS_TOKEN = credentials('artifactory-access-token')
    JOB_SAFE_NAME    = env.JOB_NAME.replaceAll('[^A-Za-z0-9]', '_')
    SERVER_HOST      = credentials('SSH_HOST')
    SERVER_USER      = credentials('SSH_USER')
    SERVER_SSH_CREDS = 'server-ssh-credentials-id'
    DISCORD_WEBHOOK  = credentials('DISCORD_WEBHOOK_URL')
    WIN_HOST         = 'ssh-server-repo.myregistry.com'
    WIN_CRED_ID      = 'WINDOWS_PROD_CRED_ESPG'
  }

  stages {
    stage('Prepare') {
      steps {
        sh 'chown -R $(id -u):$(id -g) . || true'
        cleanWs()
        checkout scm

        notifyPreBuild(
          imageName: env.IMAGE_NAME,
          envType: env.ENV_TYPE,
          discordWebhook: env.DISCORD_WEBHOOK
        )
      }
    }

    stage('Deploy QA (Linux)') {
      when { expression { params.ENV_TYPE == 'stage' } }

      stages {
        stage('Versioning') {
          when { expression { return isMainBranch() } }
          steps {
            script {
              docker.image('dalthonmh/node-alpine-git').inside('-u 0:0') {
                gitVersioningJs(
                  gitCreds: env.GIT_CREDS,
                  repoUrl: env.GIT_REPO_URL,
                  releaseType: params.RELEASE_TYPE
                )
              }
            }
          }
        }

        stage('Generate Dist Angular') {
          steps {
            script {
              def dockerArgs =
              '-u 0:0 ' +
              "-v /var/jenkins_home/caches/${env.JOB_SAFE_NAME}/yarn:/usr/local/share/.cache/yarn " +
              "-v /var/jenkins_home/caches/${env.JOB_SAFE_NAME}/angular:/root/.angular"

              docker.image('node:24-alpine').inside(dockerArgs) {
                sh 'cp ./src/environments/environment.prod.ts ./src/environments/environment.ts'
                angularBuild(envType: params.ENV_TYPE)
                sh 'chown -R $(id -u):$(id -g) dist/'
              }
            }
          }
        }

        stage('Package & Upload') {
          when { expression { return isMainBranch() } }
          steps {
            script {
              sh 'zip -qr dist.zip dist/'

              docker.image('releases-docker.jfrog.io/jfrog/jfrog-cli-v2-jf').inside('-u 0:0') {
                echo 'Uploading to Artifactory...'
                publishArtifact(
                  artifactoryUrl: env.ARTIFACTORY_URL,
                  artifactoryRepo: env.ARTIFACTORY_REPO,
                  artifactoryPath: env.ARTIFACTORY_PATH,
                  artifactoryAccessToken: env.ARTIFACTORY_ACCESS_TOKEN,
                )
              }
            }
          }
        }

        stage('Build & Push Docker') {
          steps {
            sh 'ls -la dist/'
            dockerBuildPush(
              registryUrl: env.REGISTRY_URL,
              imageName: env.IMAGE_NAME,
              dockerCreds: env.DOCKER_CREDS
            )
          }
        }

        stage('Deploy') {
          when { expression { return isMainBranch() } }
          steps {
            deployDocker(
              sshUser: env.SERVER_USER,
              sshHost: env.SERVER_HOST,
              credentialsId: env.SERVER_SSH_CREDS,
              registryUrl: env.REGISTRY_URL,
              imageName: env.IMAGE_NAME,
              port: env.PORT
            )
          }
        }
      }
    }

    stage('Deploy Prod (Windows)') {
      when { expression { params.ENV_TYPE == 'production' } }
      steps {
        deployProdWindows(
          deployPath: env.DEPLOY_PATH_PROD,
          winHost: env.WIN_HOST,
          winCredID: env.WIN_CRED_ID
        )
      }
    }
  }

  post {
    success {
      notifySuccess(
          imageName: env.IMAGE_NAME,
          envType: env.ENV_TYPE,
          discordWebhook: env.DISCORD_WEBHOOK
      )
    }

    failure {
      notifyFailed(
          imageName: env.IMAGE_NAME,
          envType: env.ENV_TYPE,
          discordWebhook: env.DISCORD_WEBHOOK
      )
    }
  }
}

De esa forma podremos reutilizar el código entre varios proyectos.