initialising for ci-cd pipeline
CI/CD - ERP Core / scripts (pull_request) Failing after 12m21s
CI/CD - ERP Core / e2e-typecheck (pull_request) Failing after 13m13s
CI/CD - ERP Core / frontend (pull_request) Failing after 14m5s
CI/CD - ERP Core / backend (pull_request) Failing after 14m57s
CI/CD - ERP Core / detect (pull_request) Failing after 13m42s
CI/CD - ERP Core / Build & Deploy (pull_request) Failing after 12m47s
CI/CD - ERP Core / ci-ok (pull_request) Failing after 14m58s

This commit is contained in:
2026-09-16 11:53:55 +05:30
parent 666979cf22
commit 8e472ac996
48 changed files with 2224 additions and 62 deletions
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# ci-cd.yml › backend — build, unit tests and the "entity changed but no migration" check.
# The dotnet work runs inside a capped SDK container. ASPNETCORE_ENVIRONMENT=Development is set
# explicitly for the EF step: `dotnet ef` boots Program.cs, and outside Development the startup
# config check would (correctly) refuse to run without the deployment secrets CI doesn't have.
# shellcheck source=common.sh
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
z="$(selinux_mount_suffix)"
with_build_lock
log "backend checks (capped: 2 CPU / 1500 MB)"
# The inner script is single-quoted on purpose: it expands inside the container, not here.
# shellcheck disable=SC2016
docker run --rm --cpus=2 --cpu-shares=512 --memory=1500m \
-e DOTNET_CLI_TELEMETRY_OPTOUT=1 -e DOTNET_NOLOGO=1 -e EF_VERSION="$DOTNET_EF_VERSION" \
-e ASPNETCORE_ENVIRONMENT=Development \
-v "$PWD/Backend:/src$z" \
-v erp-nuget:/root/.nuget/packages \
-v erp-dotnet-tools:/root/.dotnet/tools \
-w /src "$DOTNET_SDK_IMAGE" bash -ec '
dotnet restore ERPCore.sln
dotnet build ERPCore.sln -c Release --no-restore
dotnet test ERPCore.Tests/ERPCore.Tests.csproj -c Release --no-build
# Program.cs suppresses PendingModelChangesWarning at runtime, so this is the only guard
# against an entity/configuration change merged without its migration. --no-build is safe
# here: the assembly was built from this exact source two commands ago.
dotnet tool update --global dotnet-ef --version "$EF_VERSION" >/dev/null
export PATH="$PATH:/root/.dotnet/tools"
dotnet ef migrations has-pending-model-changes \
--project ERPCore/ERPCore.csproj --configuration Release --no-build
'
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env bash
# ci-cd.yml › detect — which checks does this pull request need?
# Compares the PR head's folder tree hashes with the target branch's. Tree hashes are content
# addressed, so this needs no merge-base and works on depth-1 clones.
# shellcheck source=common.sh
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
: "${BASE_REF:?BASE_REF (the PR target branch) is required}"
base="refs/remotes/origin/$BASE_REF"
git_remote fetch -q --depth 1 "$REPO_GIT_URL" "+refs/heads/$BASE_REF:$base"
# changed <path...> — succeeds if any path differs between the target branch and HEAD.
changed() {
local p
for p in "$@"; do
if [[ "$(tree_hash "$p")" != "$(tree_hash "$p" "$base")" ]]; then return 0; fi
done
return 1
}
flag() { if changed "$@"; then echo true; else echo false; fi; }
set_output backend "$(flag "$BACKEND_PATH" "$BACKEND_TESTS_PATH" Backend/ERPCore.sln)"
set_output frontend "$(flag "$FRONTEND_PATH")"
set_output e2e "$(flag "$E2E_PATH")"
set_output scripts "$(flag deploy .gitea)"
set_output frontend_hash "$(tree_hash "$FRONTEND_PATH")"
+131
View File
@@ -0,0 +1,131 @@
#!/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 <test|release> — per-environment values. Ports, unit/container names and database
# names must match the server (systemd unit, Apache vhosts, /etc/erpcore/<env>.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 <branch> — the environment a push to <branch> deploys to.
env_for_branch() {
case "$1" in
# ON HOLD — test environment: Dev) echo test ;;
production) echo release ;;
*) return 1 ;;
esac
}
# tree_hash <path> [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 <side> <hash> <result> — 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 <name> <value> — 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/<hash>.tmp or image tag if that is ever raised.
with_build_lock() {
mkdir -p "$ERP_ROOT"
exec 9>"$BUILD_LOCK"
flock 9
}
# git_remote <git args...> — 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
}
+53
View File
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# ci-cd.yml › backend — flag destructive EF migrations introduced by this pull request.
#
# A failed deploy rolls the backend binaries back but never the database, so every release must
# leave the PREVIOUS backend working on the NEW schema: add tables/columns now, drop or rename
# them in a later release (expand → contract, docs/40-DEPLOYMENT.md §8).
# into Dev → warning
# into production → failure, unless the PR title contains [allow-destructive-migration]
# shellcheck source=common.sh
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
: "${BASE_REF:?BASE_REF (the PR target branch) is required}"
base="refs/remotes/origin/$BASE_REF"
git_remote fetch -q --depth 1 "$REPO_GIT_URL" "+refs/heads/$BASE_REF:$base"
# Two-dot tree diff: needs only the two commits, not their history (depth-1 friendly).
mapfile -t files < <(
git diff --name-only --diff-filter=AM "$base" HEAD -- "$MIGRATIONS_PATH" |
grep -E '\.cs$' | grep -vE '\.Designer\.cs$|ModelSnapshot\.cs$' || true
)
if ((${#files[@]} == 0)); then
log "no new or changed migrations"
exit 0
fi
hits=""
for f in "${files[@]}"; do
# Only Up() matters — every Down() legitimately drops what its Up() created.
found="$(awk '
/override void Up\(/ { up = 1 }
/override void Down\(/ { up = 0 }
up && /migrationBuilder\.(DropTable|DropColumn|RenameColumn|RenameTable|DropSchema|DropSequence)\(/ {
printf " %s:%d: %s\n", FILENAME, FNR, $0
}' "$f")"
hits+="$found"
done
if [[ -z "$hits" ]]; then
log "checked ${#files[@]} migration file(s): additive only"
exit 0
fi
printf '%s\n' "$hits"
msg="destructive operation in a migration's Up() — keep the previous release working on the new schema: expand now, contract in a later release (docs/40-DEPLOYMENT.md §8)"
if [[ "$BASE_REF" == production ]]; then
if [[ "${PR_TITLE:-}" == *"[allow-destructive-migration]"* ]]; then
warn "$msg — allowed by the [allow-destructive-migration] PR title override"
exit 0
fi
fail "$msg"
fi
warn "$msg"
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# ci-cd.yml › Build & Deploy › Verify backend — wait until erpcore.service answers /health, and fail the job otherwise.
#
# EF migrations + seeding run BEFORE the app starts listening, so a slow start is normal. Only a
# crash (unit failed, or systemd auto-restarted it) or the ceiling counts as failure — a crashed
# service fails within seconds, and a slow but live migration is never cut short by a tight timer.
#
# Env: SERVICE (erpcore.service) · BACKEND_PORT (5220) · HEALTH_TIMEOUT seconds (300)
# RESTARTS_AT_START — NRestarts captured right after `systemctl start` (the Start step exports it)
set -euo pipefail
service="${SERVICE:-erpcore.service}"
port="${BACKEND_PORT:-5220}"
timeout="${HEALTH_TIMEOUT:-300}"
restarts_at_start="${RESTARTS_AT_START:-$(systemctl show -p NRestarts --value "$service")}"
show_journal() { journalctl -u "$service" -n 80 --no-pager || true; }
deadline=$((SECONDS + timeout))
while ((SECONDS < deadline)); do
if curl -fsS --max-time 5 -o /dev/null "http://127.0.0.1:$port/health" 2>/dev/null; then
echo "✅ $service is healthy (after ${SECONDS}s)"
exit 0
fi
state="$(systemctl show -p ActiveState --value "$service")"
restarts="$(systemctl show -p NRestarts --value "$service")"
if [[ "$state" == failed || "$restarts" != "$restarts_at_start" ]]; then
echo "❌ $service crashed during startup (state=$state, restarts $restarts_at_start → $restarts)" >&2
show_journal
exit 1
fi
sleep 3
done
echo "❌ $service did not answer /health within ${timeout}s (still starting? DB reachable?)" >&2
show_journal
exit 1