#!/usr/bin/env bash # Shared settings + helpers for the ERP-Core CI/CD scripts. Sourced by the other scripts, never # run directly. Runbook: docs/40-DEPLOYMENT.md (values here must match the server bootstrap, §4). # shellcheck disable=SC2034 # variables are consumed by the scripts that source this file set -euo pipefail # ── Repository (folders whose git tree hash decides what is checked / built / deployed) ───── BACKEND_PATH="Backend/ERPCore" BACKEND_TESTS_PATH="Backend/ERPCore.Tests" MIGRATIONS_PATH="Backend/ERPCore/Migrations" FRONTEND_PATH="Frontend/erp-system" E2E_PATH="Testing/e2e" GITEA_URL="${GITEA_URL:-https://gitea.hexdive.com}" REPO_SLUG="New_REP_SYSTEM/ERP-core" REPO_GIT_URL="$GITEA_URL/$REPO_SLUG.git" # ── Server layout of the ON-HOLD two-environment design (deploy/on-hold/). Active production does # not use these paths — see .gitea/workflows/ci-cd.yml (erpcore.service, /home/erp/core/publish). # ERP_ROOT still hosts the build lock taken by ci-backend.sh. ───────────────────────────────── ERP_ROOT="${ERP_ROOT:-/opt/erpcore}" RELEASES_DIR="$ERP_ROOT/releases/backend" BUILD_LOCK="$ERP_ROOT/.build.lock" BACKUP_DIR="${ERP_BACKUP_DIR:-/var/backups/erpcore}" E2E_REPORTS_DIR="${ERP_E2E_REPORTS_DIR:-/var/lib/erpcore/e2e-reports}" # ── Build images (builds run in CPU/memory-capped containers on the shared 8 GB VPS) ─────── DOTNET_SDK_IMAGE="mcr.microsoft.com/dotnet/sdk:10.0" NODE_IMAGE="node:24-alpine" WEB_IMAGE_REPO="erp-web" DOTNET_EF_VERSION="10.0.9" # keep in step with Microsoft.EntityFrameworkCore.* in ERPCore.csproj log() { printf '[erp] %s\n' "$*"; } warn() { printf '[erp] WARNING: %s\n' "$*" >&2; } fail() { printf '[erp] ERROR: %s\n' "$*" >&2 exit 1 } # load_env — per-environment values. Ports, unit/container names and database # names must match the server (systemd unit, Apache vhosts, /etc/erpcore/.env). # While both branches are on hold every call fails, so the lines after the case are unreachable. # shellcheck disable=SC2317 load_env() { ERP_ENV="$1" case "$ERP_ENV" in # ON HOLD — test environment (deploy/on-hold/README.md, docs/40-DEPLOYMENT.md "On hold"). # test) # SERVICE="erpcore@test" # BACKEND_PORT=5230 # WEB_CONTAINER="erp-web-test" # WEB_PORT=3030 # DB_NAME="ERPCoreStaging" # BACKUP_POLICY="on-migration" # HEALTH_TIMEOUT=90 # PUBLIC_URL="https://test-erp.hexdive.com" # ;; # ON HOLD — the two-environment layout for release (/opt/erpcore, erpcore@release). NOT how # production runs today: the active deploy is .gitea/workflows/ci-cd.yml → erpcore.service + # /home/erp/core/publish. Nothing active calls load_env. # release) # SERVICE="erpcore@release" # BACKEND_PORT=5220 # WEB_CONTAINER="erp-web" # WEB_PORT=3020 # DB_NAME="ERPCore" # BACKUP_POLICY="always" # HEALTH_TIMEOUT=300 # PUBLIC_URL="https://erp.hexdive.com" # ;; *) fail "unknown environment '$ERP_ENV' (expected test or release)" ;; esac ENV_DIR="$ERP_ROOT/$ERP_ENV" STATE_DIR="$ENV_DIR/.deployed" CURRENT_LINK="$ENV_DIR/backend/current" } # env_for_branch — the environment a push to deploys to. env_for_branch() { case "$1" in # ON HOLD — test environment: Dev) echo test ;; production) echo release ;; *) return 1 ;; esac } # tree_hash [rev] — git tree (or blob) id of a path. Identical folder content gives an # identical hash no matter which commit, branch or merge produced it — that is what lets a # Dev → production release reuse the build test already made. Empty if the path is absent. tree_hash() { git rev-parse --verify --quiet "${2:-HEAD}:$1" 2>/dev/null || true; } read_state() { cat "$STATE_DIR/$1" 2>/dev/null || true; } write_state() { mkdir -p "$STATE_DIR" printf '%s\n' "$2" >"$STATE_DIR/$1.tmp" mv -f "$STATE_DIR/$1.tmp" "$STATE_DIR/$1" } # record_history — append-only audit of every deploy attempt. record_history() { mkdir -p "$STATE_DIR" printf '%s\t%s\t%s\t%s\tsha=%s\trun=%s\n' "$(date -u +%FT%TZ)" "$1" "$2" "$3" \ "${GITHUB_SHA:-manual}" "${GITHUB_RUN_NUMBER:-}" >>"$STATE_DIR/history.log" } # set_output — Gitea Actions step output (only logged when run by hand). set_output() { if [[ -n "${GITHUB_OUTPUT:-}" ]]; then printf '%s=%s\n' "$1" "$2" >>"$GITHUB_OUTPUT"; fi log "$1=$2" } # with_build_lock — serialize heavy work on this host. The runner's capacity is 1 today; this # keeps two jobs from writing the same releases/.tmp or image tag if that is ever raised. with_build_lock() { mkdir -p "$ERP_ROOT" exec 9>"$BUILD_LOCK" flock 9 } # git_remote — run git with the ERPSECRET token as an HTTP header (never echoed, # never written to .git/config). git_remote() { : "${ERP_GIT_TOKEN:?ERP_GIT_TOKEN (secrets.ERPSECRET) is required}" local basic basic="$(printf 'oauth2:%s' "$ERP_GIT_TOKEN" | base64 | tr -d '\n')" git -c "http.extraHeader=Authorization: Basic $basic" "$@" } # selinux_mount_suffix — ":z" relabels bind mounts on SELinux hosts (AlmaLinux) so containers # can read them; empty elsewhere (e.g. a developer laptop). selinux_mount_suffix() { if command -v selinuxenabled >/dev/null 2>&1 && selinuxenabled; then echo ":z"; fi }