Add deployment scripts for git push and remote Docker rollout.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
# Deploy Idle Fantasy Save Viewer to production.
|
||||
# Usage (from repo root): .\scripts\deploy.ps1
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$BashScript = Join-Path $ScriptDir "deploy.sh"
|
||||
|
||||
if (Get-Command bash -ErrorAction SilentlyContinue) {
|
||||
& bash $BashScript
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
$DeployHost = if ($env:DEPLOY_HOST) { $env:DEPLOY_HOST } else { "root@10.0.0.5" }
|
||||
$DeployDir = if ($env:DEPLOY_DIR) { $env:DEPLOY_DIR } else { "/opt/apps/Idle-Fantasy-Save-Viewer" }
|
||||
$HealthUrl = if ($env:DEPLOY_HEALTH_URL) { $env:DEPLOY_HEALTH_URL } else { "http://127.0.0.1:5000/" }
|
||||
$HealthRetries = if ($env:DEPLOY_HEALTH_RETRIES) { [int]$env:DEPLOY_HEALTH_RETRIES } else { 20 }
|
||||
$HealthInterval = if ($env:DEPLOY_HEALTH_INTERVAL) { [int]$env:DEPLOY_HEALTH_INTERVAL } else { 2 }
|
||||
|
||||
function Write-Step([string]$Message) {
|
||||
Write-Host "==> $Message"
|
||||
}
|
||||
|
||||
function Fail([string]$Message) {
|
||||
Write-Error $Message
|
||||
exit 1
|
||||
}
|
||||
|
||||
$RepoRoot = git rev-parse --show-toplevel 2>$null
|
||||
if (-not $RepoRoot) { Fail "Not inside a git repository." }
|
||||
Set-Location $RepoRoot
|
||||
|
||||
$Branch = git rev-parse --abbrev-ref HEAD
|
||||
if ($Branch -eq "HEAD") { Fail "Detached HEAD – checkout a branch before deploying." }
|
||||
|
||||
$Status = git status --porcelain
|
||||
if ($Status) { Fail "Working tree is not clean. Commit or stash changes before deploying." }
|
||||
|
||||
$Upstream = git rev-parse --abbrev-ref --symbolic-full-name "@{u}" 2>$null
|
||||
if (-not $Upstream) { Fail "Branch '$Branch' has no upstream. Run: git push -u origin $Branch" }
|
||||
|
||||
$LocalSha = git rev-parse HEAD
|
||||
Write-Step "Local branch: $Branch ($LocalSha)"
|
||||
Write-Step "Pushing $Branch to origin…"
|
||||
git push origin $Branch
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
Write-Step "Deploying to ${DeployHost}:${DeployDir}"
|
||||
|
||||
$RemoteArgs = @(
|
||||
$DeployDir,
|
||||
$Branch,
|
||||
$LocalSha,
|
||||
$HealthUrl,
|
||||
[string]$HealthRetries,
|
||||
[string]$HealthInterval
|
||||
) -join " "
|
||||
|
||||
$RemoteScript = @'
|
||||
set -euo pipefail
|
||||
REMOTE_DIR="$1"
|
||||
BRANCH="$2"
|
||||
EXPECTED_SHA="$3"
|
||||
HEALTH_URL="$4"
|
||||
HEALTH_RETRIES="$5"
|
||||
HEALTH_INTERVAL="$6"
|
||||
|
||||
info() { printf '==> [remote] %s\n' "$*"; }
|
||||
die() { printf 'ERROR: [remote] %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
command -v docker >/dev/null 2>&1 || die "docker not found on remote host"
|
||||
docker compose version >/dev/null 2>&1 || die "docker compose not available on remote host"
|
||||
command -v curl >/dev/null 2>&1 || die "curl not found on remote host"
|
||||
|
||||
[[ -d "$REMOTE_DIR/.git" ]] || die "Directory is not a git repo: $REMOTE_DIR"
|
||||
|
||||
cd "$REMOTE_DIR"
|
||||
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
die "Remote working tree is dirty. Resolve local changes on the server first."
|
||||
fi
|
||||
|
||||
info "Fetching origin…"
|
||||
git fetch origin
|
||||
|
||||
REMOTE_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||
if [[ "$REMOTE_BRANCH" != "$BRANCH" ]]; then
|
||||
info "Checking out branch $BRANCH"
|
||||
git checkout "$BRANCH"
|
||||
fi
|
||||
|
||||
info "Fast-forwarding to origin/$BRANCH"
|
||||
git pull --ff-only origin "$BRANCH"
|
||||
|
||||
ACTUAL_SHA="$(git rev-parse HEAD)"
|
||||
if [[ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]]; then
|
||||
die "Remote SHA mismatch after pull (expected $EXPECTED_SHA, got $ACTUAL_SHA)."
|
||||
fi
|
||||
|
||||
info "Rebuilding and starting containers…"
|
||||
docker compose up -d --build --remove-orphans
|
||||
|
||||
info "Waiting for health check ($HEALTH_URL)…"
|
||||
ok=0
|
||||
for ((i = 1; i <= HEALTH_RETRIES; i++)); do
|
||||
if curl -fsS -o /dev/null "$HEALTH_URL"; then
|
||||
ok=1
|
||||
break
|
||||
fi
|
||||
sleep "$HEALTH_INTERVAL"
|
||||
done
|
||||
|
||||
if [[ "$ok" -ne 1 ]]; then
|
||||
die "Health check failed after $((HEALTH_RETRIES * HEALTH_INTERVAL))s."
|
||||
fi
|
||||
|
||||
info "Health check OK"
|
||||
|
||||
info "Pruning stopped containers…"
|
||||
docker container prune -f >/dev/null
|
||||
|
||||
info "Pruning dangling images…"
|
||||
docker image prune -f >/dev/null
|
||||
|
||||
info "Service status:"
|
||||
docker compose ps
|
||||
'@
|
||||
|
||||
$EncodedScript = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($RemoteScript))
|
||||
$SshCommand = "echo $EncodedScript | base64 -d | bash -s -- $RemoteArgs"
|
||||
|
||||
ssh -o BatchMode=yes $DeployHost $SshCommand
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
|
||||
Write-Step "Deployment finished successfully."
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env bash
|
||||
# Deploy Idle Fantasy Save Viewer to production.
|
||||
# Usage (from repo root): bash scripts/deploy.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REMOTE_HOST="${DEPLOY_HOST:-root@10.0.0.5}"
|
||||
REMOTE_DIR="${DEPLOY_DIR:-/opt/apps/Idle-Fantasy-Save-Viewer}"
|
||||
HEALTH_URL="${DEPLOY_HEALTH_URL:-http://127.0.0.1:5000/}"
|
||||
HEALTH_RETRIES="${DEPLOY_HEALTH_RETRIES:-20}"
|
||||
HEALTH_INTERVAL="${DEPLOY_HEALTH_INTERVAL:-2}"
|
||||
|
||||
info() { printf '==> %s\n' "$*"; }
|
||||
err() { printf 'ERROR: %s\n' "$*" >&2; }
|
||||
|
||||
die() {
|
||||
err "$@"
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_cmd() {
|
||||
command -v "$1" >/dev/null 2>&1 || die "Required command not found: $1"
|
||||
}
|
||||
|
||||
require_cmd git
|
||||
require_cmd ssh
|
||||
|
||||
ROOT="$(git rev-parse --show-toplevel 2>/dev/null)" || die "Not inside a git repository."
|
||||
cd "$ROOT"
|
||||
|
||||
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||
if [[ "$BRANCH" == "HEAD" ]]; then
|
||||
die "Detached HEAD – checkout a branch before deploying."
|
||||
fi
|
||||
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
die "Working tree is not clean. Commit or stash changes before deploying."
|
||||
fi
|
||||
|
||||
if ! git rev-parse --abbrev-ref --symbolic-full-name "@{u}" >/dev/null 2>&1; then
|
||||
die "Branch '$BRANCH' has no upstream. Run: git push -u origin $BRANCH"
|
||||
fi
|
||||
|
||||
LOCAL_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
info "Local branch: $BRANCH ($LOCAL_SHA)"
|
||||
|
||||
info "Pushing $BRANCH to origin…"
|
||||
git push origin "$BRANCH"
|
||||
|
||||
REMOTE_SHA="$(git rev-parse HEAD)"
|
||||
|
||||
info "Deploying to $REMOTE_HOST:$REMOTE_DIR"
|
||||
|
||||
ssh -o BatchMode=yes "$REMOTE_HOST" bash -s -- \
|
||||
"$REMOTE_DIR" \
|
||||
"$BRANCH" \
|
||||
"$REMOTE_SHA" \
|
||||
"$HEALTH_URL" \
|
||||
"$HEALTH_RETRIES" \
|
||||
"$HEALTH_INTERVAL" <<'REMOTE_SCRIPT'
|
||||
set -euo pipefail
|
||||
|
||||
REMOTE_DIR="$1"
|
||||
BRANCH="$2"
|
||||
EXPECTED_SHA="$3"
|
||||
HEALTH_URL="$4"
|
||||
HEALTH_RETRIES="$5"
|
||||
HEALTH_INTERVAL="$6"
|
||||
|
||||
info() { printf '==> [remote] %s\n' "$*"; }
|
||||
die() { printf 'ERROR: [remote] %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
command -v docker >/dev/null 2>&1 || die "docker not found on remote host"
|
||||
docker compose version >/dev/null 2>&1 || die "docker compose not available on remote host"
|
||||
command -v curl >/dev/null 2>&1 || die "curl not found on remote host"
|
||||
|
||||
[[ -d "$REMOTE_DIR/.git" ]] || die "Directory is not a git repo: $REMOTE_DIR"
|
||||
|
||||
cd "$REMOTE_DIR"
|
||||
|
||||
if [[ -n "$(git status --porcelain)" ]]; then
|
||||
die "Remote working tree is dirty. Resolve local changes on the server first."
|
||||
fi
|
||||
|
||||
info "Fetching origin…"
|
||||
git fetch origin
|
||||
|
||||
REMOTE_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
|
||||
if [[ "$REMOTE_BRANCH" != "$BRANCH" ]]; then
|
||||
info "Checking out branch $BRANCH"
|
||||
git checkout "$BRANCH"
|
||||
fi
|
||||
|
||||
info "Fast-forwarding to origin/$BRANCH"
|
||||
git pull --ff-only origin "$BRANCH"
|
||||
|
||||
ACTUAL_SHA="$(git rev-parse HEAD)"
|
||||
if [[ "$ACTUAL_SHA" != "$EXPECTED_SHA" ]]; then
|
||||
die "Remote SHA mismatch after pull (expected $EXPECTED_SHA, got $ACTUAL_SHA)."
|
||||
fi
|
||||
|
||||
info "Rebuilding and starting containers…"
|
||||
docker compose up -d --build --remove-orphans
|
||||
|
||||
info "Waiting for health check ($HEALTH_URL)…"
|
||||
ok=0
|
||||
for ((i = 1; i <= HEALTH_RETRIES; i++)); do
|
||||
if curl -fsS -o /dev/null "$HEALTH_URL"; then
|
||||
ok=1
|
||||
break
|
||||
fi
|
||||
sleep "$HEALTH_INTERVAL"
|
||||
done
|
||||
|
||||
if [[ "$ok" -ne 1 ]]; then
|
||||
die "Health check failed after $((HEALTH_RETRIES * HEALTH_INTERVAL))s."
|
||||
fi
|
||||
|
||||
info "Health check OK"
|
||||
|
||||
info "Pruning stopped containers…"
|
||||
docker container prune -f >/dev/null
|
||||
|
||||
info "Pruning dangling images…"
|
||||
docker image prune -f >/dev/null
|
||||
|
||||
info "Service status:"
|
||||
docker compose ps
|
||||
REMOTE_SCRIPT
|
||||
|
||||
info "Deployment finished successfully."
|
||||
Reference in New Issue
Block a user