rust fmt, updated ci, hooks and docs

This commit is contained in:
nvrl committed 2026-06-15 17:08:46 +02:00
1 parent 8c120ae78d
commit a4c9b5bad1
39 files changed
+691 -902

No files matched your search

+23 -8
View File
@@ -1,7 +1,11 @@
#!/bin/bash
set -euo pipefail
# This script creates a full snapshot of the database and user media.
# Best run as a daily cron job.
# Full snapshot of the SQLite database + user media. Host-side and safe to run
# while the Rust backend has the DB open (WAL mode): uses `sqlite3 .backup`
# when available, otherwise an online copy via the same backend container.
# The Rust scheduler also runs backup_db on its own interval; this is the
# manual/cron supplement. Best run as a daily cron job.
ROOT_DIR=$(git rev-parse --show-toplevel)
cd "$ROOT_DIR"
@@ -10,6 +14,7 @@ TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="full_snapshot_$TIMESTAMP"
TEMP_DIR="backend/backups/$BACKUP_NAME"
FINAL_DEST="backend/backups"
DB_SRC="backend/db.sqlite3"
echo "========================================"
echo " Starting Full Project Backup... "
@@ -17,19 +22,29 @@ echo "========================================"
mkdir -p "$TEMP_DIR"
# 1. Run the database backup command
# 1. Database — consistent snapshot.
echo "Backing up database..."
docker exec holy-backend python manage.py backup_db --output "/app/backups/$BACKUP_NAME/db.sqlite3"
if command -v sqlite3 &>/dev/null; then
# Atomic, WAL-safe even with the backend holding the DB open.
sqlite3 "$DB_SRC" ".backup '$TEMP_DIR/db.sqlite3'"
else
# No host sqlite3: copy the DB plus its WAL sidecars so an in-flight WAL
# isn't lost. Install sqlite3 for a guaranteed-atomic snapshot.
echo " host sqlite3 not found — copying db + WAL sidecars"
cp "$DB_SRC" "$TEMP_DIR/db.sqlite3"
[ -f "$DB_SRC-wal" ] && cp "$DB_SRC-wal" "$TEMP_DIR/db.sqlite3-wal"
[ -f "$DB_SRC-shm" ] && cp "$DB_SRC-shm" "$TEMP_DIR/db.sqlite3-shm"
fi
# 2. Copy the media folder
# 2. Media.
echo "Backing up media..."
cp -r backend/media "$TEMP_DIR/media"
# 3. Create compressed archive
# 3. Compress.
echo "Compressing..."
tar -czf "$FINAL_DEST/$BACKUP_NAME.tar.gz" -C "backend/backups" "$BACKUP_NAME"
tar -czf "$FINAL_DEST/$BACKUP_NAME.tar.gz" -C "$FINAL_DEST" "$BACKUP_NAME"
# 4. Cleanup temp folder
# 4. Cleanup temp folder.
rm -rf "$TEMP_DIR"
echo ""
-200
View File
@@ -1,200 +0,0 @@
#!/usr/bin/env bash
# =========================================================================
# Strangler cutover driver: Django -> Rust, zero downtime, reversible.
#
# Flips nginx `proxy_pass` targets one phase-group at a time and reloads
# nginx, then (Phase 3) hands job scheduling from Django's Celery beat to
# the Rust in-process scheduler. Every phase has an inverse `rollback`.
#
# It edits frontend/nginx.conf in place. To make reloads INSTANT (no image
# rebuild), run `cutover.sh dev-mount` once first: it bind-mounts the conf
# into holy-frontend so `nginx -s reload` picks up edits live. Without that,
# each flip rebuilds the frontend image (slower, but still no API downtime).
#
# Usage:
# scripts/cutover.sh status # show current routing + scheduler
# scripts/cutover.sh phase0 # build + start holy-rust (shadow)
# scripts/cutover.sh phase1 # flip reads
# scripts/cutover.sh phase2 # flip auth + writes
# scripts/cutover.sh phase3 # flip admin, hand off scheduler
# scripts/cutover.sh phase4 # flip catch-all (retire Django path)
# scripts/cutover.sh rollback {1|2|3|4} # undo a phase
# scripts/cutover.sh smoke # hit holy-rust /health/ directly
#
# Invariant: exactly ONE scheduler owns api_job. Django beat owns it until
# phase3; Rust's ENABLE_SCHEDULER stays false until then. Never both.
# =========================================================================
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CONF="$REPO/frontend/nginx.conf"
ENVFILE="$REPO/.env"
DC="docker compose"
# Endpoint groups per phase (nginx location prefixes, sans trailing proxy line).
READS=(/api/categories/ /api/flavors/ /api/banners/)
WRITES=(/api/auth/ "= /api/token/" /api/ratings/ /api/replies/ /api/users/ /api/notifications/ /api/tickets/)
ADMIN=(/api/admin-custom/)
CATCHALL=(/api/schema /api/)
c_red() { printf '\033[31m%s\033[0m\n' "$*"; }
c_grn() { printf '\033[32m%s\033[0m\n' "$*"; }
c_ylw() { printf '\033[33m%s\033[0m\n' "$*"; }
die() { c_red "ERROR: $*" >&2; exit 1; }
[ -f "$CONF" ] || die "nginx.conf not found at $CONF"
# Flip the upstream on the location line(s) for a given prefix.
# $1 = location token (e.g. /api/flavors/ or '= /api/token/')
# $2 = target upstream (rust_backend | django_backend)
flip_one() {
local loc="$1" target="$2"
# Match: location <loc> { proxy_pass http://<anything>; ... }
# Anchor on the exact location token to avoid hitting the catch-all /api/.
local esc; esc="$(printf '%s' "$loc" | sed 's/[.[\*^$/]/\\&/g')"
# The catch-all "/api/" lives on its own multi-line block; handle separately.
if [ "$loc" = "/api/" ]; then
# Flip only the catch-all block's proxy_pass (the one not on a one-liner).
perl -0pi -e "s{(location /api/ \{\s*\n\s*proxy_pass http://)\w+;}{\${1}${target};}g" "$CONF"
return
fi
sed -i -E "s|(location ${esc}[[:space:]]+\{[[:space:]]+proxy_pass http://)[a-z_]+;|\1${target};|" "$CONF"
}
flip_group() {
local target="$1"; shift
local g
for g in "$@"; do flip_one "$g" "$target"; done
}
reload_nginx() {
if $DC ps frontend --format '{{.Names}}' 2>/dev/null | grep -q holy-frontend \
&& docker inspect holy-frontend --format '{{range .Mounts}}{{.Destination}}{{"\n"}}{{end}}' 2>/dev/null \
| grep -q '/etc/nginx/conf.d/default.conf'; then
c_ylw "conf is bind-mounted -> live reload"
docker exec holy-frontend nginx -t && docker exec holy-frontend nginx -s reload
else
c_ylw "conf baked into image -> rebuilding frontend (no API downtime)"
$DC up -d --build frontend
fi
}
set_env() { # set_env KEY value (idempotent upsert in .env)
local k="$1" v="$2"
[ -f "$ENVFILE" ] || touch "$ENVFILE"
if grep -qE "^${k}=" "$ENVFILE"; then
sed -i -E "s|^${k}=.*|${k}=${v}|" "$ENVFILE"
else
printf '%s=%s\n' "$k" "$v" >> "$ENVFILE"
fi
}
confirm() {
read -r -p "$1 [y/N] " a; [ "$a" = y ] || [ "$a" = Y ] || die "aborted"
}
cmd_status() {
c_grn "== nginx routing (frontend/nginx.conf) =="
grep -E 'location (= )?/api/' "$CONF" | sed -E 's/\s*include.*//' \
| sed -E 's/proxy_pass http:\/\/(rust_backend)/proxy_pass -> \x1b[32m\1\x1b[0m/; s/proxy_pass http:\/\/(django_backend)/proxy_pass -> \1/'
echo
c_grn "== scheduler ownership =="
local rust_sched; rust_sched="$(grep -E '^RUST_ENABLE_SCHEDULER=' "$ENVFILE" 2>/dev/null | cut -d= -f2 || true)"
echo "RUST_ENABLE_SCHEDULER=${rust_sched:-unset (=false)}"
if $DC ps beat --format '{{.Names}} {{.State}}' 2>/dev/null | grep -q running; then
echo "Django beat: RUNNING (Django owns api_job)"
else
echo "Django beat: stopped"
fi
}
cmd_smoke() {
c_grn "Hitting holy-rust /health/ directly (bypasses nginx)..."
docker exec holy-frontend wget -qO- http://holy-rust:8001/health/ \
|| die "holy-rust not reachable — run phase0 first?"
echo; c_grn "OK"
}
cmd_phase0() {
grep -qE '^SECRET_KEY=.+' "$ENVFILE" 2>/dev/null \
|| die "SECRET_KEY must be set in .env to the SAME value Django uses (JWTs break otherwise)"
c_grn "Phase 0: building + starting holy-rust in shadow (no traffic)..."
$DC up -d --build backend-rs
c_ylw "Wait for healthy, then: scripts/cutover.sh smoke"
}
cmd_phase1() { c_grn "Phase 1: flip READS -> rust"; flip_group rust_backend "${READS[@]}"; reload_nginx; cmd_status; }
cmd_phase2() { c_grn "Phase 2: flip AUTH+WRITES -> rust"; flip_group rust_backend "${WRITES[@]}"; reload_nginx; cmd_status; }
cmd_phase3() {
c_grn "Phase 3: flip ADMIN -> rust, then hand off scheduler"
flip_group rust_backend "${ADMIN[@]}"; reload_nginx
c_ylw "About to STOP Django beat/worker/flower and ENABLE Rust scheduler."
c_ylw "This transfers api_job ownership. Ensure no job is mid-run."
confirm "Proceed with scheduler handover?"
$DC stop beat worker flower
set_env RUST_ENABLE_SCHEDULER true
$DC up -d backend-rs
c_grn "Done. Check: docker logs holy-rust | grep 'job scheduler ENABLED'"
cmd_status
}
cmd_phase4() {
c_grn "Phase 4: flip CATCH-ALL /api/ + /api/schema -> rust (Django serves only /admin/ + /static/)"
flip_group rust_backend "${CATCHALL[@]}"; reload_nginx
c_ylw "Django now handles only its admin site. To drop it entirely:"
c_ylw " $DC stop backend ; then remove /admin/ + /static/ locations from nginx.conf"
c_ylw " $DC stop redis # only a Celery broker + Django cache; Rust uses neither"
cmd_status
}
cmd_rollback() {
local p="${1:-}"
case "$p" in
1) c_grn "Rollback Phase 1"; flip_group django_backend "${READS[@]}"; reload_nginx ;;
2) c_grn "Rollback Phase 2"; flip_group django_backend "${WRITES[@]}"; reload_nginx ;;
3) c_grn "Rollback Phase 3: scheduler back to Django, admin back to Django"
set_env RUST_ENABLE_SCHEDULER false
$DC up -d backend-rs
$DC start beat worker flower
flip_group django_backend "${ADMIN[@]}"; reload_nginx ;;
4) c_grn "Rollback Phase 4"; flip_group django_backend "${CATCHALL[@]}"; reload_nginx ;;
*) die "usage: cutover.sh rollback {1|2|3|4}" ;;
esac
cmd_status
}
cmd_dev_mount() {
c_ylw "Add this under the 'frontend' service volumes in docker-compose.yml for live reloads:"
echo ' - ./frontend/nginx.conf:/etc/nginx/conf.d/default.conf'
echo ' - ./frontend/proxy_params.conf:/etc/nginx/proxy_params.conf'
c_ylw "Then: $DC up -d frontend"
}
case "${1:-}" in
status) cmd_status ;;
smoke) cmd_smoke ;;
phase0) cmd_phase0 ;;
phase1) cmd_phase1 ;;
phase2) cmd_phase2 ;;
phase3) cmd_phase3 ;;
phase4) cmd_phase4 ;;
rollback) cmd_rollback "${2:-}" ;;
dev-mount) cmd_dev_mount ;;
*) cat <<EOF
Strangler cutover driver. Phases are reversible; data never forks (shared DB).
status show routing + scheduler ownership
smoke curl holy-rust /health/ directly
phase0 build + start holy-rust (shadow, no traffic)
phase1 flip reads (categories, flavors, banners)
phase2 flip auth+writes (auth, token, ratings, replies, users, notifications, tickets)
phase3 flip admin + hand scheduler Django->Rust (CAREFUL)
phase4 flip catch-all /api/ (Django -> only /admin/ + /static/)
rollback {1|2|3|4} undo a phase
dev-mount print compose lines for live nginx reloads
See MIGRATION.md for the full runbook.
EOF
;;
esac
+7 -24
View File
@@ -3,11 +3,6 @@
# Get the root directory of the project
ROOT_DIR=$(git rev-parse --show-toplevel)
# Set environment variables for non-interactive backend checks
export SECRET_KEY="pre-push-check-key"
export DEBUG="false"
export ALLOWED_HOSTS="localhost"
echo "========================================"
echo " Running Pre-Push Integrity Checks "
echo "========================================"
@@ -23,32 +18,20 @@ if ! npm run build; then
exit 1
fi
# 2. Backend Check
# 2. Rust backend checks
echo ""
echo "Step 2: Checking Backend Integrity..."
cd "$ROOT_DIR/backend"
echo "Step 2: Checking Rust backend..."
cd "$ROOT_DIR/backend-rs"
# Detect python executable (prefer venv if exists)
if [ -f "venv/bin/python" ]; then
PYTHON_EXEC="venv/bin/python"
elif command -v python3 &>/dev/null; then
PYTHON_EXEC="python3"
else
PYTHON_EXEC="python"
fi
# Check for missing migrations
if ! $PYTHON_EXEC manage.py makemigrations --check --dry-run &>/dev/null; then
if ! cargo fmt --check; then
echo ""
echo "❌ ERROR: Missing migrations detected."
echo " Please run 'python manage.py makemigrations' locally."
echo "❌ ERROR: Rust formatting issues. Run 'cargo fmt' in backend-rs/."
exit 1
fi
# Run Django system checks
if ! $PYTHON_EXEC manage.py check; then
if ! cargo check --locked; then
echo ""
echo "❌ ERROR: Django system check failed."
echo "❌ ERROR: Rust backend does not compile."
exit 1
fi