change in to merge
CI/CD - ERP Core / detect (pull_request) Failing after 13m11s
CI/CD - ERP Core / ci-ok (pull_request) Has been cancelled
CI/CD - ERP Core / Build & Deploy (pull_request) Failing after 12m17s
CI/CD - ERP Core / backend (pull_request) Has been cancelled
CI/CD - ERP Core / frontend (pull_request) Has been cancelled
CI/CD - ERP Core / e2e-typecheck (pull_request) Has been cancelled
CI/CD - ERP Core / scripts (pull_request) Has been cancelled

This commit is contained in:
2026-09-16 23:48:07 +05:30
parent c7cf63e7de
commit 2a7a44cc27
6 changed files with 146 additions and 48 deletions
+103 -13
View File
@@ -36,7 +36,7 @@ on:
workflow_dispatch:
inputs:
deploy_backend:
description: "Deploy the backend (after editing /etc/erpcore/erpcore.env, or after a failed deploy)"
description: "Deploy the backend (after changing the config in this file's env: block, or after a failed deploy)"
type: boolean
default: false
deploy_frontend:
@@ -236,14 +236,28 @@ jobs:
PUBLISH_DIR: /home/erp/core/publish
BUILD_DIR: /tmp/erp-deploy
SERVICE: erpcore.service
SERVICE_USER: erp
SERVICE_USER: erpuser
BACKEND_PORT: "5220"
ENV_FILE: /etc/erpcore/erpcore.env
DB_NAME: ERPCore
DB_USER: erpcore
BACKUP_DIR: /var/backups/erpcore
WEB_IMAGE: erp-web:latest
WEB_CONTAINER: erp-web
WEB_PORT: "3020"
# ── Backend runtime config ────────────────────────────────────────────────────────────
# There is no /etc/erpcore/erpcore.env. These values are the single source of truth: the
# "Install unit + write config" step writes them into a systemd drop-in on every backend
# deploy. Change a setting here, then merge or use "Run workflow" with deploy_backend.
# The two secrets (DB password, GL API key) come from Gitea secrets, never from this file.
AUTHHEX_BASE_URL: "http://127.0.0.1:5433"
GL_BASE_URL: "http://127.0.0.1:5433/api/v1/"
STORAGE_ROOT: /var/lib/erpcore/hr-documents
LOG_PATH: /var/log/erpcore/erpcore-.log
SEED_DEMO_DATA: "false"
# Door policy: blank = any valid AuthHex token is admitted. Set the real AuthHex codes to
# restrict ERP to one user type / role (Infra/Auth/JwtAuthExtensions.cs).
AUTH_REQUIRED_USER_TYPE: ""
AUTH_REQUIRED_ROLE: ""
steps:
- name: Checkout code
@@ -324,25 +338,60 @@ jobs:
echo "Backup: $file"
find "$BACKUP_DIR" -maxdepth 1 -name '*.dump' -mtime +30 -delete
- name: Guard data paths
- name: Install unit + write config
if: steps.changes.outputs.backend == 'true'
env:
DB_PASSWORD: ${{ secrets.ERP_DB_PASSWORD }}
GL_API_KEY: ${{ secrets.ERP_GL_API_KEY }}
run: |
# Config lives in this workflow's env: block, not in a file on the server. It is written
# to a systemd drop-in here, before the service is stopped, so a bad value fails the job
# while the OLD release is still serving.
case "$AUTHHEX_BASE_URL$GL_BASE_URL" in
*CHANGE_ME*) echo "❌ AUTHHEX_BASE_URL / GL_BASE_URL still hold CHANGE_ME — set the real ports in ci-cd.yml (run 'ss -ltnp' on the VPS)"; exit 1 ;;
esac
[ -n "$DB_PASSWORD" ] || { echo "❌ Gitea secret ERP_DB_PASSWORD is not set"; exit 1; }
[ -n "$GL_API_KEY" ] || { echo "❌ Gitea secret ERP_GL_API_KEY is not set"; exit 1; }
case "$DB_PASSWORD" in
*";"*) echo "❌ ERP_DB_PASSWORD contains ';' — Npgsql would truncate the connection string there. Use a password without ';'"; exit 1 ;;
esac
# "Copy to deploy folder" empties the publish folder, so uploads and logs must live elsewhere.
[ -r "$ENV_FILE" ] || { echo "❌ $ENV_FILE not found"; exit 1; }
publish="$(realpath -m "$PUBLISH_DIR")"
for key in FileStorage__RootPath LogFile__Path; do
value="$(sed -n "s/^$key=//p" "$ENV_FILE" | tail -n 1 | tr -d '"')"
[ -n "$value" ] || { echo "❌ $key is not set in $ENV_FILE"; exit 1; }
case "$value" in
for path in "$STORAGE_ROOT" "$LOG_PATH"; do
case "$path" in
/*) ;;
*) echo "❌ $key=$value is relative — it would resolve inside $PUBLISH_DIR"; exit 1 ;;
*) echo "❌ $path is relative — it would resolve inside $PUBLISH_DIR"; exit 1 ;;
esac
case "$(realpath -m "$value")/" in
"$publish"/*) echo "❌ $key=$value is inside $PUBLISH_DIR, which every deploy wipes"; exit 1 ;;
case "$(realpath -m "$path")/" in
"$publish"/*) echo "❌ $path is inside $PUBLISH_DIR, which every deploy wipes"; exit 1 ;;
esac
echo "ok $key=$value"
echo "ok $path"
done
# The unit is installed from the repo, so editing deploy/systemd/erpcore.service and
# deploying is enough — no manual copy step on the server.
install -m 644 deploy/systemd/erpcore.service /etc/systemd/system/erpcore.service
install -d -m 755 /etc/systemd/system/erpcore.service.d
conf=/etc/systemd/system/erpcore.service.d/10-erpcore-env.conf
# Values are quoted: systemd would otherwise split a password containing spaces.
umask 077
# Only the listening URL. Everything else is app configuration and lands in
# appsettings.Production.json (written after the copy step) — ASPNETCORE_URLS is host
# configuration, the one setting appsettings cannot reliably provide.
cat > "$conf" <<EOF
[Service]
Environment="ASPNETCORE_URLS=http://127.0.0.1:$BACKEND_PORT"
EOF
chmod 600 "$conf"
systemctl daemon-reload
# Idempotent: creates the multi-user.target.wants symlink so the app survives a reboot.
systemctl enable "$SERVICE"
echo "✅ unit + drop-in installed ($(grep -c '^Environment=' "$conf") settings; values not printed)"
# Data dirs must exist and belong to the service user (Serilog fails silently otherwise).
install -d -m 750 -o "$SERVICE_USER" -g "$SERVICE_USER" "$STORAGE_ROOT" "$(dirname "$LOG_PATH")"
- name: Stop service
if: steps.changes.outputs.backend == 'true'
run: systemctl stop "$SERVICE" || true
@@ -362,6 +411,47 @@ jobs:
find "$PUBLISH_DIR" -type f -exec chmod 644 {} +
chmod +x "$PUBLISH_DIR/ERPCore"
# Written AFTER "Fix ownership" so it keeps mode 640 (root:erpuser) instead of being
# flattened to 644 — it holds the DB password and the GL API key.
- name: Write appsettings.Production.json
if: steps.changes.outputs.backend == 'true'
env:
DB_PASSWORD: ${{ secrets.ERP_DB_PASSWORD }}
GL_API_KEY: ${{ secrets.ERP_GL_API_KEY }}
run: |
# Deployed config lives in appsettings.Production.json, generated here from this
# workflow's env: block + the Gitea secrets. It is NOT in git (.gitignore ignores
# appsettings.*.json) and is rewritten on every backend deploy, which is why the
# publish folder being wiped does not matter. It MERGES over appsettings.json, so
# Auth:RsaPublicKeyXml, Issuer, Audience and FileStorage:MaxSizeBytes still come
# from the committed base file.
conf="$PUBLISH_DIR/appsettings.Production.json"
# python3 emits the JSON so a password containing " or \ is escaped correctly.
python3 - > "$conf" <<'PY'
import json, os
e = os.environ
print(json.dumps({
"ConnectionStrings": {
"DefaultConnection":
"Host=127.0.0.1;Port=5432;Database=%s;Username=%s;Password=%s"
% (e["DB_NAME"], e["DB_USER"], e["DB_PASSWORD"]),
},
"AuthHex": {"BaseUrl": e["AUTHHEX_BASE_URL"]},
"GeneralLedgerService": {"BaseUrl": e["GL_BASE_URL"], "ApiKey": e["GL_API_KEY"]},
"Auth": {
"RequiredUserTypeCode": e["AUTH_REQUIRED_USER_TYPE"],
"RequiredRoleCode": e["AUTH_REQUIRED_ROLE"],
},
"FileStorage": {"RootPath": e["STORAGE_ROOT"]},
"LogFile": {"Path": e["LOG_PATH"]},
"Seed": {"DemoData": e["SEED_DEMO_DATA"] == "true"},
}, indent=2))
PY
chown "root:$SERVICE_USER" "$conf"
chmod 640 "$conf"
python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$conf"
echo "✅ appsettings.Production.json written ($(wc -c < "$conf") bytes, mode 640, values not printed)"
- name: Start service
if: steps.changes.outputs.backend == 'true'
run: |