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
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:
+103
-13
@@ -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: |
|
||||
|
||||
Reference in New Issue
Block a user