-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdeploy_static.sh
More file actions
executable file
·203 lines (184 loc) · 6.92 KB
/
Copy pathdeploy_static.sh
File metadata and controls
executable file
·203 lines (184 loc) · 6.92 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
#!/usr/bin/env bash
# Sync the S3-served subtrees of static_export/ to the stats bucket.
#
# Two subtrees actually need to be in S3 in production:
#
# --static static_export/static/ → s3://$BUCKET/static/
# Browser-fetched via CloudFront (default behavior routes
# non-/api/* to the S3 origin). Contains config.json, tiles/,
# ranges/, zip/, admin/.
# Cache-Control matches backend/api.py:LongCacheStaticMiddleware:
# zip/, admin/ → 1yr immutable
# tiles/, ranges/, config.json → 300s must-revalidate
# A single /static/* CloudFront invalidation runs after upload.
#
# --forecast static_export/forecast/ → s3://$BUCKET/$FORECAST_PREFIX/
# Read by the Fargate backend via forecast_store_from_env
# (refreshes every ~5 min), so daily forecast updates can ship
# without rebuilding the API image. FORECAST_PREFIX is the
# sibling of the DATA_S3_URI stats prefix:
# s3://bucket → s3://bucket/forecast/
# s3://bucket/static → s3://bucket/forecast/
# s3://bucket/a/b → s3://bucket/a/forecast/
# No CloudFront invalidation (backend reads S3 directly).
#
# What this script does NOT upload:
#
# * SPA shell (index.html, assets/, favicons) — use ./deploy_frontend.sh.
# * static_export/data/ (verification .bin + grid.json) — baked into the API
# image by Dockerfile COPY and read from disk by the backend (see
# store_from_env in backend/static_store.py). Refreshed by rebuilding the
# image with ./deploy_fargate.sh.
#
# Default (no flags): both --static and --forecast.
#
# Env (loaded from .env if present):
# DATA_S3_URI s3://bucket[/prefix]
# CLOUDFRONT_DISTRIBUTION_ID required only when --static is enabled (unless SKIP_INVALIDATE=1)
# AWS_REGION default us-west-1
# AWS_PROFILE optional
# SKIP_BUILD=1 upload existing static_export/<subtree>/ (skip rebuild)
# SKIP_INVALIDATE=1 skip the /static/* CloudFront invalidation
#
# Usage:
# ./deploy_static.sh # static + forecast
# ./deploy_static.sh --forecast # daily forecast refresh
# ./deploy_static.sh --static # tile/config/zip/admin refresh
# SKIP_BUILD=1 ./deploy_static.sh --static # upload pre-built static/
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT"
DO_STATIC=0
DO_FORECAST=0
for arg in "$@"; do
case "$arg" in
--static) DO_STATIC=1 ;;
--forecast) DO_FORECAST=1 ;;
*)
echo "ERROR: unknown argument: $arg" >&2
echo "Usage: $0 [--static] [--forecast]" >&2
exit 2
;;
esac
done
if [[ $DO_STATIC -eq 0 && $DO_FORECAST -eq 0 ]]; then
DO_STATIC=1; DO_FORECAST=1
fi
if [[ -f "$ROOT/.env" ]]; then
set -a
# shellcheck disable=SC1091
source "$ROOT/.env"
set +a
fi
if [[ -z "${DATA_S3_URI:-}" ]]; then
echo "ERROR: DATA_S3_URI not set (expected s3://bucket[/prefix])." >&2
exit 1
fi
# Parse DATA_S3_URI=s3://bucket[/stats_prefix] → (BUCKET, FORECAST_PREFIX).
# Mirror backend/static_store.py:forecast_store_from_env:
# parent = stats_prefix.rsplit("/", 1)[0] if "/" in stats_prefix else ""
# forecast_prefix = f"{parent}/forecast" if parent else "forecast"
WITHOUT_SCHEME="${DATA_S3_URI#s3://}"
BUCKET="${WITHOUT_SCHEME%%/*}"
if [[ "$WITHOUT_SCHEME" == "$BUCKET" ]]; then
STATS_PREFIX=""
else
STATS_PREFIX="${WITHOUT_SCHEME#*/}"
STATS_PREFIX="${STATS_PREFIX%/}"
fi
if [[ "$STATS_PREFIX" == *"/"* ]]; then
FORECAST_PREFIX="${STATS_PREFIX%/*}/forecast"
else
FORECAST_PREFIX="forecast"
fi
AWS_REGION="${AWS_REGION:-us-west-1}"
if [[ -n "${AWS_PROFILE:-}" ]]; then
export AWS_PROFILE
echo "==> Using AWS_PROFILE=${AWS_PROFILE}"
fi
SITE="$ROOT/static_export"
STATIC_SRC="$SITE/static"
FORECAST_SRC="$SITE/forecast"
CC_MUTABLE="public, max-age=300, must-revalidate"
CC_IMMUTABLE="public, max-age=31536000, immutable"
run_build() {
local mode="$1" src="$2"
if [[ -n "${SKIP_BUILD:-}" ]]; then
echo "==> SKIP_BUILD set — using existing $src"
return
fi
# Do NOT rm $src first. export_static.py writes via _write_*_if_changed
# helpers (export_static.py:_write_text_if_changed / _write_bytes_if_changed),
# so unchanged files keep their mtime and `aws s3 sync` skips them — the
# whole point of preserving 100k stable ZIP JSONs across deploys. A blanket
# wipe would also nuke static/admin/ (regenerated by scripts/fetch_admin_boundaries.py,
# not by export_static.py --static).
echo "==> Rebuilding $mode (export_static.py --$mode)"
python3 export_static.py "--$mode"
}
sync_static() {
if [[ ! -d "$STATIC_SRC" ]]; then
echo "ERROR: $STATIC_SRC does not exist — run without SKIP_BUILD." >&2
exit 1
fi
local dest="s3://$BUCKET/static"
# Immutable (zip codes + admin boundaries — change yearly at most).
for sub in zip admin; do
if [[ -d "$STATIC_SRC/$sub" ]]; then
echo "==> Sync $dest/$sub/ ($CC_IMMUTABLE)"
aws s3 sync "$STATIC_SRC/$sub" "$dest/$sub" \
--region "$AWS_REGION" --delete \
--cache-control "$CC_IMMUTABLE"
fi
done
# Mutable (refreshed each stats rerun).
for sub in tiles ranges; do
if [[ -d "$STATIC_SRC/$sub" ]]; then
echo "==> Sync $dest/$sub/ ($CC_MUTABLE)"
aws s3 sync "$STATIC_SRC/$sub" "$dest/$sub" \
--region "$AWS_REGION" --delete \
--cache-control "$CC_MUTABLE"
fi
done
if [[ -f "$STATIC_SRC/config.json" ]]; then
echo "==> Upload $dest/config.json ($CC_MUTABLE)"
aws s3 cp "$STATIC_SRC/config.json" "$dest/config.json" \
--region "$AWS_REGION" \
--cache-control "$CC_MUTABLE"
fi
}
sync_forecast() {
if [[ ! -d "$FORECAST_SRC" ]]; then
echo "ERROR: $FORECAST_SRC does not exist — run without SKIP_BUILD." >&2
exit 1
fi
local dest="s3://$BUCKET/$FORECAST_PREFIX"
echo "==> Sync $dest/ (--delete)"
aws s3 sync "$FORECAST_SRC" "$dest" \
--region "$AWS_REGION" --delete
}
if [[ $DO_STATIC -eq 1 ]]; then
run_build static "$STATIC_SRC"
sync_static
fi
if [[ $DO_FORECAST -eq 1 ]]; then
run_build forecast "$FORECAST_SRC"
sync_forecast
fi
if [[ $DO_STATIC -eq 1 && -z "${SKIP_INVALIDATE:-}" ]]; then
if [[ -z "${CLOUDFRONT_DISTRIBUTION_ID:-}" ]]; then
echo "ERROR: CLOUDFRONT_DISTRIBUTION_ID not set (use SKIP_INVALIDATE=1 to skip)." >&2
exit 1
fi
echo "==> CloudFront invalidation ($CLOUDFRONT_DISTRIBUTION_ID): /static/*"
aws cloudfront create-invalidation \
--distribution-id "$CLOUDFRONT_DISTRIBUTION_ID" \
--paths "/static/*" \
--query 'Invalidation.{Id:Id,Status:Status}' --output table
elif [[ $DO_STATIC -eq 1 ]]; then
echo "==> SKIP_INVALIDATE set — not invalidating CloudFront"
fi
if [[ $DO_FORECAST -eq 1 ]]; then
echo "==> Forecast: backend re-reads S3 within ~5 min."
fi
echo "==> Done"