pipeline { agent any options { timestamps() disableConcurrentBuilds() } parameters { choice(name: 'ENVIRONMENT', choices: ['development', 'main'], description: 'Rama a ejecutar') string(name: 'VERSION_MAYOR', defaultValue: '1', description: 'Componente mayor de versión') } environment { REGISTRY = 'dev.aduanasoft.com:8443' IMAGE_NAMESPACE = 'panel-bases-anexo24' } stages { stage('Preflight tools') { steps { sh ''' set -euo pipefail echo "Node: $(hostname)" if ! command -v docker >/dev/null 2>&1; then echo "ERROR: este agente no tiene docker instalado." echo "Usa un nodo Jenkins con label 'docker' y acceso al daemon." exit 1 fi docker --version ''' } } stage('Checkout') { steps { checkout([ $class: 'GitSCM', branches: [[name: "*/${params.ENVIRONMENT}"]], userRemoteConfigs: [[ url: 'https://git.aduanasoft.com/ADUANASOFT/PANEL_BASES_ANEXO24.git', credentialsId: 'gitea_acazares' ]], extensions: [[$class: 'CloneOption', depth: 0, shallow: false]] ]) } } stage('Test') { steps { sh ''' set -euxo pipefail echo "== Test (svelte-check + tsc) ==" FE_CONTAINER="panel-bases-test-${BUILD_NUMBER}" cleanup() { docker rm -f "$FE_CONTAINER" >/dev/null 2>&1 || true } trap cleanup EXIT docker rm -f "$FE_CONTAINER" >/dev/null 2>&1 || true docker run -d --name "$FE_CONTAINER" -w /workspace node:20-alpine sleep infinity docker exec "$FE_CONTAINER" mkdir -p /workspace docker cp "$WORKSPACE/." "$FE_CONTAINER:/workspace" docker exec "$FE_CONTAINER" sh -lc ' set -euxo pipefail node --version npm config set strict-ssl false npm ci --legacy-peer-deps npm run check ' ''' } } stage('Generate version') { steps { script { def year = sh(returnStdout: true, script: 'date +%y').trim() def month = sh(returnStdout: true, script: 'date +%m').trim() if (params.ENVIRONMENT == 'development') { def shortHash = sh(returnStdout: true, script: 'git rev-parse --short=8 HEAD').trim() env.APP_VERSION = "${year}.${month}.${params.VERSION_MAYOR}.${shortHash}" } else { def lastTag = sh(returnStdout: true, script: 'git describe --tags --abbrev=0 2>/dev/null || true').trim() def commitCount = lastTag ? sh(returnStdout: true, script: "git rev-list ${lastTag}..HEAD --count").trim() : sh(returnStdout: true, script: 'git rev-list --count HEAD').trim() if (commitCount == '0') { commitCount = '1' } env.APP_VERSION = "${year}.${month}.${params.VERSION_MAYOR}.${commitCount}" } echo "VERSION: ${env.APP_VERSION}" } } } stage('Docker login') { steps { withCredentials([usernamePassword(credentialsId: 'harbor-credentials', usernameVariable: 'HARBOR_USERNAME', passwordVariable: 'HARBOR_PASSWORD')]) { sh ''' set -euo pipefail echo "$HARBOR_PASSWORD" | docker login "$REGISTRY" -u "$HARBOR_USERNAME" --password-stdin ''' } } } stage('Build + push') { steps { script { retry(3) { sh ''' set -euo pipefail export DOCKER_BUILDKIT=1 docker build \ -t "${REGISTRY}/${IMAGE_NAMESPACE}/app:${APP_VERSION}" \ -t "${REGISTRY}/${IMAGE_NAMESPACE}/app:latest" \ -f ./Dockerfile \ . docker push "${REGISTRY}/${IMAGE_NAMESPACE}/app:${APP_VERSION}" docker push "${REGISTRY}/${IMAGE_NAMESPACE}/app:latest" ''' } } } } stage('Tag release (main)') { when { expression { params.ENVIRONMENT == 'main' } } steps { withCredentials([usernamePassword(credentialsId: 'gitea_acazares', usernameVariable: 'GIT_USERNAME', passwordVariable: 'GIT_PASSWORD')]) { sh ''' set -euo pipefail git -c user.name='Jenkins' -c user.email='jenkins@gitea.local' \ tag -a "v${APP_VERSION}" -m "Release version ${APP_VERSION}" git remote set-url origin "https://${GIT_USERNAME}:${GIT_PASSWORD}@git.aduanasoft.com/ADUANASOFT/PANEL_BASES_ANEXO24.git" git push origin "v${APP_VERSION}" ''' } } } stage('Deploy development') { when { expression { params.ENVIRONMENT == 'development' } } steps { withCredentials([ usernamePassword(credentialsId: 'harbor-credentials', usernameVariable: 'HARBOR_USERNAME', passwordVariable: 'HARBOR_PASSWORD'), string(credentialsId: 'panel-server-user', variable: 'PANEL_SERVER_USER'), string(credentialsId: 'panel-server-host', variable: 'PANEL_SERVER_HOST'), sshUserPrivateKey( credentialsId: 'panel-server-ssh', keyFileVariable: 'PANEL_SERVER_KEY', usernameVariable: 'PANEL_SERVER_SSH_USER', passphraseVariable: 'PANEL_SERVER_KEY_PASSPHRASE' ) ]) { /* * Destino: Windows con OpenSSH Server. El agente Jenkins es Linux (bash). * En bash, C:\rutas\ con \ dentro de "..." se corrompen → usar siempre C:/ruta (válido en PowerShell). * DOCKER_CONFIG local sin wincred para evitar errores de "logon session". * ssh-agent + ssh-add para clave con passphrase. * StrictHostKeyChecking=accept-new con known_hosts en workspace para idempotencia. */ sh ''' set -euo pipefail WIN_DOCKER_CFG='C:/Aduanasoft/docker-jenkins-ci' WIN_APP_PROD='C:/Aduanasoft/panel_bases_prod' chmod 600 "${PANEL_SERVER_KEY}" || true eval "$(ssh-agent -s)" trap 'kill "${SSH_AGENT_PID}" 2>/dev/null || true' EXIT if [ -n "${PANEL_SERVER_KEY_PASSPHRASE}" ]; then printf '%s\n' "${PANEL_SERVER_KEY_PASSPHRASE}" | ssh-add "${PANEL_SERVER_KEY}" else ssh-add "${PANEL_SERVER_KEY}" fi KNOWN="${WORKSPACE}/.jenkins-panel-known_hosts" : > "${KNOWN}" SSHOPTS="-o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=${KNOWN}" SSH_U="${PANEL_SERVER_USER}" if [ -z "${SSH_U}" ]; then SSH_U="${PANEL_SERVER_SSH_USER}"; fi # Paso 1: inicializar DOCKER_CONFIG limpio (sin credsStore/wincred) DOCKER_CFG_PS_TEMPLATE="$(cat <<'EOF' $ErrorActionPreference = "Stop" $cfg = "_CFG_" New-Item -ItemType Directory -Force -Path $cfg | Out-Null Remove-Item -Path (Join-Path $cfg "config.json") -Force -ErrorAction SilentlyContinue Set-Content -Path (Join-Path $cfg "config.json") -Value '{"auths":{}}' -Encoding Ascii -NoNewline EOF )" DOCKER_CFG_PS_CMD="$(printf '%s' "${DOCKER_CFG_PS_TEMPLATE}" | sed "s|_CFG_|${WIN_DOCKER_CFG}|g")" DOCKER_CFG_PS_B64="$(printf '%s' "${DOCKER_CFG_PS_CMD}" | iconv -f UTF-8 -t UTF-16LE | base64 -w 0)" ssh ${SSHOPTS} "${SSH_U}@${PANEL_SERVER_HOST}" \ powershell.exe -NoProfile -NonInteractive -EncodedCommand "${DOCKER_CFG_PS_B64}" # Paso 2: escribir auth de Harbor directo en config.json (evita docker login + wincred) if [ -z "${HARBOR_PASSWORD:-}" ]; then echo "ERROR: HARBOR_PASSWORD vacío en Jenkins credentials" exit 1 fi set +x HARBOR_PASSWORD_B64="$(printf '%s' "${HARBOR_PASSWORD}" | base64 -w 0)" DOCKER_AUTH_PS_TEMPLATE="$(cat <<'EOF' $ErrorActionPreference = "Stop" $cfg = "_CFG_" $usr = "_USER_" $reg = "_REG_" $pwdB64 = "_PWD_B64_" $pwd = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($pwdB64)) if ([string]::IsNullOrWhiteSpace($pwd)) { throw "HARBOR_PASSWORD vacio o invalido." } $auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes("${usr}:$pwd")) $json = "{""auths"":{""$reg"":{""auth"":""$auth""}}}" Set-Content -Path (Join-Path $cfg "config.json") -Value $json -Encoding Ascii -NoNewline EOF )" DOCKER_AUTH_PS_CMD="$(printf '%s' "${DOCKER_AUTH_PS_TEMPLATE}" \ | sed "s|_CFG_|${WIN_DOCKER_CFG}|g" \ | sed "s|_USER_|${HARBOR_USERNAME}|g" \ | sed "s|_REG_|${REGISTRY}|g" \ | sed "s|_PWD_B64_|${HARBOR_PASSWORD_B64}|g")" AUTH_PS_B64="$(printf '%s' "${DOCKER_AUTH_PS_CMD}" | iconv -f UTF-8 -t UTF-16LE | base64 -w 0)" ssh ${SSHOPTS} "${SSH_U}@${PANEL_SERVER_HOST}" \ powershell.exe -NoProfile -NonInteractive -EncodedCommand "${AUTH_PS_B64}" # Paso 3: pull + up + prune DOCKER_COMPOSE_PS_TEMPLATE="$(cat <<'EOF' $ErrorActionPreference = "Stop" $env:DOCKER_CONFIG = "_CFG_" Set-Location "_APP_PROD_" docker compose -f docker-compose.prod.yml pull docker compose -f docker-compose.prod.yml up -d docker image prune -f EOF )" DOCKER_COMPOSE_PS_CMD="$(printf '%s' "${DOCKER_COMPOSE_PS_TEMPLATE}" \ | sed "s|_CFG_|${WIN_DOCKER_CFG}|g" \ | sed "s|_APP_PROD_|${WIN_APP_PROD}|g")" COMPOSE_PS_B64="$(printf '%s' "${DOCKER_COMPOSE_PS_CMD}" | iconv -f UTF-8 -t UTF-16LE | base64 -w 0)" set -x ssh ${SSHOPTS} "${SSH_U}@${PANEL_SERVER_HOST}" \ powershell.exe -NoProfile -NonInteractive -EncodedCommand "${COMPOSE_PS_B64}" ''' } } } } }