Add automated release script
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -Eeuo pipefail
|
||||
umask 077
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(cd -- "$SCRIPT_DIR/.." && pwd)"
|
||||
|
||||
SERVICE_NAME="todolist.service"
|
||||
REMOTE_NAME="origin"
|
||||
RELEASE_BRANCH="main"
|
||||
HEALTH_URL="http://127.0.0.1:8081/api/todos"
|
||||
BACKUP_DIR="$HOME/backups/todolist"
|
||||
LOCK_FILE="${XDG_RUNTIME_DIR:-/tmp}/todolist-release.lock"
|
||||
|
||||
CURRENT_STEP="initialization"
|
||||
OLD_COMMIT=""
|
||||
NEW_COMMIT=""
|
||||
BACKUP_FILE=""
|
||||
|
||||
step() {
|
||||
CURRENT_STEP="$1"
|
||||
printf '\n==> %s\n' "$CURRENT_STEP"
|
||||
}
|
||||
|
||||
fail() {
|
||||
echo "Error: $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
show_failure_context() {
|
||||
local exit_code=$?
|
||||
|
||||
printf '\nRelease failed during: %s\n' "$CURRENT_STEP" >&2
|
||||
|
||||
if [[ -n "$OLD_COMMIT" ]]; then
|
||||
echo "Previous commit: $OLD_COMMIT" >&2
|
||||
fi
|
||||
|
||||
if [[ -n "$NEW_COMMIT" ]]; then
|
||||
echo "Current commit: $NEW_COMMIT" >&2
|
||||
fi
|
||||
|
||||
if [[ -n "$BACKUP_FILE" && -f "$BACKUP_FILE" ]]; then
|
||||
echo "Database backup: $BACKUP_FILE" >&2
|
||||
fi
|
||||
|
||||
systemctl --user status "$SERVICE_NAME" --no-pager >&2 || true
|
||||
journalctl --user -u "$SERVICE_NAME" -n 50 --no-pager >&2 || true
|
||||
|
||||
exit "$exit_code"
|
||||
}
|
||||
|
||||
trap show_failure_context ERR
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
step "Preflight checks"
|
||||
|
||||
for command_name in git uv mysqldump curl systemctl flock; do
|
||||
command -v "$command_name" >/dev/null 2>&1 ||
|
||||
fail "required command not found: $command_name"
|
||||
done
|
||||
|
||||
[[ -d ".git" ]] ||
|
||||
fail "$PROJECT_DIR is not a Git repository"
|
||||
|
||||
[[ -r ".env" ]] ||
|
||||
fail "$PROJECT_DIR/.env is missing or unreadable"
|
||||
|
||||
exec 9>"$LOCK_FILE"
|
||||
flock -n 9 ||
|
||||
fail "another TodoList release is already running"
|
||||
|
||||
CURRENT_BRANCH="$(git branch --show-current)"
|
||||
|
||||
[[ "$CURRENT_BRANCH" == "$RELEASE_BRANCH" ]] ||
|
||||
fail "release must run on $RELEASE_BRANCH; current branch is $CURRENT_BRANCH"
|
||||
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
echo "Error: Git working tree is not clean" >&2
|
||||
git status --short >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
systemctl --user is-active --quiet "$SERVICE_NAME" ||
|
||||
fail "$SERVICE_NAME is not currently active"
|
||||
|
||||
OLD_COMMIT="$(git rev-parse HEAD)"
|
||||
|
||||
echo "Project: $PROJECT_DIR"
|
||||
echo "Branch: $CURRENT_BRANCH"
|
||||
echo "Service: $SERVICE_NAME"
|
||||
echo "Current: $OLD_COMMIT"
|
||||
|
||||
step "Fetch latest code"
|
||||
|
||||
git fetch --prune "$REMOTE_NAME" "$RELEASE_BRANCH"
|
||||
|
||||
REMOTE_COMMIT="$(git rev-parse "$REMOTE_NAME/$RELEASE_BRANCH")"
|
||||
MERGE_BASE="$(git merge-base HEAD "$REMOTE_NAME/$RELEASE_BRANCH")"
|
||||
|
||||
[[ "$MERGE_BASE" == "$OLD_COMMIT" ]] ||
|
||||
fail "local branch contains unpushed commits or has diverged from $REMOTE_NAME/$RELEASE_BRANCH"
|
||||
|
||||
step "Load database settings"
|
||||
|
||||
eval "$(
|
||||
uv run python - <<'PYENV'
|
||||
import shlex
|
||||
|
||||
from dotenv import dotenv_values
|
||||
|
||||
required_keys = (
|
||||
"MYSQL_HOST",
|
||||
"MYSQL_PORT",
|
||||
"MYSQL_USER",
|
||||
"MYSQL_PASSWORD",
|
||||
"MYSQL_DATABASE",
|
||||
)
|
||||
|
||||
values = dotenv_values(".env")
|
||||
missing = [key for key in required_keys if not values.get(key)]
|
||||
|
||||
if missing:
|
||||
raise SystemExit(
|
||||
"Missing required variables in .env: " + ", ".join(missing)
|
||||
)
|
||||
|
||||
for key in required_keys:
|
||||
print(f"export {key}={shlex.quote(str(values[key]))}")
|
||||
PYENV
|
||||
)"
|
||||
|
||||
step "Back up database"
|
||||
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
chmod 700 "$BACKUP_DIR"
|
||||
|
||||
TIMESTAMP="$(date '+%Y%m%d-%H%M%S')"
|
||||
BACKUP_FILE="$BACKUP_DIR/${MYSQL_DATABASE}-${TIMESTAMP}-${REMOTE_COMMIT:0:12}.sql"
|
||||
|
||||
MYSQL_PWD="$MYSQL_PASSWORD" mysqldump \
|
||||
--no-tablespaces \
|
||||
--single-transaction \
|
||||
--quick \
|
||||
--skip-lock-tables \
|
||||
--host="$MYSQL_HOST" \
|
||||
--port="$MYSQL_PORT" \
|
||||
--user="$MYSQL_USER" \
|
||||
"$MYSQL_DATABASE" \
|
||||
> "$BACKUP_FILE"
|
||||
|
||||
[[ -s "$BACKUP_FILE" ]] ||
|
||||
fail "database backup file is empty"
|
||||
|
||||
echo "Backup: $BACKUP_FILE"
|
||||
|
||||
step "Update working tree"
|
||||
|
||||
git merge --ff-only "$REMOTE_NAME/$RELEASE_BRANCH"
|
||||
NEW_COMMIT="$(git rev-parse HEAD)"
|
||||
|
||||
echo "Updated to: $NEW_COMMIT"
|
||||
|
||||
step "Synchronize Python dependencies"
|
||||
|
||||
uv sync --frozen
|
||||
|
||||
step "Run code checks"
|
||||
|
||||
uv run ruff check .
|
||||
uv run ruff format --check .
|
||||
uv run python -c "import main"
|
||||
|
||||
step "Apply database migrations"
|
||||
|
||||
uv run alembic upgrade head
|
||||
uv run alembic current --check-heads
|
||||
|
||||
step "Restart TodoList user service"
|
||||
|
||||
systemctl --user daemon-reload
|
||||
systemctl --user restart "$SERVICE_NAME"
|
||||
systemctl --user is-active --quiet "$SERVICE_NAME"
|
||||
|
||||
step "Run health check"
|
||||
|
||||
HEALTHY="false"
|
||||
|
||||
for attempt in {1..15}; do
|
||||
if curl \
|
||||
--fail \
|
||||
--silent \
|
||||
--show-error \
|
||||
--max-time 3 \
|
||||
"$HEALTH_URL" \
|
||||
>/dev/null; then
|
||||
HEALTHY="true"
|
||||
break
|
||||
fi
|
||||
|
||||
echo "Health check attempt $attempt/15 failed; retrying..."
|
||||
sleep 1
|
||||
done
|
||||
|
||||
if [[ "$HEALTHY" != "true" ]]; then
|
||||
echo "Error: health check failed: $HEALTH_URL" >&2
|
||||
systemctl --user status "$SERVICE_NAME" --no-pager >&2 || true
|
||||
journalctl --user -u "$SERVICE_NAME" -n 50 --no-pager >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '\nTodoList release succeeded\n'
|
||||
echo "Previous commit: $OLD_COMMIT"
|
||||
echo "Current commit: $NEW_COMMIT"
|
||||
echo "Database backup: $BACKUP_FILE"
|
||||
echo "Health check: $HEALTH_URL"
|
||||
git log -1 --oneline
|
||||
Reference in New Issue
Block a user