feat(deploy): implement atomic Box web console deploy script and test suite
This commit is contained in:
Executable
+117
@@ -0,0 +1,117 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# deploy-box-web.sh — Atomically package and deploy Box Web Console assets to the Google Cloud VM
|
||||||
|
#
|
||||||
|
# Assets: web/box/www/{index.html, box.js, box.css}
|
||||||
|
# Destination: /srv/box/www/ on 34.139.37.135 (box.muse-dev.online)
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
SRC_DIR="$REPO_ROOT/web/box/www"
|
||||||
|
|
||||||
|
# Default configuration
|
||||||
|
DEST_HOST="${BOX_WEB_HOST:-34.139.37.135}"
|
||||||
|
DEST_USER="${BOX_WEB_USER:-super}"
|
||||||
|
DEST_DIR="${BOX_WEB_DEST:-/srv/box/www}"
|
||||||
|
DRY_RUN=0
|
||||||
|
BUNDLE_ONLY=0
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--dry-run)
|
||||||
|
DRY_RUN=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--bundle-only)
|
||||||
|
BUNDLE_ONLY=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
--host)
|
||||||
|
DEST_HOST="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--user)
|
||||||
|
DEST_USER="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--dest)
|
||||||
|
DEST_DIR="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
echo "Usage: $0 [options]"
|
||||||
|
echo ""
|
||||||
|
echo "Options:"
|
||||||
|
echo " --dry-run Preview assets, hashes, and destination without deploying"
|
||||||
|
echo " --bundle-only Package the atomic tarball into /tmp and exit"
|
||||||
|
echo " --host <host> Target host (default: 34.139.37.135)"
|
||||||
|
echo " --user <user> SSH user on target (default: super)"
|
||||||
|
echo " --dest <dir> Remote directory path (default: /srv/box/www)"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Unknown option: $1" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Pre-flight asset verification
|
||||||
|
if [[ ! -d "$SRC_DIR" ]]; then
|
||||||
|
echo "Error: Source directory missing at $SRC_DIR" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
REQUIRED_FILES=("index.html" "box.js" "box.css")
|
||||||
|
for f in "${REQUIRED_FILES[@]}"; do
|
||||||
|
if [[ ! -f "$SRC_DIR/$f" ]]; then
|
||||||
|
echo "Error: Missing required asset: $SRC_DIR/$f" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "=== Box Web Console Deployment Plan ==="
|
||||||
|
echo "Source: $SRC_DIR"
|
||||||
|
echo "Target: $DEST_USER@$DEST_HOST:$DEST_DIR"
|
||||||
|
echo "Mode: $([ "$DRY_RUN" -eq 1 ] && echo "DRY-RUN (no changes)" || echo "LIVE")"
|
||||||
|
echo ""
|
||||||
|
echo "Asset Manifest & SHA256 Checksums:"
|
||||||
|
for f in "${REQUIRED_FILES[@]}"; do
|
||||||
|
csum=$(sha256sum "$SRC_DIR/$f" | cut -d' ' -f1)
|
||||||
|
size=$(wc -c < "$SRC_DIR/$f" | tr -d ' ')
|
||||||
|
printf " • %-12s %6s bytes sha256:%s\n" "$f" "$size" "$csum"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Dry-run exit
|
||||||
|
if [[ "$DRY_RUN" -eq 1 ]]; then
|
||||||
|
echo "[dry-run] Validation complete. No files transferred."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create atomic bundle in temp directory
|
||||||
|
BUNDLE_PATH="/tmp/box-web-bundle-$(date +%s).tar.gz"
|
||||||
|
echo "Creating atomic archive: $BUNDLE_PATH..."
|
||||||
|
tar -czf "$BUNDLE_PATH" -C "$SRC_DIR" index.html box.js box.css
|
||||||
|
echo "Archive created ($(wc -c < "$BUNDLE_PATH" | tr -d ' ') bytes)."
|
||||||
|
|
||||||
|
if [[ "$BUNDLE_ONLY" -eq 1 ]]; then
|
||||||
|
echo "Bundle generated at $BUNDLE_PATH. Skipping network deployment."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Deploy to remote target
|
||||||
|
echo "Deploying to $DEST_USER@$DEST_HOST:$DEST_DIR..."
|
||||||
|
REMOTE_TMP="/tmp/box-web-bundle-$$.tar.gz"
|
||||||
|
|
||||||
|
scp -o ConnectTimeout=10 -o BatchMode=yes "$BUNDLE_PATH" "$DEST_USER@$DEST_HOST:$REMOTE_TMP"
|
||||||
|
ssh -o ConnectTimeout=10 -o BatchMode=yes "$DEST_USER@$DEST_HOST" "
|
||||||
|
mkdir -p '$DEST_DIR' &&
|
||||||
|
tar -xzf '$REMOTE_TMP' -C '$DEST_DIR' &&
|
||||||
|
rm -f '$REMOTE_TMP'
|
||||||
|
"
|
||||||
|
rm -f "$BUNDLE_PATH"
|
||||||
|
|
||||||
|
echo "✓ Deployment succeeded! Assets live at https://box.muse-dev.online/"
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
"""Tests for bin/deploy-box-web.sh."""
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import tarfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
SCRIPT_PATH = os.path.join(REPO_ROOT, "bin", "deploy-box-web.sh")
|
||||||
|
|
||||||
|
class TestDeployBoxWeb(unittest.TestCase):
|
||||||
|
def test_script_exists_and_executable(self):
|
||||||
|
self.assertTrue(os.path.exists(SCRIPT_PATH), f"Missing script: {SCRIPT_PATH}")
|
||||||
|
self.assertTrue(os.access(SCRIPT_PATH, os.X_OK), "Script not executable")
|
||||||
|
|
||||||
|
def test_bash_syntax(self):
|
||||||
|
proc = subprocess.run(["bash", "-n", SCRIPT_PATH], capture_output=True, text=True)
|
||||||
|
self.assertEqual(proc.returncode, 0, f"Syntax error in script: {proc.stderr}")
|
||||||
|
|
||||||
|
def test_dry_run_output(self):
|
||||||
|
proc = subprocess.run([SCRIPT_PATH, "--dry-run"], capture_output=True, text=True)
|
||||||
|
self.assertEqual(proc.returncode, 0, f"Dry-run failed: {proc.stderr}")
|
||||||
|
self.assertIn("Box Web Console Deployment Plan", proc.stdout)
|
||||||
|
self.assertIn("34.139.37.135", proc.stdout)
|
||||||
|
self.assertIn("/srv/box/www", proc.stdout)
|
||||||
|
self.assertIn("index.html", proc.stdout)
|
||||||
|
self.assertIn("box.js", proc.stdout)
|
||||||
|
self.assertIn("box.css", proc.stdout)
|
||||||
|
self.assertIn("DRY-RUN (no changes)", proc.stdout)
|
||||||
|
self.assertIn("sha256:", proc.stdout)
|
||||||
|
|
||||||
|
def test_custom_parameters(self):
|
||||||
|
cmd = [
|
||||||
|
SCRIPT_PATH,
|
||||||
|
"--host", "custom.domain.net",
|
||||||
|
"--user", "deployer",
|
||||||
|
"--dest", "/opt/box/web",
|
||||||
|
"--dry-run"
|
||||||
|
]
|
||||||
|
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||||
|
self.assertEqual(proc.returncode, 0)
|
||||||
|
self.assertIn("deployer@custom.domain.net:/opt/box/web", proc.stdout)
|
||||||
|
|
||||||
|
def test_bundle_only(self):
|
||||||
|
proc = subprocess.run([SCRIPT_PATH, "--bundle-only"], capture_output=True, text=True)
|
||||||
|
self.assertEqual(proc.returncode, 0, f"Bundle creation failed: {proc.stderr}")
|
||||||
|
self.assertIn("Archive created", proc.stdout)
|
||||||
|
# Extract bundle path from output
|
||||||
|
for line in proc.stdout.splitlines():
|
||||||
|
if "Bundle generated at" in line:
|
||||||
|
bundle_path = line.split("Bundle generated at")[1].strip().split()[0].rstrip(".")
|
||||||
|
self.assertTrue(os.path.exists(bundle_path))
|
||||||
|
with tarfile.open(bundle_path, "r:gz") as tar:
|
||||||
|
names = tar.getnames()
|
||||||
|
self.assertIn("index.html", names)
|
||||||
|
self.assertIn("box.js", names)
|
||||||
|
self.assertIn("box.css", names)
|
||||||
|
os.remove(bundle_path)
|
||||||
|
break
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user