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
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:
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy.yml › backend — build (or reuse) the self-contained release for BACKEND_HASH into
|
||||
# /opt/erpcore/releases/backend/<hash>. Releases are shared by test and release, so a
|
||||
# Dev → production merge that carries the same Backend/ERPCore tree skips the build entirely.
|
||||
# shellcheck source=common.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
: "${BACKEND_HASH:?BACKEND_HASH is required}"
|
||||
target="$RELEASES_DIR/$BACKEND_HASH"
|
||||
|
||||
reuse_if_built() {
|
||||
if [[ -x "$target/ERPCore" ]]; then
|
||||
log "backend $BACKEND_HASH already built → reused ($target)"
|
||||
set_output reused true
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
reuse_if_built
|
||||
with_build_lock
|
||||
reuse_if_built # another job may have built it while we waited for the lock
|
||||
|
||||
# Only the exact tree can be built. A rollback hash whose release was pruned cannot be recreated
|
||||
# from a different checkout — fail instead of deploying something else under that name.
|
||||
[[ "$(tree_hash "$BACKEND_PATH")" == "$BACKEND_HASH" ]] ||
|
||||
fail "no release for $BACKEND_HASH, and this checkout's $BACKEND_PATH is a different tree (pruned rollback target?)"
|
||||
|
||||
mkdir -p "$RELEASES_DIR"
|
||||
tmp="$target.tmp"
|
||||
rm -rf "$tmp"
|
||||
mkdir -p "$tmp"
|
||||
z="$(selinux_mount_suffix)"
|
||||
|
||||
log "building backend $BACKEND_HASH (capped: 2 CPU / 1500 MB)"
|
||||
docker run --rm --cpus=2 --cpu-shares=512 --memory=1500m \
|
||||
-e DOTNET_CLI_TELEMETRY_OPTOUT=1 -e DOTNET_NOLOGO=1 \
|
||||
-v "$PWD/Backend:/src$z" \
|
||||
-v "$tmp:/out$z" \
|
||||
-v erp-nuget:/root/.nuget/packages \
|
||||
-w /src "$DOTNET_SDK_IMAGE" \
|
||||
dotnet publish ERPCore/ERPCore.csproj -c Release -r linux-x64 --self-contained true -o /out
|
||||
|
||||
[[ -x "$tmp/ERPCore" ]] || fail "publish finished but $tmp/ERPCore is missing"
|
||||
|
||||
# Root-owned and read-only for the service user: the app never writes into its own release
|
||||
# (uploads and logs live under /var/lib/erpcore and /var/log/erpcore).
|
||||
chown -R root:root "$tmp"
|
||||
chmod -R u=rwX,go=rX "$tmp"
|
||||
chmod 755 "$tmp/ERPCore"
|
||||
mv "$tmp" "$target"
|
||||
if command -v restorecon >/dev/null 2>&1; then restorecon -R "$target"; fi
|
||||
|
||||
log "backend release ready: $target"
|
||||
set_output reused false
|
||||
@@ -0,0 +1,63 @@
|
||||
#!/usr/bin/env bash
|
||||
# ci.yml › frontend and deploy.yml › frontend — build (or reuse) image erp-web:<FRONTEND_HASH>.
|
||||
#
|
||||
# `npm ci` + `next build` run in a capped node container; the image itself is runtime-only
|
||||
# (Frontend/erp-system/Dockerfile). The PR check builds the same tag with WITH_CHECKS=true
|
||||
# (type-check + lint first), so the deploy after a merge normally just reuses that image.
|
||||
# shellcheck source=common.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
: "${FRONTEND_HASH:?FRONTEND_HASH is required}"
|
||||
image="$WEB_IMAGE_REPO:$FRONTEND_HASH"
|
||||
with_checks="${WITH_CHECKS:-false}"
|
||||
app="$PWD/$FRONTEND_PATH"
|
||||
checks="npx tsc --noEmit && npx eslint ."
|
||||
|
||||
image_exists() { docker image inspect "$image" >/dev/null 2>&1; }
|
||||
image_checked() { [[ "$(docker image inspect -f '{{ index .Config.Labels "erp.checked" }}' "$image" 2>/dev/null)" == true ]]; }
|
||||
|
||||
# run_node <shell steps> — run steps in the frontend folder inside a capped node container.
|
||||
run_node() {
|
||||
local z
|
||||
z="$(selinux_mount_suffix)"
|
||||
docker run --rm --cpus=2 --cpu-shares=512 --memory=2500m \
|
||||
-e CI=true -e NEXT_TELEMETRY_DISABLED=1 -e NODE_OPTIONS=--max-old-space-size=2048 \
|
||||
-v "$app:/app$z" \
|
||||
-v erp-npm-cache:/root/.npm \
|
||||
-v erp-next-cache:/app/.next/cache \
|
||||
-w /app "$NODE_IMAGE" sh -ec "$1"
|
||||
}
|
||||
|
||||
with_build_lock
|
||||
|
||||
if image_exists; then
|
||||
if [[ "$with_checks" == true ]] && ! image_checked; then
|
||||
# Same content was built earlier by a deploy (no checks) — the PR still has to pass them.
|
||||
log "$image exists but was built without checks → running type-check + lint only"
|
||||
run_node "npm ci --no-audit --no-fund && $checks"
|
||||
fi
|
||||
log "$image already built → reused"
|
||||
set_output reused true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[[ "$(tree_hash "$FRONTEND_PATH")" == "$FRONTEND_HASH" ]] ||
|
||||
fail "no image for $FRONTEND_HASH, and this checkout's $FRONTEND_PATH is a different tree (pruned rollback target?)"
|
||||
|
||||
steps="npm ci --no-audit --no-fund"
|
||||
if [[ "$with_checks" == true ]]; then steps="$steps && $checks"; fi
|
||||
steps="$steps && npm run build"
|
||||
|
||||
log "building frontend $FRONTEND_HASH (checks=$with_checks, capped: 2 CPU / 2500 MB)"
|
||||
run_node "$steps"
|
||||
[[ -f "$app/.next/standalone/server.js" ]] ||
|
||||
fail "next build did not produce .next/standalone/server.js (is output: \"standalone\" set in next.config.ts?)"
|
||||
|
||||
docker build \
|
||||
--label "erp.frontend-tree=$FRONTEND_HASH" \
|
||||
--label "erp.source-sha=${GITHUB_SHA:-local}" \
|
||||
--label "erp.checked=$with_checks" \
|
||||
-t "$image" "$app"
|
||||
|
||||
log "frontend image ready: $image"
|
||||
set_output reused false
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy.yml › backend — switch an environment to BACKEND_HASH:
|
||||
# pg_dump (policy) → stop → repoint `current` → start → wait for /health
|
||||
# On failure the previous release is restored and the job fails (red).
|
||||
# The DATABASE is never rolled back — hence add-only migrations (docs/40-DEPLOYMENT.md §8).
|
||||
# shellcheck source=common.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
: "${ERP_ENV:?ERP_ENV is required}" "${BACKEND_HASH:?BACKEND_HASH is required}"
|
||||
load_env "$ERP_ENV"
|
||||
is_rollback="${IS_ROLLBACK:-false}"
|
||||
migrations_changed="${MIGRATIONS_CHANGED:-false}"
|
||||
|
||||
target="$RELEASES_DIR/$BACKEND_HASH"
|
||||
[[ -x "$target/ERPCore" ]] || fail "backend release not found: $target"
|
||||
|
||||
backup_database() {
|
||||
local pg_dump file
|
||||
pg_dump="$(command -v pg_dump || echo /usr/pgsql-16/bin/pg_dump)"
|
||||
[[ -x "$pg_dump" ]] || fail "pg_dump not found (install postgresql16 client tools or fix PATH)"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
file="$BACKUP_DIR/$ERP_ENV-$(date -u +%Y%m%dT%H%M%SZ)-${BACKEND_HASH:0:12}.dump"
|
||||
log "backing up database $DB_NAME → $file"
|
||||
runuser -u postgres -- "$pg_dump" -Fc "$DB_NAME" >"$file.partial"
|
||||
mv "$file.partial" "$file"
|
||||
}
|
||||
|
||||
# wait_healthy — succeeds once /health answers 200. EF migrations + seeding run BEFORE the app
|
||||
# starts listening, so a slow start is normal. Only a crash (unit failed / auto-restarted) or the
|
||||
# generous per-environment ceiling counts as failure: never cut a live migration on a tight timer.
|
||||
wait_healthy() {
|
||||
local deadline=$((SECONDS + HEALTH_TIMEOUT)) restarts_at_start state restarts
|
||||
restarts_at_start="$(systemctl show -p NRestarts --value "$SERVICE")"
|
||||
while ((SECONDS < deadline)); do
|
||||
if curl -fsS --max-time 5 -o /dev/null "http://127.0.0.1:$BACKEND_PORT/health" 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
state="$(systemctl show -p ActiveState --value "$SERVICE")"
|
||||
restarts="$(systemctl show -p NRestarts --value "$SERVICE")"
|
||||
if [[ "$state" == failed || "$restarts" != "$restarts_at_start" ]]; then
|
||||
warn "$SERVICE crashed during startup (state=$state, restarts $restarts_at_start → $restarts)"
|
||||
return 1
|
||||
fi
|
||||
sleep 3
|
||||
done
|
||||
warn "$SERVICE was not healthy within ${HEALTH_TIMEOUT}s"
|
||||
return 1
|
||||
}
|
||||
|
||||
switch_to() {
|
||||
systemctl stop "$SERVICE" || true
|
||||
mkdir -p "$(dirname "$CURRENT_LINK")"
|
||||
ln -sfn "$1" "$CURRENT_LINK"
|
||||
systemctl reset-failed "$SERVICE" 2>/dev/null || true
|
||||
systemctl start "$SERVICE" || warn "systemctl start $SERVICE returned an error"
|
||||
}
|
||||
|
||||
if [[ "$is_rollback" != true ]] && [[ "$BACKUP_POLICY" == always || "$migrations_changed" == true ]]; then
|
||||
backup_database
|
||||
else
|
||||
log "no database backup (policy=$BACKUP_POLICY, migrations changed=$migrations_changed, rollback=$is_rollback)"
|
||||
fi
|
||||
|
||||
previous="$(readlink "$CURRENT_LINK" 2>/dev/null || true)"
|
||||
log "deploying backend $BACKEND_HASH to $ERP_ENV (previous: ${previous:-none})"
|
||||
switch_to "$target"
|
||||
|
||||
if wait_healthy; then
|
||||
write_state backend "$BACKEND_HASH"
|
||||
if [[ "$is_rollback" != true && -n "${MIGRATIONS_HASH:-}" ]]; then write_state migrations "$MIGRATIONS_HASH"; fi
|
||||
if [[ "$is_rollback" == true ]]; then result=rollback; else result=deployed; fi
|
||||
record_history backend "$BACKEND_HASH" "$result"
|
||||
log "✅ backend $BACKEND_HASH is healthy on $ERP_ENV"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
journalctl -u "$SERVICE" -n 80 --no-pager || true
|
||||
record_history backend "$BACKEND_HASH" failed
|
||||
if [[ -n "$previous" && "$previous" != "$target" && -x "$previous/ERPCore" ]]; then
|
||||
warn "restoring previous release $previous"
|
||||
switch_to "$previous"
|
||||
if wait_healthy; then
|
||||
log "previous release is serving again"
|
||||
else
|
||||
warn "the previous release is not healthy either — investigate now"
|
||||
fi
|
||||
else
|
||||
warn "no previous release to restore; stopping $SERVICE"
|
||||
systemctl stop "$SERVICE" || true
|
||||
fi
|
||||
fail "backend deploy of $BACKEND_HASH to $ERP_ENV failed"
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy.yml › frontend — replace the environment's web container with erp-web:<FRONTEND_HASH>
|
||||
# and wait for it to serve /login. On failure the previously deployed image is started again
|
||||
# and the job fails.
|
||||
# shellcheck source=common.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
: "${ERP_ENV:?ERP_ENV is required}" "${FRONTEND_HASH:?FRONTEND_HASH is required}"
|
||||
load_env "$ERP_ENV"
|
||||
is_rollback="${IS_ROLLBACK:-false}"
|
||||
|
||||
docker image inspect "$WEB_IMAGE_REPO:$FRONTEND_HASH" >/dev/null 2>&1 ||
|
||||
fail "image $WEB_IMAGE_REPO:$FRONTEND_HASH not found (pruned?)"
|
||||
|
||||
start_web() {
|
||||
docker rm -f "$WEB_CONTAINER" >/dev/null 2>&1 || true
|
||||
docker run -d --name "$WEB_CONTAINER" \
|
||||
-p "127.0.0.1:$WEB_PORT:3000" \
|
||||
--memory=512m --cpus=1 \
|
||||
--restart unless-stopped \
|
||||
--label "erp.env=$ERP_ENV" \
|
||||
"$WEB_IMAGE_REPO:$1" >/dev/null
|
||||
}
|
||||
|
||||
wait_web() {
|
||||
local deadline=$((SECONDS + 30))
|
||||
while ((SECONDS < deadline)); do
|
||||
if curl -fsS --max-time 5 -o /dev/null "http://127.0.0.1:$WEB_PORT/login" 2>/dev/null; then return 0; fi
|
||||
if [[ "$(docker inspect -f '{{.State.Running}}' "$WEB_CONTAINER" 2>/dev/null)" != true ]]; then
|
||||
warn "$WEB_CONTAINER is not running"
|
||||
return 1
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
warn "$WEB_CONTAINER did not serve /login within 30s"
|
||||
return 1
|
||||
}
|
||||
|
||||
previous="$(read_state frontend)"
|
||||
log "deploying frontend $FRONTEND_HASH to $ERP_ENV (previous: ${previous:-none})"
|
||||
start_web "$FRONTEND_HASH"
|
||||
|
||||
if wait_web; then
|
||||
write_state frontend "$FRONTEND_HASH"
|
||||
if [[ "$is_rollback" == true ]]; then result=rollback; else result=deployed; fi
|
||||
record_history frontend "$FRONTEND_HASH" "$result"
|
||||
log "✅ frontend $FRONTEND_HASH is serving on $ERP_ENV"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
docker logs --tail 80 "$WEB_CONTAINER" || true
|
||||
record_history frontend "$FRONTEND_HASH" failed
|
||||
if [[ -n "$previous" && "$previous" != "$FRONTEND_HASH" ]] &&
|
||||
docker image inspect "$WEB_IMAGE_REPO:$previous" >/dev/null 2>&1; then
|
||||
warn "restoring previous frontend $previous"
|
||||
start_web "$previous"
|
||||
wait_web || warn "the previous frontend is not serving either — investigate now"
|
||||
fi
|
||||
fail "frontend deploy of $FRONTEND_HASH to $ERP_ENV failed"
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy.yml › detect — decide what this push deploys.
|
||||
#
|
||||
# A side deploys when its folder's tree hash differs from the hash last deployed SUCCESSFULLY to
|
||||
# this environment (stored on the server in /opt/erpcore/<env>/.deployed). So:
|
||||
# • docs / PROGRESS.md / smoke-script pushes deploy nothing,
|
||||
# • a failed deploy is retried by the next run (its stored hash never moved),
|
||||
# • force-pushes, multi-commit pushes and merges need no special handling.
|
||||
# Manual runs can force a side (config change) or pin an older hash (rollback).
|
||||
# shellcheck source=common.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
: "${REF_NAME:?REF_NAME (the pushed branch) is required}"
|
||||
env_name="$(env_for_branch "$REF_NAME")" ||
|
||||
fail "branch '$REF_NAME' is not deployable (Dev → test, production → release)"
|
||||
load_env "$env_name"
|
||||
|
||||
be_hash="$(tree_hash "$BACKEND_PATH")"
|
||||
fe_hash="$(tree_hash "$FRONTEND_PATH")"
|
||||
mig_hash="$(tree_hash "$MIGRATIONS_PATH")"
|
||||
[[ -n "$be_hash" && -n "$fe_hash" ]] || fail "could not compute tree hashes — is this an ERP-core checkout?"
|
||||
|
||||
be_rollback=false
|
||||
fe_rollback=false
|
||||
if [[ -n "${BACKEND_HASH_INPUT:-}" ]]; then be_hash="$BACKEND_HASH_INPUT" && be_rollback=true; fi
|
||||
if [[ -n "${FRONTEND_HASH_INPUT:-}" ]]; then fe_hash="$FRONTEND_HASH_INPUT" && fe_rollback=true; fi
|
||||
|
||||
be_deployed="$(read_state backend)"
|
||||
fe_deployed="$(read_state frontend)"
|
||||
be_changed=false
|
||||
fe_changed=false
|
||||
if [[ "$be_hash" != "$be_deployed" ]]; then be_changed=true; fi
|
||||
if [[ "$fe_hash" != "$fe_deployed" ]]; then fe_changed=true; fi
|
||||
|
||||
deploy_backend=$be_changed
|
||||
deploy_frontend=$fe_changed
|
||||
if [[ "${FORCE_BACKEND:-}" == true ]]; then deploy_backend=true; fi
|
||||
if [[ "${FORCE_FRONTEND:-}" == true ]]; then deploy_frontend=true; fi
|
||||
|
||||
migrations_changed=false
|
||||
if ! $be_rollback && [[ "$mig_hash" != "$(read_state migrations)" ]]; then migrations_changed=true; fi
|
||||
|
||||
# A Gitea release records real content changes on release only — not forced restarts or
|
||||
# rollbacks (those are in .deployed/history.log on the server).
|
||||
record_release=false
|
||||
if [[ "$env_name" == release ]] && ! $be_rollback && ! $fe_rollback && { $be_changed || $fe_changed; }; then
|
||||
record_release=true
|
||||
fi
|
||||
|
||||
log "environment $env_name — deployed now: backend ${be_deployed:-none}, frontend ${fe_deployed:-none}"
|
||||
set_output env "$env_name"
|
||||
set_output backend "$deploy_backend"
|
||||
set_output frontend "$deploy_frontend"
|
||||
set_output backend_hash "$be_hash"
|
||||
set_output frontend_hash "$fe_hash"
|
||||
set_output backend_rollback "$be_rollback"
|
||||
set_output frontend_rollback "$fe_rollback"
|
||||
set_output migrations_hash "$mig_hash"
|
||||
set_output migrations_changed "$migrations_changed"
|
||||
set_output record_release "$record_release"
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy.yml › e2e — Playwright suite against the TEST environment. Never release: the suite
|
||||
# creates vendors, items and POs on every run.
|
||||
# shellcheck source=common.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
: "${E2E_ADMIN_EMAIL:?secrets.E2E_ADMIN_EMAIL is required}" "${E2E_ADMIN_PASSWORD:?secrets.E2E_ADMIN_PASSWORD is required}"
|
||||
load_env test
|
||||
base_url="${E2E_BASE_URL:-$PUBLIC_URL}"
|
||||
|
||||
# The container's browsers must match the locked @playwright/test version exactly.
|
||||
pw_version="$(grep -A1 '"node_modules/@playwright/test"' "$E2E_PATH/package-lock.json" |
|
||||
sed -n 's/.*"version": "\([^"]*\)".*/\1/p')"
|
||||
[[ -n "$pw_version" ]] || fail "could not read the locked @playwright/test version"
|
||||
image="mcr.microsoft.com/playwright:v$pw_version-noble"
|
||||
z="$(selinux_mount_suffix)"
|
||||
|
||||
with_build_lock # Chromium is the second-heaviest workload on the box — never overlap a build
|
||||
|
||||
log "E2E against $base_url with $image (capped: 2 CPU / 1500 MB)"
|
||||
status=0
|
||||
docker run --rm --cpus=2 --cpu-shares=512 --memory=1500m --ipc=host \
|
||||
-e CI=true -e E2E_BASE_URL="$base_url" -e E2E_API_URL="$base_url" \
|
||||
-e E2E_ADMIN_EMAIL -e E2E_ADMIN_PASSWORD \
|
||||
-v "$PWD/$E2E_PATH:/e2e$z" -w /e2e "$image" \
|
||||
bash -ec 'npm ci --no-audit --no-fund && npx playwright test' || status=$?
|
||||
|
||||
if [[ -d "$E2E_PATH/playwright-report" ]]; then
|
||||
report_dir="$E2E_REPORTS_DIR/$(date -u +%Y%m%dT%H%M%SZ)-run${GITHUB_RUN_NUMBER:-manual}"
|
||||
mkdir -p "$report_dir"
|
||||
cp -r "$E2E_PATH/playwright-report" "$report_dir/"
|
||||
if [[ -d "$E2E_PATH/test-results" ]]; then cp -r "$E2E_PATH/test-results" "$report_dir/"; fi
|
||||
log "report saved on the server: $report_dir/playwright-report/index.html"
|
||||
fi
|
||||
exit "$status"
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# Weekly housekeeping — root cron on the VPS (docs/40-DEPLOYMENT.md §4):
|
||||
# 0 4 * * 0 /opt/erpcore/prune.sh >> /var/log/erpcore/prune.log 2>&1
|
||||
# Keeps the newest KEEP backend releases and frontend images (anything currently deployed in
|
||||
# either environment is always kept), prunes build cache, old database backups and E2E reports.
|
||||
# shellcheck source=common.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
KEEP="${KEEP:-5}"
|
||||
with_build_lock # never delete a release or image that a running job is producing
|
||||
|
||||
live=()
|
||||
for e in test release; do
|
||||
load_env "$e"
|
||||
live+=("$(read_state backend)" "$(read_state frontend)")
|
||||
if [[ -L "$CURRENT_LINK" ]]; then live+=("$(basename "$(readlink "$CURRENT_LINK")")"); fi
|
||||
done
|
||||
is_live() {
|
||||
local h
|
||||
for h in "${live[@]}"; do
|
||||
if [[ -n "$h" && "$h" == "$1" ]]; then return 0; fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ -d "$RELEASES_DIR" ]]; then
|
||||
n=0
|
||||
while IFS= read -r dir; do
|
||||
name="$(basename "$dir")"
|
||||
if [[ "$name" == *.tmp ]]; then
|
||||
log "removing abandoned build $name"
|
||||
rm -rf "$dir"
|
||||
continue
|
||||
fi
|
||||
n=$((n + 1))
|
||||
if ((n > KEEP)) && ! is_live "$name"; then
|
||||
log "removing backend release $name"
|
||||
rm -rf "$dir"
|
||||
fi
|
||||
done < <(find "$RELEASES_DIR" -mindepth 1 -maxdepth 1 -type d -printf '%T@ %p\n' | sort -rn | cut -d' ' -f2-)
|
||||
fi
|
||||
|
||||
n=0
|
||||
while IFS= read -r tag; do
|
||||
if [[ -z "$tag" || "$tag" == "<none>" ]]; then continue; fi
|
||||
n=$((n + 1))
|
||||
if ((n > KEEP)) && ! is_live "$tag"; then
|
||||
log "removing image $WEB_IMAGE_REPO:$tag"
|
||||
docker rmi "$WEB_IMAGE_REPO:$tag" >/dev/null || warn "could not remove $WEB_IMAGE_REPO:$tag"
|
||||
fi
|
||||
done < <(docker image ls "$WEB_IMAGE_REPO" --format '{{.Tag}}') # newest first
|
||||
|
||||
docker image prune -f >/dev/null
|
||||
docker builder prune -f --filter until=168h >/dev/null
|
||||
|
||||
find "$BACKUP_DIR" -maxdepth 1 -name 'test-*.dump' -mtime +7 -delete 2>/dev/null || true
|
||||
find "$BACKUP_DIR" -maxdepth 1 -name 'release-*.dump' -mtime +30 -delete 2>/dev/null || true
|
||||
find "$E2E_REPORTS_DIR" -mindepth 1 -maxdepth 1 -type d -mtime +14 -exec rm -rf {} + 2>/dev/null || true
|
||||
log "prune complete"
|
||||
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env bash
|
||||
# deploy.yml › release — record a successful release deploy as a Gitea release + tag
|
||||
# (release-YYYY.MM.DD-N on the deployed commit), including the tree hashes to roll back to.
|
||||
# shellcheck source=common.sh
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/common.sh"
|
||||
|
||||
: "${GITHUB_SHA:?}" "${ERP_GIT_TOKEN:?}" "${BACKEND_HASH:?}" "${FRONTEND_HASH:?}"
|
||||
command -v python3 >/dev/null 2>&1 || fail "python3 is required to build the release JSON"
|
||||
load_env release
|
||||
|
||||
# side_result <job result> <reused> — human-readable outcome for the notes table.
|
||||
side_result() {
|
||||
case "$1" in
|
||||
success) if [[ "$2" == true ]]; then echo "deployed (build reused)"; else echo "deployed (built)"; fi ;;
|
||||
skipped) echo "unchanged" ;;
|
||||
*) echo "$1" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
today="$(TZ=Asia/Colombo date +%Y.%m.%d)"
|
||||
mapfile -t tags < <(git_remote ls-remote --tags --refs "$REPO_GIT_URL" 'release-*' |
|
||||
awk '{ sub("refs/tags/", "", $2); print $2 }' | sort -V)
|
||||
previous="${tags[${#tags[@]}-1]:-}"
|
||||
same_day=0
|
||||
for t in "${tags[@]}"; do [[ "$t" == "release-$today-"* ]] && same_day=$((same_day + 1)); done
|
||||
tag="release-$today-$((same_day + 1))"
|
||||
|
||||
# Pull requests since the previous release: fetch only the history after that tag.
|
||||
if [[ -n "$previous" ]]; then
|
||||
git_remote fetch -q --shallow-exclude="$previous" "$REPO_GIT_URL" "+refs/heads/production:refs/remotes/origin/production" ||
|
||||
git_remote fetch -q --deepen=200 "$REPO_GIT_URL" "+refs/heads/production:refs/remotes/origin/production" || true
|
||||
else
|
||||
git_remote fetch -q --deepen=200 "$REPO_GIT_URL" "+refs/heads/production:refs/remotes/origin/production" || true
|
||||
fi
|
||||
prs="$(git log --merges --format='- %s' "$GITHUB_SHA" 2>/dev/null | grep -i ' into Dev' || true)"
|
||||
|
||||
body="Deployed to **release** ($PUBLIC_URL) from \`${GITHUB_SHA:0:12}\`.
|
||||
|
||||
| Side | Result | Tree hash — rollback input |
|
||||
|---|---|---|
|
||||
| Backend | $(side_result "${BACKEND_RESULT:-}" "${BACKEND_REUSED:-}") | \`$BACKEND_HASH\` |
|
||||
| Frontend | $(side_result "${FRONTEND_RESULT:-}" "${FRONTEND_REUSED:-}") | \`$FRONTEND_HASH\` |
|
||||
| Migrations | $([[ "${MIGRATIONS_CHANGED:-}" == true ]] && echo "changed (backup taken)" || echo "unchanged") | \`${MIGRATIONS_HASH:-}\` |
|
||||
|
||||
### Pull requests since ${previous:-the beginning}
|
||||
${prs:-_(none found)_}
|
||||
|
||||
Rollback: Actions → **Deploy** → Run workflow on \`production\` with \`backend_hash\` / \`frontend_hash\` from a previous release (docs/40-DEPLOYMENT.md §9)."
|
||||
|
||||
payload="$(TAG="$tag" SHA="$GITHUB_SHA" BODY="$body" python3 -c '
|
||||
import json, os
|
||||
print(json.dumps({"tag_name": os.environ["TAG"], "target_commitish": os.environ["SHA"],
|
||||
"name": os.environ["TAG"], "body": os.environ["BODY"]}))')"
|
||||
|
||||
log "creating Gitea release $tag"
|
||||
curl -sS --fail-with-body -o /dev/null -X POST \
|
||||
-H "Authorization: token $ERP_GIT_TOKEN" -H "Content-Type: application/json" \
|
||||
--data "$payload" "$GITEA_URL/api/v1/repos/$REPO_SLUG/releases"
|
||||
log "✅ $GITEA_URL/$REPO_SLUG/releases/tag/$tag"
|
||||
Reference in New Issue
Block a user