diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..b28469e --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,258 @@ +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}" + ''' + } + } + } + } +} diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..52e79e6 --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,47 @@ +services: + app: + image: dev.aduanasoft.com:8443/panel-bases-anexo24/app:latest + restart: unless-stopped + ports: + - "3000:3000" + environment: + # SQL Server: credenciales globales (server_name/database_name vienen de a24c.database_nodes en Postgres) + - PANEL_MSSQL_USER=${PANEL_MSSQL_USER} + - PANEL_MSSQL_PASSWORD=${PANEL_MSSQL_PASSWORD} + - PANEL_MSSQL_DOCKER=true + - IN_DOCKER=true + + # PostgreSQL: BD externa (ControlDesk), accesible desde el host + - DB_POSTGRES_HOST=host.docker.internal + - DB_POSTGRES_PORT=${DB_POSTGRES_PORT:-5432} + - DB_POSTGRES_USER=${DB_POSTGRES_USER} + - DB_POSTGRES_PASS=${DB_POSTGRES_PASS} + - DB_POSTGRES_DB=${DB_POSTGRES_DB} + + # JWT + - JWT_SECRET=${JWT_SECRET} + + # Ruta de respaldos montada desde el host + - BACKUP_PATH=/data/backups + + - PORT=3000 + - ORIGIN=${ORIGIN} + - NODE_ENV=production + + # SMTP para avisos (opcional — vacío deshabilita el envío sin errores) + - SMTP_HOST=${SMTP_HOST:-} + - SMTP_PORT=${SMTP_PORT:-587} + - SMTP_USER=${SMTP_USER:-} + - SMTP_PASSWORD=${SMTP_PASSWORD:-} + - SMTP_FROM=${SMTP_FROM:-} + - SMTP_USE_TLS=${SMTP_USE_TLS:-true} + volumes: + - ${BACKUP_HOST_PATH}:/data/backups + extra_hosts: + - "host.docker.internal:host-gateway" + networks: + - aduanasoft-network + +networks: + aduanasoft-network: + external: true