Merge remote-tracking branch 'origin/main' into codex/housekeeping-rebuild-stepwise
This commit is contained in:
commit
ce4a3ba32c
129 files changed
+4364
-1599
No files matched your search
Executable
+89
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# docker-update.sh — Automatic daily update for the Dockerized EpicNext-CMS.
|
||||
#
|
||||
# Steps:
|
||||
# 1. Verify the working tree is clean (uncommitted changes abort).
|
||||
# 2. git pull (fast-forward only).
|
||||
# 3. Run CMS migrations on the HOST (the slim runtime container has no source).
|
||||
# 4. docker compose build (auto-detects pnpm/yarn/npm via lockfile).
|
||||
# 5. docker compose up -d && wait for a healthy container.
|
||||
# 6. Record everything in update.log.
|
||||
#
|
||||
# Exit codes: 0 ok, 1 update skipped, 2 build/deploy failed, 3 health failed.
|
||||
# ==============================================================================
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "$DIR" || exit 2
|
||||
|
||||
LOG_FILE="${LOG_FILE:-$DIR/logs/docker-update.log}"
|
||||
PM2_APP="${PM2_APP:-next}" # host-side CMS that must stay stopped (port 3002)
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
|
||||
die() { log "ERROR: $*"; exit "${2:-2}"; }
|
||||
|
||||
touch "$LOG_FILE"
|
||||
|
||||
log "=== Start docker-update ==="
|
||||
|
||||
# --- 0. Guard: uncommitted changes would break git pull / taint deploys ---
|
||||
if ! { git diff --quiet --exit-code && git diff --cached --quiet --exit-code; }; then
|
||||
die "working tree has uncommitted changes; commit or stash first" 1
|
||||
fi
|
||||
|
||||
# --- 1. Pull latest ---
|
||||
git pull --ff-only --quiet 2>>"$LOG_FILE"
|
||||
pull_status=$?
|
||||
if [ $pull_status -ne 0 ]; then
|
||||
die "git pull failed (status $pull_status)" 1
|
||||
fi
|
||||
log "git pull OK: $(git rev-parse --short HEAD)"
|
||||
|
||||
# --- 2. Host-side migrations (idempotent; only applies CMS-owned tables) ---
|
||||
if [ -f pnpm-lock.yaml ] && command -v pnpm >/dev/null 2>&1; then
|
||||
pnpm db:migrate >>"$LOG_FILE" 2>&1 || die "db:migrate (pnpm) failed"
|
||||
elif command -v npm >/dev/null 2>&1; then
|
||||
npm run db:migrate >>"$LOG_FILE" 2>&1 || die "db:migrate (npm) failed"
|
||||
else
|
||||
die "no package manager found for migrations" 2
|
||||
fi
|
||||
log "db:migrate OK"
|
||||
|
||||
# --- 3. Rebuild the image ---
|
||||
docker compose build >>"$LOG_FILE" 2>&1 || die "docker compose build failed" 2
|
||||
log "docker compose build OK"
|
||||
|
||||
# --- 4. Recreate the container ---
|
||||
docker compose up -d >>"$LOG_FILE" 2>&1 || die "docker compose up failed" 2
|
||||
log "docker compose up OK"
|
||||
|
||||
# --- 5. Wait for health (up to ~4 min) ---
|
||||
healthy=0
|
||||
for i in $(seq 1 16); do
|
||||
status="$(docker inspect --format='{{.State.Health.Status}}' epicnext-cms 2>/dev/null || true)"
|
||||
case "$status" in
|
||||
healthy) healthy=1; break ;;
|
||||
unhealthy) break ;;
|
||||
esac
|
||||
sleep 15
|
||||
done
|
||||
|
||||
if [ "$healthy" -eq 1 ]; then
|
||||
log "CMS healthy after update (commit $(git rev-parse --short HEAD))"
|
||||
else
|
||||
log "WARNING: container not healthy (status='${status:-unknown}')"
|
||||
# Leave the container running so it can be debugged; report failure exit.
|
||||
exit 3
|
||||
fi
|
||||
|
||||
# --- 6. Make sure the stale host-side PM2 CMS stays stopped ---
|
||||
if command -v pm2 >/dev/null 2>&1 && pm2 jlist >/dev/null 2>&1; then
|
||||
if pm2 list 2>/dev/null | grep -q "${PM2_APP}"; then
|
||||
pm2 stop "$PM2_APP" >/dev/null 2>&1 && log "pm2 '${PM2_APP}' kept stopped (avoids port 3002 clash)"
|
||||
fi
|
||||
fi
|
||||
|
||||
log "=== docker-update finished OK ==="
|
||||
exit 0
|
||||
+47
-2
@@ -1,8 +1,13 @@
|
||||
import "./load-env";
|
||||
import { Cron } from "croner";
|
||||
import { lt, sql } from "drizzle-orm";
|
||||
import { and, eq, lt, lte, sql } from "drizzle-orm";
|
||||
import { env } from "../src/env";
|
||||
import { db, PasswordReset, WebsiteLoginLogs } from "../src/lib/db";
|
||||
import {
|
||||
db,
|
||||
PasswordReset,
|
||||
WebsiteArticles,
|
||||
WebsiteLoginLogs,
|
||||
} from "../src/lib/db";
|
||||
import { logger } from "../src/lib/logger";
|
||||
import { redis } from "../src/lib/redis";
|
||||
import { emulatorOffline, healthDegraded } from "../src/lib/services/alert";
|
||||
@@ -224,6 +229,37 @@ async function cleanupOldSessions(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function publishScheduledArticles(): Promise<void> {
|
||||
try {
|
||||
const now = new Date();
|
||||
const result = await db
|
||||
.update(WebsiteArticles)
|
||||
.set({
|
||||
status: "published",
|
||||
publishedAt: now,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(WebsiteArticles.status, "scheduled"),
|
||||
lte(WebsiteArticles.publishAt, now),
|
||||
),
|
||||
);
|
||||
const info = result as unknown as {
|
||||
affectedRows?: number;
|
||||
rowsAffected?: number;
|
||||
};
|
||||
const published = info.affectedRows ?? info.rowsAffected ?? 0;
|
||||
if (published > 0) {
|
||||
logger.info(`Published ${published} scheduled article(s)`, {
|
||||
module: "jobs",
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
captureWorkerError(err, "Scheduled article publish failed");
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
logger.info("Worker started", { module: "jobs" });
|
||||
|
||||
@@ -257,6 +293,15 @@ async function main() {
|
||||
});
|
||||
logger.info("Scheduled: ops health probe (every 5 min)", { module: "jobs" });
|
||||
|
||||
new Cron("* * * * *", () => {
|
||||
publishScheduledArticles().catch((e) =>
|
||||
captureWorkerError(e, "ScheduledArticlePublish"),
|
||||
);
|
||||
});
|
||||
logger.info("Scheduled: publish scheduled articles (every minute)", {
|
||||
module: "jobs",
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
backupEmulatorJar(),
|
||||
cleanupOldLogs(),
|
||||
|
||||
Reference in new issue
Block a user