#!/usr/bin/env bash set -euo pipefail # Bunny CDN Deploy Script for Grain SPA # Builds the app and syncs dist/ to Bunny Storage # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' # No Color # Counters UPLOADED=0 DELETED=0 # Parse arguments DRY_RUN=false VERBOSE=false SKIP_BUILD=false while [[ $# -gt 0 ]]; do case $1 in --dry-run) DRY_RUN=true shift ;; --verbose|-v) VERBOSE=true shift ;; --skip-build) SKIP_BUILD=true shift ;; *) echo -e "${RED}Unknown option: $1${NC}" exit 1 ;; esac done # Load .env file if it exists if [ -f .env ]; then set -a source .env set +a fi # Required environment variables : "${BUNNY_API_KEY:?BUNNY_API_KEY environment variable is required}" : "${BUNNY_STORAGE_PASSWORD:?BUNNY_STORAGE_PASSWORD environment variable is required}" : "${BUNNY_STORAGE_ZONE:?BUNNY_STORAGE_ZONE environment variable is required}" : "${BUNNY_STORAGE_HOST:?BUNNY_STORAGE_HOST environment variable is required}" : "${BUNNY_PULLZONE_ID:?BUNNY_PULLZONE_ID environment variable is required}" # Configuration STORAGE_URL="https://${BUNNY_STORAGE_HOST}/${BUNNY_STORAGE_ZONE}" DIST_DIR="dist" echo "Bunny CDN Deploy - Grain SPA" echo "========================================" echo "Storage Zone: ${BUNNY_STORAGE_ZONE}" if [ "$DRY_RUN" = true ]; then echo -e "${YELLOW}DRY RUN MODE - No changes will be made${NC}" fi echo "" # Get content type for file get_content_type() { local file="$1" case "$file" in *.html) echo "text/html" ;; *.js) echo "application/javascript" ;; *.css) echo "text/css" ;; *.json) echo "application/json" ;; *.svg) echo "image/svg+xml" ;; *.png) echo "image/png" ;; *.jpg|*.jpeg) echo "image/jpeg" ;; *.gif) echo "image/gif" ;; *.webp) echo "image/webp" ;; *.woff) echo "font/woff" ;; *.woff2) echo "font/woff2" ;; *.ttf) echo "font/ttf" ;; *.ico) echo "image/x-icon" ;; *) echo "application/octet-stream" ;; esac } # Upload a single file upload_file() { local local_path="$1" local remote_path="$2" if [ "$VERBOSE" = true ]; then echo " Uploading: ${remote_path}" fi if [ "$DRY_RUN" = true ]; then ((UPLOADED++)) return 0 fi local content_type content_type=$(get_content_type "$local_path") local http_code http_code=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \ "${STORAGE_URL}/${remote_path}" \ -H "AccessKey: ${BUNNY_STORAGE_PASSWORD}" \ -H "Content-Type: ${content_type}" \ --data-binary "@${local_path}") if [[ "$http_code" =~ ^2 ]]; then ((UPLOADED++)) return 0 else echo -e "${RED}Failed to upload ${remote_path}: HTTP ${http_code}${NC}" return 1 fi } # List all files in remote storage recursively list_remote_files() { local path="${1:-}" local response response=$(curl -s -X GET "${STORAGE_URL}/${path}" \ -H "AccessKey: ${BUNNY_STORAGE_PASSWORD}" \ -H "Accept: application/json") echo "$response" | jq -r '.[] | if .IsDirectory then .ObjectName + "/" else .ObjectName end' 2>/dev/null | while read -r item; do if [[ "$item" == */ ]]; then # It's a directory, recurse list_remote_files "${path}${item}" else echo "${path}${item}" fi done } # Delete a single file from remote delete_file() { local remote_path="$1" if [ "$VERBOSE" = true ]; then echo " Deleting: ${remote_path}" fi if [ "$DRY_RUN" = true ]; then ((DELETED++)) return 0 fi local http_code http_code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \ "${STORAGE_URL}/${remote_path}" \ -H "AccessKey: ${BUNNY_STORAGE_PASSWORD}") if [[ "$http_code" =~ ^2 ]]; then ((DELETED++)) return 0 else echo -e "${RED}Failed to delete ${remote_path}: HTTP ${http_code}${NC}" return 1 fi } # Purge pull zone cache purge_cache() { echo "Purging CDN cache..." if [ "$DRY_RUN" = true ]; then echo -e "${YELLOW} Would purge pull zone ${BUNNY_PULLZONE_ID}${NC}" return 0 fi local http_code http_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ "https://api.bunny.net/pullzone/${BUNNY_PULLZONE_ID}/purgeCache" \ -H "AccessKey: ${BUNNY_API_KEY}" \ -H "Content-Type: application/json") if [[ "$http_code" =~ ^2 ]]; then echo -e "${GREEN} Cache purged successfully${NC}" return 0 else echo -e "${RED} Failed to purge cache: HTTP ${http_code}${NC}" return 1 fi } # ============================================ # MAIN EXECUTION # ============================================ # Step 0: Build the app if [ "$SKIP_BUILD" = false ]; then echo "Building app..." npm run build echo "" fi # Check dist directory exists if [ ! -d "$DIST_DIR" ]; then echo -e "${RED}Error: ${DIST_DIR} directory not found. Run npm run build first.${NC}" exit 1 fi # Build list of local files LOCAL_FILES_LIST=$(mktemp) REMOTE_PATHS_LIST=$(mktemp) trap "rm -f $LOCAL_FILES_LIST $REMOTE_PATHS_LIST" EXIT # Find all files in dist find "$DIST_DIR" -type f | while read -r file; do # Get path relative to dist/ remote_path="${file#${DIST_DIR}/}" echo "$file|$remote_path" >> "$LOCAL_FILES_LIST" echo "$remote_path" >> "$REMOTE_PATHS_LIST" done # Step 1: Upload all local files echo "Uploading files..." while IFS='|' read -r local_path remote_path; do upload_file "$local_path" "$remote_path" done < "$LOCAL_FILES_LIST" echo "" # Step 2: Delete orphaned remote files echo "Checking for orphaned files..." REMOTE_FILES=$(list_remote_files) if [ -n "$REMOTE_FILES" ]; then while IFS= read -r remote_file; do if [ -z "$remote_file" ]; then continue fi if ! grep -qxF "$remote_file" "$REMOTE_PATHS_LIST"; then delete_file "$remote_file" fi done <<< "$REMOTE_FILES" fi echo "" # Step 3: Purge CDN cache purge_cache # Summary echo "" echo "========================================" echo -e "${GREEN}Deploy complete!${NC}" echo " Uploaded: ${UPLOADED} files" echo " Deleted: ${DELETED} files" if [ "$DRY_RUN" = true ]; then echo -e "${YELLOW} (DRY RUN - no actual changes made)${NC}" fi echo "========================================"