#!/usr/bin/env bash set -Eeuo pipefail SOURCE_REPO="/srv/repos/PlotDirector" PUBLISH_DIR="/srv/apps/plotdirector-dev/current" SERVICE_NAME="plotdirector-dev" SMOKE_URL="http://127.0.0.1/Account/Login" SMOKE_HOST="dev.plotdirector.com" CONFIGURATION="Release" SKIP_BUILD=0 DRY_RUN=0 usage() { cat <<'USAGE' Usage: scripts/development/refresh-plotdirector.sh [options] Quickly refresh the PlotDirector development instance from the current working tree. This script does not pull, switch branches, commit, push, reset, clean, change database schema, run SQL scripts, or modify runtime secrets. Options: --help Show this help text. --dry-run Show intended actions without changing the running application. --skip-build Skip dotnet build and publish the current working tree directly. Defaults: Source repository: /srv/repos/PlotDirector Publish path: /srv/apps/plotdirector-dev/current Service: plotdirector-dev Smoke route: http://127.0.0.1/Account/Login with Host dev.plotdirector.com USAGE } log() { printf '%s\n' "$*" } fail() { printf 'ERROR: %s\n' "$*" >&2 exit 1 } show_recent_logs() { log "Latest ${SERVICE_NAME} logs:" sudo -n journalctl -u "$SERVICE_NAME" --no-pager -n 100 | sed -E \ -e 's/(Password=)[^;[:space:]]+/\1/g' \ -e 's/(SecretKey=)[^;[:space:]]+/\1/g' \ -e 's/(ApiKey=)[^;[:space:]]+/\1/g' || true } cleanup() { if [[ -n "${PUBLISH_TMP:-}" && -d "${PUBLISH_TMP:-}" ]]; then rm -rf "$PUBLISH_TMP" fi } trap cleanup EXIT while [[ "$#" -gt 0 ]]; do case "$1" in --help) usage exit 0 ;; --dry-run) DRY_RUN=1 ;; --skip-build) SKIP_BUILD=1 ;; *) fail "Unknown option: $1" ;; esac shift done [[ -d "$SOURCE_REPO/.git" ]] || fail "PlotDirector repository not found at ${SOURCE_REPO}" [[ -f "${SOURCE_REPO}/PlotLine/PlotLine.csproj" ]] || fail "PlotLine project not found" command -v dotnet >/dev/null 2>&1 || fail "dotnet command not found" command -v rsync >/dev/null 2>&1 || fail "rsync command not found" command -v curl >/dev/null 2>&1 || fail "curl command not found" sudo -n true >/dev/null 2>&1 || fail "passwordless non-interactive sudo is required" case "$PUBLISH_DIR" in ""|"/"|"/srv"|"/srv/"|"/srv/apps"|"/srv/apps/") fail "Unsafe publish path: ${PUBLISH_DIR}" ;; esac if [[ "$PUBLISH_DIR" != "/srv/apps/plotdirector-dev/current" ]]; then fail "Publish path must be /srv/apps/plotdirector-dev/current; got ${PUBLISH_DIR}" fi branch="$(git -C "$SOURCE_REPO" branch --show-current)" dirty="no" if [[ -n "$(git -C "$SOURCE_REPO" status --porcelain)" ]]; then dirty="yes" fi log "PlotDirector source: ${SOURCE_REPO}" log "Current branch: ${branch:-detached}" log "Working tree dirty: ${dirty}" log "Publish target: ${PUBLISH_DIR}" if [[ "$SKIP_BUILD" != "1" ]]; then log "Building PlotDirector (${CONFIGURATION})..." dotnet build "${SOURCE_REPO}/PlotLine/PlotLine.csproj" --configuration "$CONFIGURATION" else log "Skipping build by request." fi PUBLISH_TMP="$(mktemp -d /tmp/plotdirector-refresh.XXXXXX)" log "Publishing to temporary directory..." dotnet publish "${SOURCE_REPO}/PlotLine/PlotLine.csproj" --configuration "$CONFIGURATION" --no-build --output "$PUBLISH_TMP" [[ -f "${PUBLISH_TMP}/PlotLine.dll" ]] || fail "Publish output did not contain PlotLine.dll" if [[ "$DRY_RUN" == "1" ]]; then log "Dry-run mode: publish succeeded; running application was not changed." log "Would stop ${SERVICE_NAME}, replace ${PUBLISH_DIR}, start ${SERVICE_NAME}, and smoke test ${SMOKE_URL}." exit 0 fi log "Stopping ${SERVICE_NAME}..." sudo -n systemctl stop "$SERVICE_NAME" log "Replacing published application files..." sudo -n install -d -m 0750 -o plotdirector -g plotdirector "$PUBLISH_DIR" sudo -n rsync -a --delete "${PUBLISH_TMP}/" "${PUBLISH_DIR}/" sudo -n chown -R plotdirector:plotdirector "$PUBLISH_DIR" sudo -n find "$PUBLISH_DIR" -type d -exec chmod 0750 {} + sudo -n find "$PUBLISH_DIR" -type f -exec chmod 0640 {} + log "Starting ${SERVICE_NAME}..." sudo -n systemctl start "$SERVICE_NAME" for _ in {1..20}; do if systemctl is-active --quiet "$SERVICE_NAME"; then break fi sleep 1 done if ! systemctl is-active --quiet "$SERVICE_NAME"; then log "${SERVICE_NAME} did not become active." show_recent_logs exit 1 fi log "Smoke testing ${SMOKE_URL}..." smoke_ok=0 for _ in {1..30}; do if curl --fail --silent --show-error --noproxy '*' -H "Host: ${SMOKE_HOST}" "$SMOKE_URL" >/dev/null 2>&1; then smoke_ok=1 break fi sleep 1 done if [[ "$smoke_ok" != "1" ]]; then log "Smoke test failed for ${SMOKE_URL}." curl --fail --silent --show-error --noproxy '*' -H "Host: ${SMOKE_HOST}" "$SMOKE_URL" >/dev/null || true show_recent_logs exit 1 fi log "Refresh complete. ${SERVICE_NAME} is active and ${SMOKE_URL} returned HTTP 200."