diff --git a/scripts/README.md b/scripts/README.md index 4acbfb4..5ecdb28 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -54,3 +54,4 @@ Additional areas may be added when a repeated operational need exists. - Discovery and validation scripts must not dump environment variables, private keys, tokens, or complete sensitive configuration files. - SQL helper scripts must not embed or print SQL administrator passwords. - Firewall inspection should use `sudo ufw status numbered`. +- Development refresh scripts may operate on dirty application working trees, but must not change Git state or run schema migrations implicitly. diff --git a/scripts/development/README.md b/scripts/development/README.md index ae035db..cc931bc 100644 --- a/scripts/development/README.md +++ b/scripts/development/README.md @@ -1,7 +1,9 @@ # Development Scripts -This directory is reserved for development workflow helpers that do not belong to installation, deployment, backup, restore, monitoring, maintenance, or system validation. +This directory contains development workflow helpers that do not belong to installation, deployment, backup, restore, monitoring, maintenance, or system validation. + +`refresh-plotdirector.sh` rebuilds and republishes the current `/srv/repos/PlotDirector` working tree to `/srv/apps/plotdirector-dev/current`, restarts `plotdirector-dev`, and smoke-tests `/Account/Login` through nginx. It allows uncommitted application changes and does not pull, switch branches, commit, push, reset, clean, alter the database schema, run SQL scripts, or modify `/etc/plotdirector/plotdirector.env`. Do not store secrets, generated logs, package caches, or one-off experiments here. -Future structure is `TODO: Confirm during the relevant implementation phase.` +Generated publish output remains outside the repository. diff --git a/scripts/development/refresh-plotdirector.sh b/scripts/development/refresh-plotdirector.sh new file mode 100755 index 0000000..089f500 --- /dev/null +++ b/scripts/development/refresh-plotdirector.sh @@ -0,0 +1,169 @@ +#!/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."