Qué vas a conseguir
Que ningún script del equipo llegue a la rama principal sin haber pasado un análisis estático y sus pruebas, y que cuando alguien rompa uno, el canal de Teams lo diga con el nombre y el commit. El repositorio de la guía de Git del Nivel 2 deja de ser una carpeta compartida y pasa a ser código con integración continua.
Antes de empezar
- El servidor de herramientas con Docker (guía de Docker) y 4 GB de RAM libres.
- El repositorio de scripts en Git (GitHub, GitLab o Azure DevOps) con una cuenta de servicio de solo lectura o un token de acceso para Jenkins.
- Al menos un script de PowerShell con un test de Pester (
tests/*.Tests.ps1) y uno de Bash. - El webhook de Teams de la guía de Grafana (vale el mismo canal).
1. Jenkins en Docker
services: jenkins: image: jenkins/jenkins:2.479.1-lts-jdk17 container_name: jenkins restart: unless-stopped ports: ["8080:8080", "50000:50000"] volumes: - jenkins-home:/var/jenkins_home - /var/run/docker.sock:/var/run/docker.sock environment: JAVA_OPTS: -Djenkins.install.runSetupWizard=truevolumes: jenkins-home:docker compose up -d && sleep 45docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword3f9c1a2b7d4e4c88a9e0b1f2c3d4e5f6docker compose exec -u root jenkins sh -c "apt-get update -qq && apt-get install -y -qq docker.io >/dev/null && usermod -aG docker jenkins"Abre http://srv-tools-01:8080, pega la contraseña inicial, elige Install suggested plugins y crea el usuario administrador (contraseña en el gestor del equipo, no en la wiki). Añade después los plugins Docker Pipeline, Office 365 Connector (Teams) y Warnings Next Generation. Montar el socket de Docker permite que cada etapa corra en un contenedor limpio con las herramientas que necesite, sin instalarlas en Jenkins.
2. Conectar el repositorio
Multibranch crea un job por rama y por pull request automáticamente: la rama feature/inventario-v2 de la guía de Git se prueba sola en cuanto se sube. El webhook del repositorio hacia http://srv-tools-01:8080/github-webhook/ evita esperar los 15 minutos; si el servidor no es accesible desde internet, el sondeo periódico basta.
3. El Jenkinsfile
pipeline { agent none options { timestamps(); disableConcurrentBuilds() } stages { stage('Lint Bash') { agent { docker { image 'koalaman/shellcheck-alpine:v0.10.0' } } steps { sh 'find bash -name "*.sh" -print0 | xargs -0 shellcheck -f checkstyle > shellcheck.xml || true' recordIssues tool: checkStyle(pattern: 'shellcheck.xml', name: 'shellcheck'), qualityGates: [[threshold: 1, type: 'TOTAL_ERROR', unstable: false]] } } stage('Lint PowerShell') { agent { docker { image 'mcr.microsoft.com/powershell:7.4-ubuntu-22.04' } } steps { sh ''' pwsh -NoProfile -Command " Install-Module PSScriptAnalyzer -Force -Scope CurrentUser \$r = Invoke-ScriptAnalyzer -Path ./powershell -Recurse -Severity Error,Warning \$r | Format-Table -AutoSize if (\$r | Where-Object Severity -eq 'Error') { exit 1 }" ''' } } stage('Test PowerShell') { agent { docker { image 'mcr.microsoft.com/powershell:7.4-ubuntu-22.04' } } steps { sh ''' pwsh -NoProfile -Command " Install-Module Pester -Force -Scope CurrentUser -MinimumVersion 5.6 \$c = New-PesterConfiguration \$c.Run.Path = './tests'; \$c.Run.Exit = \$true \$c.TestResult.Enabled = \$true; \$c.TestResult.OutputPath = 'pester.xml'; \$c.TestResult.OutputFormat = 'JUnitXml' Invoke-Pester -Configuration \$c" ''' } post { always { junit 'pester.xml' } } } } post { failure { office365ConnectorSend webhookUrl: credentials('teams-it-ops-webhook'), message: "❌ ${env.JOB_NAME} #${env.BUILD_NUMBER} falló (${env.GIT_COMMIT.take(7)})", status: 'Failed' } fixed { office365ConnectorSend webhookUrl: credentials('teams-it-ops-webhook'), message: "✅ ${env.JOB_NAME} vuelve a estar en verde", status: 'Fixed' } }}Cada etapa corre en su imagen: shellcheck, PowerShell 7 con PSScriptAnalyzer, PowerShell 7 con Pester. recordIssues pinta las advertencias en Jenkins; el exit 1 en errores de PSScriptAnalyzer y Run.Exit de Pester son lo que hace que el pipeline falle de verdad. El webhook de Teams se guarda en Manage Jenkins → Credentials como texto secreto con el id teams-it-ops-webhook; nunca en el Jenkinsfile.
4. La primera ejecución y el primer fallo
[Pipeline] stage (Lint Bash)+ find bash -name *.sh -print0+ xargs -0 shellcheck -f checkstyle[shellcheck] Found 2 issues (1 error, 1 warning)[Pipeline] stage (Lint PowerShell)RuleName Severity ScriptName Line Message-------- -------- ---------- ---- -------PSAvoidUsingPlainTextForPassword Warning Get-Inventory.ps1 14 Parameter '$Password' should use SecureString[Pipeline] stage (Test PowerShell)Starting discovery in 1 files.Describing Get-Inventory [+] devuelve una fila por equipo 112ms [-] escribe el CSV con las columnas esperadas 38ms Expected 'LastBoot' to be found in collection @('Name','OS','FreeGB'), but it was not found.Tests completed in 1.21sTests Passed: 1, Failed: 1, Skipped: 0[Pipeline] junitRecording test resultsERROR: script returned exit code 1Finished: FAILUREEl pipeline hizo su trabajo: alguien cambió las columnas del CSV y el test lo cazó antes de que el script de inventario de la guía de PowerShell dejara de servir al informe mensual. En Teams aparece el mensaje con el commit. El arreglo es un commit en la rama, y Jenkins vuelve a probar solo.
Consejo: configura la rama main como protegida en el repositorio, con "require status checks" apuntando al job de Jenkins. Así un pull request no se puede fusionar en rojo, y la regla la pone el repositorio, no la buena voluntad.
5. Lo que revisas cada mes
- Jobs en rojo más de una semana: o se arreglan o se borran los scripts.
- Manage Jenkins → Plugins: actualizaciones de seguridad pendientes; Jenkins avisa con un banner.
- Espacio de
jenkins-homey la retención de builds (options { buildDiscarder(logRotator(numToKeepStr: '30')) }en el Jenkinsfile). - Credenciales que caducan (el token de Git, el webhook) y quién las conoce.
Siguientes pasos
- Una etapa de despliegue: cuando
mainesté en verde, copiar los scripts al recurso compartido desde el que los lanza la tarea programada, o lanzar el playbook de Ansible conansible-playbooken un contenedor. - Lo mismo con GitHub Actions o Azure Pipelines si el repositorio ya está allí: el Jenkinsfile se traduce casi línea a línea y te ahorras el servidor.
- Añade
Pestertambién a los scripts de Bash con bats-core: el patrón es el mismo.
¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.
What you'll get
No team script reaches the main branch without passing static analysis and its tests, and when someone breaks one, the Teams channel says so with the name and the commit. The repository from the Level 2 Git guide stops being a shared folder and becomes code with continuous integration.
Before you start
- The tools server with Docker (Docker guide) and 4 GB of free RAM.
- The scripts repository in Git (GitHub, GitLab or Azure DevOps) with a read-only service account or an access token for Jenkins.
- At least one PowerShell script with a Pester test (
tests/*.Tests.ps1) and one Bash script. - The Teams webhook from the Grafana guide (the same channel works).
1. Jenkins in Docker
services: jenkins: image: jenkins/jenkins:2.479.1-lts-jdk17 container_name: jenkins restart: unless-stopped ports: ["8080:8080", "50000:50000"] volumes: - jenkins-home:/var/jenkins_home - /var/run/docker.sock:/var/run/docker.sock environment: JAVA_OPTS: -Djenkins.install.runSetupWizard=truevolumes: jenkins-home:docker compose up -d && sleep 45docker compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword3f9c1a2b7d4e4c88a9e0b1f2c3d4e5f6docker compose exec -u root jenkins sh -c "apt-get update -qq && apt-get install -y -qq docker.io >/dev/null && usermod -aG docker jenkins"Open http://srv-tools-01:8080, paste the initial password, choose Install suggested plugins and create the admin user (password in the team's password manager, not the wiki). Then add the Docker Pipeline, Office 365 Connector (Teams) and Warnings Next Generation plugins. Mounting the Docker socket lets each stage run in a clean container with the tools it needs, without installing them in Jenkins.
2. Connect the repository
Multibranch creates one job per branch and per pull request automatically: the feature/inventory-v2 branch from the Git guide gets tested on its own as soon as it is pushed. A repository webhook to http://srv-tools-01:8080/github-webhook/ avoids waiting the 15 minutes; if the server is not reachable from the internet, periodic polling is enough.
3. The Jenkinsfile
pipeline { agent none options { timestamps(); disableConcurrentBuilds() } stages { stage('Lint Bash') { agent { docker { image 'koalaman/shellcheck-alpine:v0.10.0' } } steps { sh 'find bash -name "*.sh" -print0 | xargs -0 shellcheck -f checkstyle > shellcheck.xml || true' recordIssues tool: checkStyle(pattern: 'shellcheck.xml', name: 'shellcheck'), qualityGates: [[threshold: 1, type: 'TOTAL_ERROR', unstable: false]] } } stage('Lint PowerShell') { agent { docker { image 'mcr.microsoft.com/powershell:7.4-ubuntu-22.04' } } steps { sh ''' pwsh -NoProfile -Command " Install-Module PSScriptAnalyzer -Force -Scope CurrentUser \$r = Invoke-ScriptAnalyzer -Path ./powershell -Recurse -Severity Error,Warning \$r | Format-Table -AutoSize if (\$r | Where-Object Severity -eq 'Error') { exit 1 }" ''' } } stage('Test PowerShell') { agent { docker { image 'mcr.microsoft.com/powershell:7.4-ubuntu-22.04' } } steps { sh ''' pwsh -NoProfile -Command " Install-Module Pester -Force -Scope CurrentUser -MinimumVersion 5.6 \$c = New-PesterConfiguration \$c.Run.Path = './tests'; \$c.Run.Exit = \$true \$c.TestResult.Enabled = \$true; \$c.TestResult.OutputPath = 'pester.xml'; \$c.TestResult.OutputFormat = 'JUnitXml' Invoke-Pester -Configuration \$c" ''' } post { always { junit 'pester.xml' } } } } post { failure { office365ConnectorSend webhookUrl: credentials('teams-it-ops-webhook'), message: "❌ ${env.JOB_NAME} #${env.BUILD_NUMBER} failed (${env.GIT_COMMIT.take(7)})", status: 'Failed' } fixed { office365ConnectorSend webhookUrl: credentials('teams-it-ops-webhook'), message: "✅ ${env.JOB_NAME} is green again", status: 'Fixed' } }}Each stage runs in its own image: shellcheck, PowerShell 7 with PSScriptAnalyzer, PowerShell 7 with Pester. recordIssues renders the warnings in Jenkins; the exit 1 on PSScriptAnalyzer errors and Pester's Run.Exit are what make the pipeline fail for real. The Teams webhook is stored in Manage Jenkins → Credentials as secret text with the id teams-it-ops-webhook; never in the Jenkinsfile.
4. The first run and the first failure
[Pipeline] stage (Lint Bash)+ find bash -name *.sh -print0+ xargs -0 shellcheck -f checkstyle[shellcheck] Found 2 issues (1 error, 1 warning)[Pipeline] stage (Lint PowerShell)RuleName Severity ScriptName Line Message-------- -------- ---------- ---- -------PSAvoidUsingPlainTextForPassword Warning Get-Inventory.ps1 14 Parameter '$Password' should use SecureString[Pipeline] stage (Test PowerShell)Starting discovery in 1 files.Describing Get-Inventory [+] returns one row per device 112ms [-] writes the CSV with the expected columns 38ms Expected 'LastBoot' to be found in collection @('Name','OS','FreeGB'), but it was not found.Tests completed in 1.21sTests Passed: 1, Failed: 1, Skipped: 0[Pipeline] junitRecording test resultsERROR: script returned exit code 1Finished: FAILUREThe pipeline did its job: someone changed the CSV columns and the test caught it before the inventory script from the PowerShell guide stopped feeding the monthly report. In Teams the message appears with the commit. The fix is a commit on the branch, and Jenkins tests again by itself.
Tip: set the main branch as protected in the repository, with "require status checks" pointing at the Jenkins job. That way a pull request cannot be merged while red, and the rule is enforced by the repository, not by goodwill.
5. What you review each month
- Jobs red for more than a week: either fix them or delete the scripts.
- Manage Jenkins → Plugins: pending security updates; Jenkins warns with a banner.
jenkins-homedisk and build retention (options { buildDiscarder(logRotator(numToKeepStr: '30')) }in the Jenkinsfile).- Credentials that expire (the Git token, the webhook) and who knows them.
Next steps
- A deploy stage: when
mainis green, copy the scripts to the share the scheduled task runs them from, or launch the Ansible playbook withansible-playbookin a container. - The same with GitHub Actions or Azure Pipelines if the repository already lives there: the Jenkinsfile translates almost line by line and you save the server.
- Add tests to the Bash scripts too with bats-core: the pattern is the same.
Is something missing, or different in your environment? Get in touch and I'll fix it.
Tech Career Lab · newsletter
¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.Found this useful? Every two weeks I send one practical idea for IT professionals: LinkedIn, projects, networking and hiring processes.
Apúntate y llévate las 33 ideasSign up and get the 33 ideas →