Build the ERP frontend on the Jenkins controller and deploy it to ANY LogShip instance (demo clones etc.) over ssh.
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.
Keeps the instance .env + data/, regenerates NUXT_* overrides, pm2 restart from ecosystem, rollback on failed health check.
false
-1
5
-1
-1
false
TARGET_HOST
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)
192.168.12.68
true
BRANCH
Git branch to build (dev, main, feature/…)
dev
true
2
https://github.com/younex1/erp-nuxt-frontend.git
younex1
*/${BRANCH}
false
built-in
false
false
false
false
__BUILD_TOKEN__
false
#!/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" ] && . "$NVM_DIR/nvm.sh"
nvm install "$NODE_VERSION" >/dev/null
nvm use "$NODE_VERSION" >/dev/null
echo "== node $(node -v) / npm $(npm -v) on $(hostname), target $TARGET_HOST, branch ${GIT_BRANCH:-?} ${GIT_COMMIT:-}"
$SSH "hostname && test -d $LIVE_DIR && 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>/dev/null || echo unknown)"
build_time="$(date '+%d.%m.%Y %H:%M')"
echo "== npm ci"
npm ci --no-audit --no-fund 2>&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>&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 >/dev/null 2>&1 || true
npm rebuild sqlite3 --build-from-source 2>&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 && cp -f "$SQLITE3_BIN" .output/server/node_modules/sqlite3/build/Release/
[ -d node_modules/tesseract.js-core ] && { 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 ] && { 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>/dev/null || true; }
( cd .output/server && 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" <<'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" >/dev/null
NODE_BIN="$NVM_DIR/versions/node/$NODE_VERSION/bin/node"; [ -x "$NODE_BIN" ] || { nvm install "$NODE_VERSION" >/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" && "$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" ] && 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 >/dev/null 2>&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" && pm2 start ecosystem.config.cjs --only erpfrontend --update-env --interpreter "$NODE_BIN" >/dev/null && pm2 save --force >/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 ] && 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 >/dev/null 2>&1 || true; rm -rf "$LIVE"; mv "$RB" "$LIVE"; cd "$LIVE" && pm2 start ecosystem.config.cjs --only erpfrontend --update-env >/dev/null; pm2 save --force >/dev/null; exit 1; }
echo "DEPLOY_OK"
TARGET
echo "== done: $TARGET_HOST runs $git_hash ($build_time)"