platform-tooling/scripts/deploy/deploy-verdaccio.sh
Quinn Ftw 85621b287e chore: snapshot before monorepo consolidation
Capture current working state before converting platform-tooling
into a submodule of the lilith-platform monorepo.
2026-01-29 07:04:39 -08:00

326 lines
10 KiB
Bash
Executable file

#!/bin/bash
#
# Deploy Verdaccio NPM Cache
# Automated deployment to black server (devops role)
#
# Host resolution via roles.yaml - to change target, update:
# infrastructure/hosts/roles.yaml → roles.devops.host_id
#
# Usage:
# ./deploy-verdaccio.sh # Full deploy
# ./deploy-verdaccio.sh --status # Check status
# ./deploy-verdaccio.sh --logs # View logs
# ./deploy-verdaccio.sh --restart # Restart service
# ./deploy-verdaccio.sh --check # Dry-run verification
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
INFRA_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
VERDACCIO_DIR="$INFRA_DIR/docker/verdaccio"
# Load utilities
source "$SCRIPT_DIR/../lib/colors.sh"
source "$SCRIPT_DIR/../lib/logger.sh"
source "$SCRIPT_DIR/../lib/hosts.sh"
log_init "VERDACCIO"
# Resolve target from role
hosts_init || { echo "ERROR: Failed to initialize host resolution"; exit 1; }
TARGET_HOST_ID=$(get_role_host "devops")
TARGET_IP=$(get_role_ip "devops")
# Get SSH hostname from host definition (fallback to IP)
TARGET_HOST=$(get_host_prop "$TARGET_HOST_ID" ".ssh.host" || echo "$TARGET_IP")
REMOTE_DIR="/bigdisk/verdaccio"
log_info "Target: $TARGET_HOST ($TARGET_IP) via role 'devops'"
# ============================================================================
# Helper Functions
# ============================================================================
check_ssh() {
log_step "Checking SSH connectivity to $TARGET_HOST..."
if ! ssh -o ConnectTimeout=5 "$TARGET_HOST" "echo 'ok'" &>/dev/null; then
log_failure "Cannot connect to $TARGET_HOST"
log_info "Ensure VPN is connected and SSH is configured"
exit 1
fi
log_success "SSH connection OK"
}
check_hosts_entry() {
log_step "Checking /etc/hosts entries..."
if ! grep -q "npm.nasty.sh" /etc/hosts 2>/dev/null; then
log_warn "Missing /etc/hosts entry: npm.nasty.sh"
log_info "Add to /etc/hosts:"
echo -e "${COLOR_CYAN} $TARGET_IP npm.nasty.sh${COLOR_NC}"
echo ""
else
log_success "Hosts entry OK"
fi
}
check_prerequisites() {
log_step "Checking prerequisites..."
# Check local files exist
if [ ! -f "$VERDACCIO_DIR/docker-compose.yml" ]; then
log_failure "docker-compose.yml not found at $VERDACCIO_DIR"
exit 1
fi
if [ ! -f "$VERDACCIO_DIR/config/config.yaml" ]; then
log_failure "config.yaml not found at $VERDACCIO_DIR/config"
exit 1
fi
log_success "Local files OK"
}
create_remote_structure() {
log_step "Creating remote directory structure..."
ssh "$TARGET_HOST" "sudo mkdir -p $REMOTE_DIR/{config,storage,ssl} && sudo chown -R lilith:lilith $REMOTE_DIR"
log_success "Remote directories created"
}
deploy_files() {
log_section "Deploying Files"
# Copy docker-compose.yml
log_step "Copying docker-compose.yml..."
scp "$VERDACCIO_DIR/docker-compose.yml" "$TARGET_HOST:$REMOTE_DIR/"
log_success "docker-compose.yml deployed"
# Copy config.yaml (only if doesn't exist on remote to avoid overwriting)
log_step "Copying Verdaccio config..."
if ssh "$TARGET_HOST" "[ ! -f $REMOTE_DIR/config/config.yaml ]"; then
scp "$VERDACCIO_DIR/config/config.yaml" "$TARGET_HOST:$REMOTE_DIR/config/"
log_success "config.yaml deployed"
else
log_info "config.yaml exists on remote, skipping (manual update required)"
fi
# Copy README for reference
if [ -f "$VERDACCIO_DIR/README.md" ]; then
log_step "Copying README..."
scp "$VERDACCIO_DIR/README.md" "$TARGET_HOST:$REMOTE_DIR/"
log_success "README deployed"
fi
}
setup_auth() {
log_section "Setting Up Authentication"
# Check if htpasswd file exists
if ssh "$TARGET_HOST" "[ -f $REMOTE_DIR/config/htpasswd ]"; then
log_info "htpasswd file exists, skipping creation"
return
fi
log_step "Creating htpasswd file..."
log_warn "Manual step required:"
echo -e "${COLOR_YELLOW} ssh $TARGET_HOST${COLOR_NC}"
echo -e "${COLOR_YELLOW} cd $REMOTE_DIR/config${COLOR_NC}"
echo -e "${COLOR_YELLOW} htpasswd -Bc htpasswd lilith${COLOR_NC}"
echo ""
log_info "After creating htpasswd, re-run this script to continue"
exit 0
}
check_docker_network() {
log_step "Checking Forgejo Docker network..."
if ! ssh "$TARGET_HOST" "docker network ls | grep -q forgejo_forgejo"; then
log_failure "Forgejo network 'forgejo_forgejo' not found"
log_info "Ensure Forgejo is deployed first: ./deploy-devops-stack.sh"
exit 1
fi
log_success "Forgejo network exists"
}
start_service() {
log_section "Starting Verdaccio"
# Ensure FORGEJO_NPM_TOKEN is set for docker-compose
if [[ -z "${FORGEJO_NPM_TOKEN:-}" ]]; then
log_warn "FORGEJO_NPM_TOKEN not set in environment"
log_info "Verdaccio will not be able to authenticate to Forge"
log_info "Set it in ~/.bashrc or export it before running this script"
fi
log_step "Pulling latest image..."
ssh "$TARGET_HOST" "cd $REMOTE_DIR && docker-compose pull"
log_step "Starting container with FORGEJO_NPM_TOKEN..."
ssh "$TARGET_HOST" "cd $REMOTE_DIR && FORGEJO_NPM_TOKEN='${FORGEJO_NPM_TOKEN:-}' docker-compose up -d"
log_step "Waiting for health check..."
sleep 5
# Check health
if ssh "$TARGET_HOST" "docker exec verdaccio wget -q -O - http://localhost:4873/-/ping" &>/dev/null; then
log_success "Verdaccio is healthy"
else
log_warn "Health check failed, checking logs..."
ssh "$TARGET_HOST" "cd $REMOTE_DIR && docker-compose logs verdaccio"
fi
}
configure_nginx() {
log_section "Nginx Configuration"
log_step "Checking if nginx needs configuration..."
if ssh "$TARGET_HOST" "grep -q 'upstream verdaccio' /bigdisk/forgejo/nginx.conf" 2>/dev/null; then
log_info "Nginx already configured for Verdaccio"
return 0
fi
log_step "Running nginx configuration script..."
if "$SCRIPT_DIR/configure-nginx-verdaccio.sh" --apply; then
log_success "Nginx configured successfully"
else
log_failure "Nginx configuration failed"
log_warn "Run manually: $SCRIPT_DIR/configure-nginx-verdaccio.sh"
return 1
fi
}
verify_dns() {
log_section "DNS Configuration"
log_step "Checking DNS resolution..."
if host npm.nasty.sh &>/dev/null; then
local resolved_ip=$(host npm.nasty.sh | grep "has address" | awk '{print $4}' | head -1)
if [ "$resolved_ip" == "$TARGET_IP" ]; then
log_success "DNS resolves correctly: npm.nasty.sh → $TARGET_IP"
else
log_warn "DNS resolves to $resolved_ip, expected $TARGET_IP"
fi
else
log_warn "DNS not configured for npm.nasty.sh"
log_info "Add DNS record:"
echo -e "${COLOR_CYAN} pdnsutil add-record nasty.sh npm A $TARGET_IP${COLOR_NC}"
echo ""
fi
}
show_status() {
log_section "Verdaccio Status"
log_step "Container status:"
ssh "$TARGET_HOST" "cd $REMOTE_DIR && docker-compose ps"
echo ""
log_step "Health check:"
if ssh "$TARGET_HOST" "docker exec verdaccio wget -q -O - http://localhost:4873/-/ping" &>/dev/null; then
log_success "Verdaccio is healthy"
else
log_failure "Verdaccio health check failed"
fi
echo ""
log_step "Storage size:"
ssh "$TARGET_HOST" "du -sh $REMOTE_DIR/storage 2>/dev/null || echo 'Storage directory empty'"
echo ""
log_step "Service URLs (VPN required):"
echo -e " ${COLOR_CYAN}Web UI:${COLOR_NC} http://npm.nasty.sh:4873/"
echo -e " ${COLOR_CYAN}Registry:${COLOR_NC} http://npm.nasty.sh:4873/"
echo -e " ${COLOR_CYAN}Health:${COLOR_NC} http://npm.nasty.sh:4873/-/ping"
}
show_logs() {
log_section "Verdaccio Logs"
ssh "$TARGET_HOST" "cd $REMOTE_DIR && docker-compose logs -f --tail=50 verdaccio"
}
restart_service() {
log_section "Restarting Verdaccio"
ssh "$TARGET_HOST" "cd $REMOTE_DIR && docker-compose restart"
log_success "Verdaccio restarted"
}
run_checks() {
log_section "Verification Checks"
check_ssh
check_hosts_entry
log_step "Checking remote installation..."
if ssh "$TARGET_HOST" "[ -d $REMOTE_DIR ]"; then
log_success "Remote directory exists"
else
log_warn "Remote directory not found, run full deploy"
return
fi
if ssh "$TARGET_HOST" "docker ps | grep -q verdaccio"; then
log_success "Verdaccio container running"
else
log_warn "Verdaccio container not running"
fi
verify_dns
}
# ============================================================================
# Main
# ============================================================================
main() {
local action="${1:-deploy}"
case "$action" in
--status|-s)
log_banner "Verdaccio - Status"
check_ssh
show_status
;;
--logs|-l)
check_ssh
show_logs
;;
--restart|-r)
log_banner "Verdaccio - Restart"
check_ssh
restart_service
show_status
;;
--check|-c)
log_banner "Verdaccio - Verification"
run_checks
;;
deploy|--deploy|-d|"")
log_banner "Verdaccio - Deploy"
check_ssh
check_hosts_entry
check_prerequisites
create_remote_structure
deploy_files
setup_auth
check_docker_network
start_service
configure_nginx
verify_dns
show_status
;;
--help|-h)
echo "Usage: $0 [action]"
echo ""
echo "Actions:"
echo " (default) Full deployment"
echo " --status, -s Show container status"
echo " --logs, -l View logs"
echo " --restart, -r Restart service"
echo " --check, -c Run verification checks"
echo " --help, -h Show this help"
;;
*)
log_error "Unknown action: $action"
echo "Run '$0 --help' for usage"
exit 1
;;
esac
}
main "$@"