Add PlotDirector development refresh script

This commit is contained in:
Nick Beckley 2026-07-10 19:31:56 +00:00
parent ec8ce94082
commit 8cc0baa4b8
3 changed files with 174 additions and 2 deletions

View File

@ -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. - 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. - SQL helper scripts must not embed or print SQL administrator passwords.
- Firewall inspection should use `sudo ufw status numbered`. - 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.

View File

@ -1,7 +1,9 @@
# Development Scripts # 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. 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.

View File

@ -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<redacted>/g' \
-e 's/(SecretKey=)[^;[:space:]]+/\1<redacted>/g' \
-e 's/(ApiKey=)[^;[:space:]]+/\1<redacted>/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."