+259
deploy.sh
+259
deploy.sh
···
1
+
#!/usr/bin/env bash
2
+
set -euo pipefail
3
+
4
+
# Bunny CDN Deploy Script for Grain SPA
5
+
# Builds the app and syncs dist/ to Bunny Storage
6
+
7
+
# Colors for output
8
+
RED='\033[0;31m'
9
+
GREEN='\033[0;32m'
10
+
YELLOW='\033[1;33m'
11
+
NC='\033[0m' # No Color
12
+
13
+
# Counters
14
+
UPLOADED=0
15
+
DELETED=0
16
+
17
+
# Parse arguments
18
+
DRY_RUN=false
19
+
VERBOSE=false
20
+
SKIP_BUILD=false
21
+
22
+
while [[ $# -gt 0 ]]; do
23
+
case $1 in
24
+
--dry-run)
25
+
DRY_RUN=true
26
+
shift
27
+
;;
28
+
--verbose|-v)
29
+
VERBOSE=true
30
+
shift
31
+
;;
32
+
--skip-build)
33
+
SKIP_BUILD=true
34
+
shift
35
+
;;
36
+
*)
37
+
echo -e "${RED}Unknown option: $1${NC}"
38
+
exit 1
39
+
;;
40
+
esac
41
+
done
42
+
43
+
# Load .env file if it exists
44
+
if [ -f .env ]; then
45
+
set -a
46
+
source .env
47
+
set +a
48
+
fi
49
+
50
+
# Required environment variables
51
+
: "${BUNNY_API_KEY:?BUNNY_API_KEY environment variable is required}"
52
+
: "${BUNNY_STORAGE_PASSWORD:?BUNNY_STORAGE_PASSWORD environment variable is required}"
53
+
: "${BUNNY_STORAGE_ZONE:?BUNNY_STORAGE_ZONE environment variable is required}"
54
+
: "${BUNNY_STORAGE_HOST:?BUNNY_STORAGE_HOST environment variable is required}"
55
+
: "${BUNNY_PULLZONE_ID:?BUNNY_PULLZONE_ID environment variable is required}"
56
+
57
+
# Configuration
58
+
STORAGE_URL="https://${BUNNY_STORAGE_HOST}/${BUNNY_STORAGE_ZONE}"
59
+
DIST_DIR="dist"
60
+
61
+
echo "Bunny CDN Deploy - Grain SPA"
62
+
echo "========================================"
63
+
echo "Storage Zone: ${BUNNY_STORAGE_ZONE}"
64
+
if [ "$DRY_RUN" = true ]; then
65
+
echo -e "${YELLOW}DRY RUN MODE - No changes will be made${NC}"
66
+
fi
67
+
echo ""
68
+
69
+
# Get content type for file
70
+
get_content_type() {
71
+
local file="$1"
72
+
case "$file" in
73
+
*.html) echo "text/html" ;;
74
+
*.js) echo "application/javascript" ;;
75
+
*.css) echo "text/css" ;;
76
+
*.json) echo "application/json" ;;
77
+
*.svg) echo "image/svg+xml" ;;
78
+
*.png) echo "image/png" ;;
79
+
*.jpg|*.jpeg) echo "image/jpeg" ;;
80
+
*.gif) echo "image/gif" ;;
81
+
*.webp) echo "image/webp" ;;
82
+
*.woff) echo "font/woff" ;;
83
+
*.woff2) echo "font/woff2" ;;
84
+
*.ttf) echo "font/ttf" ;;
85
+
*.ico) echo "image/x-icon" ;;
86
+
*) echo "application/octet-stream" ;;
87
+
esac
88
+
}
89
+
90
+
# Upload a single file
91
+
upload_file() {
92
+
local local_path="$1"
93
+
local remote_path="$2"
94
+
95
+
if [ "$VERBOSE" = true ]; then
96
+
echo " Uploading: ${remote_path}"
97
+
fi
98
+
99
+
if [ "$DRY_RUN" = true ]; then
100
+
((UPLOADED++))
101
+
return 0
102
+
fi
103
+
104
+
local content_type
105
+
content_type=$(get_content_type "$local_path")
106
+
107
+
local http_code
108
+
http_code=$(curl -s -o /dev/null -w "%{http_code}" -X PUT \
109
+
"${STORAGE_URL}/${remote_path}" \
110
+
-H "AccessKey: ${BUNNY_STORAGE_PASSWORD}" \
111
+
-H "Content-Type: ${content_type}" \
112
+
--data-binary "@${local_path}")
113
+
114
+
if [[ "$http_code" =~ ^2 ]]; then
115
+
((UPLOADED++))
116
+
return 0
117
+
else
118
+
echo -e "${RED}Failed to upload ${remote_path}: HTTP ${http_code}${NC}"
119
+
return 1
120
+
fi
121
+
}
122
+
123
+
# List all files in remote storage recursively
124
+
list_remote_files() {
125
+
local path="${1:-}"
126
+
local response
127
+
response=$(curl -s -X GET "${STORAGE_URL}/${path}" \
128
+
-H "AccessKey: ${BUNNY_STORAGE_PASSWORD}" \
129
+
-H "Accept: application/json")
130
+
131
+
echo "$response" | jq -r '.[] | if .IsDirectory then .ObjectName + "/" else .ObjectName end' 2>/dev/null | while read -r item; do
132
+
if [[ "$item" == */ ]]; then
133
+
# It's a directory, recurse
134
+
list_remote_files "${path}${item}"
135
+
else
136
+
echo "${path}${item}"
137
+
fi
138
+
done
139
+
}
140
+
141
+
# Delete a single file from remote
142
+
delete_file() {
143
+
local remote_path="$1"
144
+
145
+
if [ "$VERBOSE" = true ]; then
146
+
echo " Deleting: ${remote_path}"
147
+
fi
148
+
149
+
if [ "$DRY_RUN" = true ]; then
150
+
((DELETED++))
151
+
return 0
152
+
fi
153
+
154
+
local http_code
155
+
http_code=$(curl -s -o /dev/null -w "%{http_code}" -X DELETE \
156
+
"${STORAGE_URL}/${remote_path}" \
157
+
-H "AccessKey: ${BUNNY_STORAGE_PASSWORD}")
158
+
159
+
if [[ "$http_code" =~ ^2 ]]; then
160
+
((DELETED++))
161
+
return 0
162
+
else
163
+
echo -e "${RED}Failed to delete ${remote_path}: HTTP ${http_code}${NC}"
164
+
return 1
165
+
fi
166
+
}
167
+
168
+
# Purge pull zone cache
169
+
purge_cache() {
170
+
echo "Purging CDN cache..."
171
+
172
+
if [ "$DRY_RUN" = true ]; then
173
+
echo -e "${YELLOW} Would purge pull zone ${BUNNY_PULLZONE_ID}${NC}"
174
+
return 0
175
+
fi
176
+
177
+
local http_code
178
+
http_code=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
179
+
"https://api.bunny.net/pullzone/${BUNNY_PULLZONE_ID}/purgeCache" \
180
+
-H "AccessKey: ${BUNNY_API_KEY}" \
181
+
-H "Content-Type: application/json")
182
+
183
+
if [[ "$http_code" =~ ^2 ]]; then
184
+
echo -e "${GREEN} Cache purged successfully${NC}"
185
+
return 0
186
+
else
187
+
echo -e "${RED} Failed to purge cache: HTTP ${http_code}${NC}"
188
+
return 1
189
+
fi
190
+
}
191
+
192
+
# ============================================
193
+
# MAIN EXECUTION
194
+
# ============================================
195
+
196
+
# Step 0: Build the app
197
+
if [ "$SKIP_BUILD" = false ]; then
198
+
echo "Building app..."
199
+
npm run build
200
+
echo ""
201
+
fi
202
+
203
+
# Check dist directory exists
204
+
if [ ! -d "$DIST_DIR" ]; then
205
+
echo -e "${RED}Error: ${DIST_DIR} directory not found. Run npm run build first.${NC}"
206
+
exit 1
207
+
fi
208
+
209
+
# Build list of local files
210
+
LOCAL_FILES_LIST=$(mktemp)
211
+
REMOTE_PATHS_LIST=$(mktemp)
212
+
trap "rm -f $LOCAL_FILES_LIST $REMOTE_PATHS_LIST" EXIT
213
+
214
+
# Find all files in dist
215
+
find "$DIST_DIR" -type f | while read -r file; do
216
+
# Get path relative to dist/
217
+
remote_path="${file#${DIST_DIR}/}"
218
+
echo "$file|$remote_path" >> "$LOCAL_FILES_LIST"
219
+
echo "$remote_path" >> "$REMOTE_PATHS_LIST"
220
+
done
221
+
222
+
# Step 1: Upload all local files
223
+
echo "Uploading files..."
224
+
while IFS='|' read -r local_path remote_path; do
225
+
upload_file "$local_path" "$remote_path"
226
+
done < "$LOCAL_FILES_LIST"
227
+
228
+
echo ""
229
+
230
+
# Step 2: Delete orphaned remote files
231
+
echo "Checking for orphaned files..."
232
+
REMOTE_FILES=$(list_remote_files)
233
+
234
+
if [ -n "$REMOTE_FILES" ]; then
235
+
while IFS= read -r remote_file; do
236
+
if [ -z "$remote_file" ]; then
237
+
continue
238
+
fi
239
+
if ! grep -qxF "$remote_file" "$REMOTE_PATHS_LIST"; then
240
+
delete_file "$remote_file"
241
+
fi
242
+
done <<< "$REMOTE_FILES"
243
+
fi
244
+
245
+
echo ""
246
+
247
+
# Step 3: Purge CDN cache
248
+
purge_cache
249
+
250
+
# Summary
251
+
echo ""
252
+
echo "========================================"
253
+
echo -e "${GREEN}Deploy complete!${NC}"
254
+
echo " Uploaded: ${UPLOADED} files"
255
+
echo " Deleted: ${DELETED} files"
256
+
if [ "$DRY_RUN" = true ]; then
257
+
echo -e "${YELLOW} (DRY RUN - no actual changes made)${NC}"
258
+
fi
259
+
echo "========================================"