<?xml version='1.1' encoding='UTF-8'?>
<project>
  <actions/>
  <description>Build the ERP frontend on the Jenkins controller and deploy it to ANY LogShip instance (demo clones etc.) over ssh.&#xd;
Parameters: TARGET_HOST (instance ip), BRANCH (git branch). Script source of truth: reference/ci/deploy-instance.sh in the repo — paste it here again after changes.&#xd;
Keeps the instance .env + data/, regenerates NUXT_* overrides, pm2 restart from ecosystem, rollback on failed health check.</description>
  <keepDependencies>false</keepDependencies>
  <properties>
    <jenkins.model.BuildDiscarderProperty>
      <strategy class="hudson.tasks.LogRotator">
        <daysToKeep>-1</daysToKeep>
        <numToKeep>5</numToKeep>
        <artifactDaysToKeep>-1</artifactDaysToKeep>
        <artifactNumToKeep>-1</artifactNumToKeep>
        <removeLastBuild>false</removeLastBuild>
      </strategy>
    </jenkins.model.BuildDiscarderProperty>
    <hudson.model.ParametersDefinitionProperty>
      <parameterDefinitions>
        <hudson.model.StringParameterDefinition>
          <name>TARGET_HOST</name>
          <description>IP or hostname of the LogShip instance (ssh as root from the jenkins user; its key must be in authorized_keys — true for every clone of the golden template)</description>
          <defaultValue>192.168.12.68</defaultValue>
          <trim>true</trim>
        </hudson.model.StringParameterDefinition>
        <hudson.model.StringParameterDefinition>
          <name>BRANCH</name>
          <description>Git branch to build (dev, main, feature/…)</description>
          <defaultValue>dev</defaultValue>
          <trim>true</trim>
        </hudson.model.StringParameterDefinition>
      </parameterDefinitions>
    </hudson.model.ParametersDefinitionProperty>
  </properties>
  <scm class="hudson.plugins.git.GitSCM" plugin="git@5.2.1">
    <configVersion>2</configVersion>
    <userRemoteConfigs>
      <hudson.plugins.git.UserRemoteConfig>
        <url>https://github.com/younex1/erp-nuxt-frontend.git</url>
        <credentialsId>younex1</credentialsId>
      </hudson.plugins.git.UserRemoteConfig>
    </userRemoteConfigs>
    <branches>
      <hudson.plugins.git.BranchSpec>
        <name>*/${BRANCH}</name>
      </hudson.plugins.git.BranchSpec>
    </branches>
    <doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
    <submoduleCfg class="empty-list"/>
    <extensions/>
  </scm>
  <assignedNode>built-in</assignedNode>
  <canRoam>false</canRoam>
  <disabled>false</disabled>
  <blockBuildWhenDownstreamBuilding>false</blockBuildWhenDownstreamBuilding>
  <blockBuildWhenUpstreamBuilding>false</blockBuildWhenUpstreamBuilding>
  <authToken>__BUILD_TOKEN__</authToken>
  <triggers/>
  <concurrentBuild>false</concurrentBuild>
  <builders>
    <hudson.tasks.Shell>
      <command>#!/bin/bash
# =====================================================================
# deploy-instance.sh — build the ERP frontend on the Jenkins controller and deploy it to ANY
# LogShip instance (demo clones, blut24-style instances, …) over ssh.
#
# This is the shell step of the Jenkins job "erp-frontend-deploy-instance" (parameters
# TARGET_HOST, BRANCH; job config: reference/ci/jenkins-job-deploy-instance.xml). The job's
# Git SCM checks out BRANCH into the workspace; this script then:
#   1. builds with the Node version the deploy pins (nvm, jenkins user), heap 10 GB
#   2. compiles sqlite3 from source for the controller's glibc (= Debian 12 like the targets) and
#      copies the binary + the OCR/PDF runtime assets into .output/server (same as deploy-production.sh)
#   3. rsyncs the build to TARGET_HOST:/opt/erp-nuxt-frontend-incoming/
#   4. runs the target-side swap: keeps the instance's .env, data/ and SQLite DBs, moves the old tree to
#      /opt/erp-nuxt-frontend-rollback, regenerates the NUXT_* overrides (05-apply-env-overrides.sh —
#      runtimeConfig is baked at build time, see .claude/rules/ops/runtimeconfig-env-reload.md), restarts
#      pm2 from the ecosystem file, load-tests the native modules and checks /signin.
# Unlike production, the build does NOT run on the target (16 GB demo VMs cannot hold iDempiere + a
# 12 GB Nuxt build), so targets need no Node toolchain for deploys — only pm2/Node for running.
#
# Manual use (from the controller as user jenkins, inside a checkout):
#   TARGET_HOST=192.168.12.68 bash reference/ci/deploy-instance.sh
# =====================================================================
set -euo pipefail

TARGET_HOST="${TARGET_HOST:?TARGET_HOST (ip or hostname of the instance) required}"
TARGET_USER="${TARGET_USER:-root}"
NODE_VERSION="${NODE_VERSION:-v22.22.3}"
BUILD_HEAP_MB="${BUILD_HEAP_MB:-10240}"
LIVE_DIR="/opt/erp-nuxt-frontend"
INCOMING_DIR="/opt/erp-nuxt-frontend-incoming"
ROLLBACK_DIR="/opt/erp-nuxt-frontend-rollback"
SSH="ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new $TARGET_USER@$TARGET_HOST"

export NVM_DIR="${NVM_DIR:-$HOME/.nvm}"
# shellcheck disable=SC1090
[ -s "$NVM_DIR/nvm.sh" ] &amp;&amp; . "$NVM_DIR/nvm.sh"
nvm install "$NODE_VERSION" &gt;/dev/null
nvm use "$NODE_VERSION" &gt;/dev/null
echo "== node $(node -v) / npm $(npm -v) on $(hostname), target $TARGET_HOST, branch ${GIT_BRANCH:-?} ${GIT_COMMIT:-}"
$SSH "hostname &amp;&amp; test -d $LIVE_DIR &amp;&amp; test -f $LIVE_DIR/.env" || { echo "FATAL: $TARGET_HOST has no $LIVE_DIR/.env (not a LogShip instance?)"; exit 1; }

# --- 1. build ------------------------------------------------------------------------------
git_hash="$(git rev-parse --short HEAD 2&gt;/dev/null || echo unknown)"
build_time="$(date '+%d.%m.%Y %H:%M')"
echo "== npm ci"
npm ci --no-audit --no-fund 2&gt;&amp;1 | tail -3
echo "== build (heap ${BUILD_HEAP_MB} MB)"
export NODE_OPTIONS="--max-old-space-size=${BUILD_HEAP_MB}"
# GITHASH/BUILDTIME are read by nuxt.config.ts at build time (public.githash / buildtime)
GITHASH="$git_hash" BUILDTIME="$build_time" npm run build 2&gt;&amp;1 | tail -15
unset NODE_OPTIONS
[ -f .output/server/index.mjs ] || { echo "FATAL: build produced no .output/server/index.mjs"; exit 1; }
# service-worker cache version
if [ -f .output/public/sw-notifications.js ]; then
  sed -i "s/const CACHE_NAME = '[^']*'/const CACHE_NAME = 'logship-erp-$git_hash'/" .output/public/sw-notifications.js
fi

# --- 2. native modules + runtime assets (same rules as deploy-production.sh) --------------------
echo "== native modules"
npm rebuild better-sqlite3 &gt;/dev/null 2&gt;&amp;1 || true
npm rebuild sqlite3 --build-from-source 2&gt;&amp;1 | tail -2
SQLITE3_BIN="node_modules/sqlite3/build/Release/node_sqlite3.node"
[ -f "$SQLITE3_BIN" ] || { echo "FATAL: sqlite3 source build produced no binary"; exit 1; }
mkdir -p .output/server/node_modules/sqlite3/build/Release &amp;&amp; cp -f "$SQLITE3_BIN" .output/server/node_modules/sqlite3/build/Release/
[ -d node_modules/tesseract.js-core ] &amp;&amp; { mkdir -p .output/server/node_modules/tesseract.js-core; cp -rf node_modules/tesseract.js-core/. .output/server/node_modules/tesseract.js-core/; }
[ -d node_modules/pdfjs-dist/legacy/build ] &amp;&amp; { mkdir -p .output/server/node_modules/pdfjs-dist/legacy/build; cp -f node_modules/pdfjs-dist/legacy/build/pdf*.mjs .output/server/node_modules/pdfjs-dist/legacy/build/ 2&gt;/dev/null || true; }
( cd .output/server &amp;&amp; node -e 'require("./node_modules/sqlite3"); require("./node_modules/better-sqlite3"); console.log("  verified: sqlite3 + better-sqlite3 load OK (controller)")' )

# --- 3. ship -------------------------------------------------------------------------------------
echo "== rsync to $TARGET_HOST:$INCOMING_DIR"
$SSH "mkdir -p $INCOMING_DIR"
# anchored excludes (leading /): the top-level node_modules stays home, .output/server/node_modules must travel
rsync -a --delete --exclude /node_modules --exclude /.git --exclude /.nuxt --exclude /.env --exclude '/data/' --exclude /android \
      --exclude '/reference/' -e "ssh -o BatchMode=yes" ./ "$TARGET_USER@$TARGET_HOST:$INCOMING_DIR/"
# GITHASH/BUILDTIME into the instance .env happen on the target (its .env is kept)

# --- 4. swap on the target ----------------------------------------------------------------------
echo "== deploy on target"
$SSH "GIT_HASH='$git_hash' BUILD_TIME='$build_time' NODE_VERSION='$NODE_VERSION' bash -s" &lt;&lt;'TARGET'
set -euo pipefail; export LANG=C.UTF-8
LIVE=/opt/erp-nuxt-frontend; IN=/opt/erp-nuxt-frontend-incoming; RB=/opt/erp-nuxt-frontend-rollback
export NVM_DIR=/root/.nvm; . "$NVM_DIR/nvm.sh" &gt;/dev/null
NODE_BIN="$NVM_DIR/versions/node/$NODE_VERSION/bin/node"; [ -x "$NODE_BIN" ] || { nvm install "$NODE_VERSION" &gt;/dev/null; }
export PATH="$NVM_DIR/versions/node/$NODE_VERSION/bin:$PATH"
[ -f "$IN/.output/server/index.mjs" ] || { echo "FATAL: incoming build missing"; exit 1; }
# native modules must load on THIS host before anything is touched
( cd "$IN/.output/server" &amp;&amp; "$NODE_BIN" -e 'require("./node_modules/sqlite3"); require("./node_modules/better-sqlite3"); console.log("  verified: native modules load on target")' )
# keep instance state: .env + data/ (SQLite DBs, uploads, signing packages, …)
cp -p "$LIVE/.env" "$IN/.env"
mkdir -p "$IN/data"; [ -d "$LIVE/data" ] &amp;&amp; cp -a "$LIVE/data/." "$IN/data/"
sed -i -E "s|^GITHASH=.*|GITHASH=$GIT_HASH|; s|^BUILDTIME=.*|BUILDTIME=$BUILD_TIME|" "$IN/.env"
# runtimeConfig overrides for THIS .env (values are baked into the bundle with the controller's env)
if [ -x /root/template-cleanup/05-apply-env-overrides.sh ]; then
  /root/template-cleanup/05-apply-env-overrides.sh "$IN/.env" "$IN/nuxt.config.ts" | tail -1
else
  echo "  (05-apply-env-overrides.sh not on this host — NUXT_* overrides unchanged)"
fi
# swap
pm2 delete erpfrontend &gt;/dev/null 2&gt;&amp;1 || true
rm -rf "$RB"; mv "$LIVE" "$RB"; mv "$IN" "$LIVE"
# ecosystem interpreter path follows the pinned Node
sed -i -E "s|/root/.nvm/versions/node/v[0-9.]+/bin/node|$NODE_BIN|" "$LIVE/ecosystem.config.cjs"
cd "$LIVE" &amp;&amp; pm2 start ecosystem.config.cjs --only erpfrontend --update-env --interpreter "$NODE_BIN" &gt;/dev/null &amp;&amp; pm2 save --force &gt;/dev/null
# (|| true: a refused connection makes curl exit 7, which would abort the script under set -e)
for i in $(seq 1 40); do code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:3001/signin || true); [ "${code:-000}" = 200 ] &amp;&amp; break; sleep 3; done
echo "  /signin http ${code:-000} (build $GIT_HASH, $BUILD_TIME)"
[ "${code:-000}" = 200 ] || { echo "FATAL: frontend not answering — rolling back"; pm2 delete erpfrontend &gt;/dev/null 2&gt;&amp;1 || true; rm -rf "$LIVE"; mv "$RB" "$LIVE"; cd "$LIVE" &amp;&amp; pm2 start ecosystem.config.cjs --only erpfrontend --update-env &gt;/dev/null; pm2 save --force &gt;/dev/null; exit 1; }
echo "DEPLOY_OK"
TARGET
echo "== done: $TARGET_HOST runs $git_hash ($build_time)"
</command>
      <configuredLocalRules/>
    </hudson.tasks.Shell>
  </builders>
  <publishers/>
  <buildWrappers/>
</project>
