react-native-bspatch 4.1.0 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -27
- package/android/build.gradle +25 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +86 -6
- package/android/src/main/res/values/bspatch_public_key.xml +5 -0
- package/cpp/bsdiff/bspatch_wrapper.c +34 -0
- package/cpp/bsdiff/bspatch_wrapper.h +2 -0
- package/cpp/bspatch_jni.cpp +18 -0
- package/ios/RNBsPatch.mm +142 -1
- package/lib/commonjs/NativeBsPatch.js.map +1 -1
- package/lib/commonjs/index.js +9 -2
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/ota/OtaProvider.js +165 -35
- package/lib/commonjs/ota/OtaProvider.js.map +1 -1
- package/lib/commonjs/ota/eligibility.js +47 -0
- package/lib/commonjs/ota/eligibility.js.map +1 -0
- package/lib/commonjs/ota/entrySignature.js +23 -0
- package/lib/commonjs/ota/entrySignature.js.map +1 -0
- package/lib/commonjs/ota/installId.js +27 -0
- package/lib/commonjs/ota/installId.js.map +1 -0
- package/lib/commonjs/ota/otaCore.js +96 -21
- package/lib/commonjs/ota/otaCore.js.map +1 -1
- package/lib/commonjs/ota/storage/{GitHubRawStorage.js → R2Storage.js} +20 -15
- package/lib/commonjs/ota/storage/R2Storage.js.map +1 -0
- package/lib/commonjs/ota/types.js.map +1 -1
- package/lib/module/NativeBsPatch.js.map +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/ota/OtaProvider.js +165 -35
- package/lib/module/ota/OtaProvider.js.map +1 -1
- package/lib/module/ota/eligibility.js +41 -0
- package/lib/module/ota/eligibility.js.map +1 -0
- package/lib/module/ota/entrySignature.js +17 -0
- package/lib/module/ota/entrySignature.js.map +1 -0
- package/lib/module/ota/installId.js +22 -0
- package/lib/module/ota/installId.js.map +1 -0
- package/lib/module/ota/otaCore.js +96 -21
- package/lib/module/ota/otaCore.js.map +1 -1
- package/lib/module/ota/storage/{GitHubRawStorage.js → R2Storage.js} +18 -14
- package/lib/module/ota/storage/R2Storage.js.map +1 -0
- package/lib/module/ota/types.js.map +1 -1
- package/lib/typescript/NativeBsPatch.d.ts +28 -2
- package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +3 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
- package/lib/typescript/ota/eligibility.d.ts +25 -0
- package/lib/typescript/ota/eligibility.d.ts.map +1 -0
- package/lib/typescript/ota/entrySignature.d.ts +9 -0
- package/lib/typescript/ota/entrySignature.d.ts.map +1 -0
- package/lib/typescript/ota/installId.d.ts +3 -0
- package/lib/typescript/ota/installId.d.ts.map +1 -0
- package/lib/typescript/ota/otaCore.d.ts.map +1 -1
- package/lib/typescript/ota/storage/{GitHubRawStorage.d.ts → R2Storage.d.ts} +4 -3
- package/lib/typescript/ota/storage/R2Storage.d.ts.map +1 -0
- package/lib/typescript/ota/types.d.ts +58 -5
- package/lib/typescript/ota/types.d.ts.map +1 -1
- package/package.json +3 -2
- package/react-native-bspatch.podspec +3 -1
- package/src/NativeBsPatch.ts +34 -2
- package/src/index.ts +5 -1
- package/src/ota/OtaProvider.tsx +167 -42
- package/src/ota/eligibility.ts +57 -0
- package/src/ota/entrySignature.ts +31 -0
- package/src/ota/installId.ts +18 -0
- package/src/ota/otaCore.ts +102 -21
- package/src/ota/storage/{GitHubRawStorage.ts → R2Storage.ts} +17 -13
- package/src/ota/types.ts +55 -5
- package/tools/build_ota.sh +343 -116
- package/tools/doctor.js +130 -0
- package/tools/ensure_signing_key.js +216 -0
- package/tools/fingerprint.js +111 -0
- package/tools/ios_fingerprint.sh +9 -0
- package/tools/ota_admin.js +131 -0
- package/tools/setup_project.js +41 -15
- package/tools/update_registry.js +111 -11
- package/tools/upload_ota.sh +85 -184
- package/tools/verify_publish.js +81 -0
- package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +0 -1
- package/lib/module/ota/storage/GitHubRawStorage.js.map +0 -1
- package/lib/typescript/ota/storage/GitHubRawStorage.d.ts.map +0 -1
package/tools/build_ota.sh
CHANGED
|
@@ -8,10 +8,16 @@
|
|
|
8
8
|
# Clients on the store build download the full bundle once, then patch.
|
|
9
9
|
#
|
|
10
10
|
# Usage (npx react-native-bspatch ...):
|
|
11
|
-
# patch [android|ios] [--flavor staging] [--release-version 1.0.4] [-y]
|
|
12
|
-
#
|
|
11
|
+
# patch [android|ios] [--flavor staging] [--release-version 1.0.4] [-m "notes"] [-y] [--reset-cache]
|
|
12
|
+
# [--dry-run] [--json] → build + publish the next OTA (both platforms if omitted)
|
|
13
|
+
# promote --from <flavor> [--source-version N] --flavor <target>
|
|
14
|
+
# → publish another flavor's bundle here, no build
|
|
15
|
+
# rollback --to N → publish vN's bundle again as the next version, no build
|
|
16
|
+
# status | doctor → what's live for this release / check the whole setup
|
|
17
|
+
# disable N | enable N | rollout N <percent> | mandatory N [--off] | qa N --ids a,b
|
|
18
|
+
# → release controls on the live version (registry only)
|
|
13
19
|
# reset [--flavor ...] [--push] → wipe this release's OTAs
|
|
14
|
-
# setup → wire OtaBundleResolver into
|
|
20
|
+
# setup → wire OtaBundleResolver + runtime fingerprint into the native project
|
|
15
21
|
# version <x.y.z> → set the native app version (package.json, Gradle, Xcode)
|
|
16
22
|
# The release version is read from the native project and confirmed by a prompt (Enter keeps it).
|
|
17
23
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -48,7 +54,19 @@ while [[ "$#" -gt 0 ]]; do
|
|
|
48
54
|
--release-version|--app-version) APP_VERSION_ARG="$2"; shift ;;
|
|
49
55
|
-y|--yes|--non-interactive) CONFIRM_VERSION=false ;;
|
|
50
56
|
--push) PUSH_REMOTE=true ;;
|
|
57
|
+
--reset-cache) RESET_CACHE=true ;;
|
|
58
|
+
-m|--message|--notes) export BSPATCH_NOTES="$2"; NOTES_GIVEN=true; shift ;;
|
|
59
|
+
--dry-run) DRY_RUN=true ;;
|
|
60
|
+
--json) JSON_OUT=true ;;
|
|
61
|
+
--from) SOURCE_FLAVOR="$2"; shift ;;
|
|
62
|
+
--source-version|--to) SOURCE_VERSION="$2"; shift ;;
|
|
63
|
+
--ids) ADMIN_IDS="$2"; shift ;;
|
|
64
|
+
--off) ADMIN_OFF=true ;;
|
|
51
65
|
patch) ACTION="ota" ;;
|
|
66
|
+
promote) ACTION="ota"; PUBLISH_MODE="promote" ;;
|
|
67
|
+
rollback) ACTION="ota"; PUBLISH_MODE="rollback" ;;
|
|
68
|
+
status|doctor|disable|enable|rollout|mandatory|qa) ACTION="admin"; ADMIN_CMD="$1" ;;
|
|
69
|
+
[0-9]*) ADMIN_ARGS+=("$1") ;;
|
|
52
70
|
android|ios) PLATFORM_ARG="$1" ;;
|
|
53
71
|
reset|setup) ACTION="$1" ;;
|
|
54
72
|
base|seed|apk|ipa|--apk|--ipa|*.apk|*.aab|*.ipa)
|
|
@@ -60,6 +78,17 @@ while [[ "$#" -gt 0 ]]; do
|
|
|
60
78
|
esac
|
|
61
79
|
shift
|
|
62
80
|
done
|
|
81
|
+
# promote: another flavor's latest (or --source-version) bundle; rollback: this flavor's vN
|
|
82
|
+
if [ "$PUBLISH_MODE" = "promote" ] && [ -z "$SOURCE_FLAVOR" ]; then
|
|
83
|
+
echo "❌ promote needs --from <flavor> (e.g. promote --from staging --flavor production)"; exit 1
|
|
84
|
+
fi
|
|
85
|
+
if [ "$PUBLISH_MODE" = "rollback" ]; then
|
|
86
|
+
if [ -z "$SOURCE_VERSION" ]; then echo "❌ rollback needs --to <version> (see 'status')"; exit 1; fi
|
|
87
|
+
SOURCE_FLAVOR="$FLAVOR"
|
|
88
|
+
fi
|
|
89
|
+
SOURCE_FLAVOR=$(echo "$SOURCE_FLAVOR" | tr '[:upper:]' '[:lower:]')
|
|
90
|
+
case "$SOURCE_FLAVOR" in dev) SOURCE_FLAVOR="development" ;; prod) SOURCE_FLAVOR="production" ;; esac
|
|
91
|
+
|
|
63
92
|
# Configuration
|
|
64
93
|
FLAVOR_LOWER=$(echo "$FLAVOR" | tr '[:upper:]' '[:lower:]')
|
|
65
94
|
if [ "$FLAVOR_LOWER" = "dev" ]; then FLAVOR_LOWER="development"; fi
|
|
@@ -67,19 +96,27 @@ if [ "$FLAVOR_LOWER" = "prod" ]; then FLAVOR_LOWER="production"; fi
|
|
|
67
96
|
|
|
68
97
|
# ── Output (exported: upload_ota.sh prints in the same layout) ────────────────
|
|
69
98
|
if [ -t 1 ]; then
|
|
70
|
-
BOLD=$'\033[1m'; DIM=$'\033[2m';
|
|
99
|
+
BOLD=$'\033[1m'; DIM=$'\033[2m'; RED=$'\033[31m'; GREEN=$'\033[38;5;84m'; YELLOW=$'\033[38;5;221m'
|
|
100
|
+
CYAN=$'\033[38;5;81m'; PINK=$'\033[38;5;213m'; RESET=$'\033[0m'
|
|
71
101
|
else
|
|
72
|
-
BOLD=""; DIM=""; GREEN=""; YELLOW=""; RESET=""
|
|
102
|
+
BOLD=""; DIM=""; RED=""; GREEN=""; YELLOW=""; CYAN=""; PINK=""; RESET=""
|
|
73
103
|
fi
|
|
74
104
|
# Pad to N characters, not bytes ("→" is 3 bytes, printf %-Ns counts bytes)
|
|
75
105
|
padr() { local LC_ALL=en_US.UTF-8; printf "%s%*s" "$1" $(( $2 > ${#1} ? $2 - ${#1} : 0 )) ""; }
|
|
76
|
-
info() { printf " ${
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
106
|
+
info() { printf " ${CYAN}%s${RESET} %s\n" "$(padr "$1" 9)" "$2"; } # " storage R2 bucket"
|
|
107
|
+
badge() { # <NAME> → " NAME " on a colored background (plain without a terminal)
|
|
108
|
+
local C
|
|
109
|
+
case "$1" in
|
|
110
|
+
BUILD) C="97;48;5;33" ;; DIFF) C="97;48;5;129" ;; SHIP) C="30;48;5;44" ;; DONE) C="30;48;5;42" ;; FAIL) C="97;48;5;160" ;;
|
|
111
|
+
IOS) C="97;48;5;240" ;; ANDROID) C="30;48;5;113" ;; *) C="97;48;5;57" ;;
|
|
112
|
+
esac
|
|
113
|
+
if [ -n "$RESET" ]; then printf "\033[1;%sm %-5s \033[0m" "$C" "$1"; else printf " %-5s " "$1"; fi
|
|
114
|
+
}
|
|
115
|
+
export BOLD DIM RED GREEN YELLOW CYAN PINK RESET
|
|
80
116
|
if [ "$ACTION" != "setup" ]; then
|
|
81
117
|
if [ "$ACTION" = "ota" ]; then TITLE="patch ${PLATFORM_ARG:-android + ios}"; else TITLE="$ACTION"; fi
|
|
82
|
-
|
|
118
|
+
PKG_VERSION=$(node -p "require(process.argv[1]).version" "$SCRIPT_DIR/../package.json" 2>/dev/null)
|
|
119
|
+
printf "\n%s ${BOLD}%s${RESET} ${DIM}·${RESET} ${PINK}%s${RESET} ${DIM}· v%s${RESET}\n\n" "$(badge BSPATCH)" "$TITLE" "$FLAVOR_LOWER" "$PKG_VERSION"
|
|
83
120
|
fi
|
|
84
121
|
|
|
85
122
|
# ── Environment ──────────────────────────────────────────────────────────────
|
|
@@ -121,19 +158,23 @@ if [ "$ACTION" = "setup" ]; then
|
|
|
121
158
|
fi
|
|
122
159
|
|
|
123
160
|
# ── Build environment ──────────────────────────────────────────────────────
|
|
124
|
-
if [ "$ACTION" = "ota" ]; then
|
|
161
|
+
if [ "$ACTION" = "ota" ] && [ -z "$SOURCE_FLAVOR" ]; then
|
|
125
162
|
# Kill any existing Metro processes to prevent file locks
|
|
126
163
|
pkill -f "react-native/local-cli/cli.js" || true
|
|
127
164
|
pkill -f "metro-config/src/index.js" || true
|
|
128
165
|
|
|
129
|
-
#
|
|
166
|
+
# A warm cache saves most of the bundle time. Metro's cache key doesn't cover .env files,
|
|
167
|
+
# so a babel plugin inlining env values would reuse another flavor's output: clear the
|
|
168
|
+
# cache when this flavor's env file changed since the last build, or on --reset-cache.
|
|
130
169
|
METRO_CACHE_DIR=$(node -e "console.log(require('os').tmpdir())")/metro-cache
|
|
131
|
-
|
|
132
|
-
|
|
170
|
+
METRO_KEY="$FLAVOR_LOWER $(shasum -a 256 < "$ROOT/.env.$FLAVOR_LOWER" | cut -c1-16)"
|
|
171
|
+
if [ "$RESET_CACHE" = true ] || [ "$(cat "$METRO_CACHE_DIR/.bspatch-env" 2>/dev/null)" != "$METRO_KEY" ]; then
|
|
172
|
+
rm -rf "$METRO_CACHE_DIR" # by hand: robust vs ENOTEMPTY
|
|
133
173
|
info metro "stopped · cache cleared"
|
|
134
174
|
else
|
|
135
|
-
info metro "stopped"
|
|
175
|
+
info metro "stopped · cache reused ${DIM}(--reset-cache to clear)${RESET}"
|
|
136
176
|
fi
|
|
177
|
+
mkdir -p "$METRO_CACHE_DIR" && echo "$METRO_KEY" > "$METRO_CACHE_DIR/.bspatch-env"
|
|
137
178
|
fi
|
|
138
179
|
|
|
139
180
|
mkdir -p "$ROOT/android/output"
|
|
@@ -147,35 +188,17 @@ if [ "$ACTION" != "setup" ]; then
|
|
|
147
188
|
BASE_URL="${OTA_BASE_URL:-}"
|
|
148
189
|
BASE_FOLDER="${OTA_FOLDER:-ota}"
|
|
149
190
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
r2() { curl -sS --aws-sigv4 "aws:amz:auto:s3" --user "${OTA_R2_ACCESS_KEY_ID:?}:${OTA_R2_SECRET_ACCESS_KEY:?}" "$@"; }
|
|
155
|
-
# S3 API, not the public URL: always the latest object, no CDN cache
|
|
156
|
-
API_HTTP=$(r2 -o "$REGISTRY_SHARED.tmp" -w "%{http_code}" "$R2_URL/$BASE_FOLDER/ota_registry.json" || echo 0)
|
|
157
|
-
else
|
|
158
|
-
if [[ "$BASE_URL" != "https://raw.githubusercontent.com/"* ]]; then
|
|
159
|
-
echo " ❌ Set OTA_R2_* (Cloudflare R2) or OTA_BASE_URL=https://raw.githubusercontent.com/<owner>/<repo>/<branch>"
|
|
160
|
-
exit 1
|
|
161
|
-
fi
|
|
162
|
-
CLEAN_URL="${BASE_URL#https://raw.githubusercontent.com/}"
|
|
163
|
-
GH_OWNER=$(echo "$CLEAN_URL" | cut -d'/' -f1)
|
|
164
|
-
GH_REPO=$(echo "$CLEAN_URL" | cut -d'/' -f2)
|
|
165
|
-
GH_BRANCH=$(echo "$CLEAN_URL" | cut -d'/' -f3)
|
|
166
|
-
if [[ -z "$GH_BRANCH" ]]; then GH_BRANCH="main"; fi
|
|
167
|
-
|
|
168
|
-
# Contents API (not the raw CDN) always returns the latest commit; the raw
|
|
169
|
-
# media type has no 1 MB limit and needs no base64 decoding.
|
|
170
|
-
GH_API_TOKEN="$(gh auth token 2>/dev/null || true)"
|
|
171
|
-
GH_API_TOKEN="${GH_API_TOKEN:-${GH_TOKEN:-${GITHUB_TOKEN:-}}}"
|
|
172
|
-
GH_API_AUTH=()
|
|
173
|
-
if [[ -n "$GH_API_TOKEN" ]]; then GH_API_AUTH=(-H "Authorization: Bearer $GH_API_TOKEN"); fi
|
|
174
|
-
|
|
175
|
-
API_HTTP=$(curl -s -o "$REGISTRY_SHARED.tmp" -w "%{http_code}" "${GH_API_AUTH[@]}" \
|
|
176
|
-
-H "Accept: application/vnd.github.raw" \
|
|
177
|
-
"https://api.github.com/repos/${GH_OWNER}/${GH_REPO}/contents/${BASE_FOLDER}/ota_registry.json?ref=${GH_BRANCH}" || echo 0)
|
|
191
|
+
# Cloudflare R2 is the only storage for now (until custom servers get their own support)
|
|
192
|
+
if [[ -z "${OTA_R2_BUCKET:-}" ]]; then
|
|
193
|
+
echo " ❌ OTA_R2_BUCKET not set: add OTA_R2_ACCOUNT_ID / OTA_R2_BUCKET / OTA_R2_ACCESS_KEY_ID / OTA_R2_SECRET_ACCESS_KEY to .env.ota"
|
|
194
|
+
exit 1
|
|
178
195
|
fi
|
|
196
|
+
info storage "R2 $OTA_R2_BUCKET → $BASE_URL/$BASE_FOLDER"
|
|
197
|
+
|
|
198
|
+
R2_URL="https://${OTA_R2_ACCOUNT_ID:?set OTA_R2_ACCOUNT_ID}.r2.cloudflarestorage.com/${OTA_R2_BUCKET}"
|
|
199
|
+
r2() { curl -sS --aws-sigv4 "aws:amz:auto:s3" --user "${OTA_R2_ACCESS_KEY_ID:?}:${OTA_R2_SECRET_ACCESS_KEY:?}" "$@"; }
|
|
200
|
+
# S3 API, not the public URL: always the latest object, no CDN cache
|
|
201
|
+
API_HTTP=$(r2 -o "$REGISTRY_SHARED.tmp" -w "%{http_code}" "$R2_URL/$BASE_FOLDER/ota_registry.json" || echo 0)
|
|
179
202
|
|
|
180
203
|
if [[ "$API_HTTP" == "200" ]] && node -e "JSON.parse(require('fs').readFileSync(process.argv[1],'utf8'))" "$REGISTRY_SHARED.tmp" 2>/dev/null; then
|
|
181
204
|
mv "$REGISTRY_SHARED.tmp" "$REGISTRY_SHARED"
|
|
@@ -195,20 +218,12 @@ if [ "$ACTION" != "setup" ]; then
|
|
|
195
218
|
fi
|
|
196
219
|
|
|
197
220
|
# ── Helper: Download from remote storage ───────────────────────────────────────
|
|
198
|
-
download_remote() {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if [[ -n "${OTA_R2_BUCKET:-}" ]]; then
|
|
203
|
-
r2 -f -o "$DEST.tmp" "$R2_URL/$REMOTE_PATH" 2>/dev/null || OK=1
|
|
221
|
+
download_remote() { # <remote path> <dest>
|
|
222
|
+
# --compressed: bundles are stored gzip Content-Encoded
|
|
223
|
+
if r2 -f --compressed -o "$2.tmp" "$R2_URL/$1" 2>/dev/null; then
|
|
224
|
+
mv "$2.tmp" "$2"
|
|
204
225
|
else
|
|
205
|
-
|
|
206
|
-
curl -s -f -H "Cache-Control: no-cache" -o "$DEST.tmp" "$BASE_URL/$REMOTE_PATH?cb=$(date +%s)" || OK=1
|
|
207
|
-
fi
|
|
208
|
-
if [ $OK -eq 0 ]; then
|
|
209
|
-
mv "$DEST.tmp" "$DEST"
|
|
210
|
-
else
|
|
211
|
-
rm -f "$DEST.tmp"
|
|
226
|
+
rm -f "$2.tmp"
|
|
212
227
|
return 1
|
|
213
228
|
fi
|
|
214
229
|
}
|
|
@@ -218,42 +233,102 @@ sha256() {
|
|
|
218
233
|
shasum -a 256 "$1" | awk '{print $1}'
|
|
219
234
|
}
|
|
220
235
|
|
|
221
|
-
# ── Progress
|
|
236
|
+
# ── Progress ───────────────────────────────────────────────────────────────────
|
|
237
|
+
# BUILD Bundle JS 5.8 MB ██████░░░░░░░░ 18s
|
|
238
|
+
# BUILD Hermes ⠹ 12s · <last log line> ← running (terminal only, redrawn in place)
|
|
239
|
+
# Bars are each step's share of the platform's total time, so finished rows show an empty
|
|
240
|
+
# bar and timeline_redraw fills them in after the last step.
|
|
222
241
|
human_size() {
|
|
223
242
|
awk -v b="$1" 'BEGIN { split("B KB MB GB", u); i = 1; while (b >= 1024 && i < 4) { b /= 1024; i++ }
|
|
224
243
|
printf (i == 1 ? "%d %s" : "%.1f %s"), b, u[i] }'
|
|
225
244
|
}
|
|
226
245
|
file_size() { wc -c < "$1" | tr -d ' '; }
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
246
|
+
row() { # <badge> <label> <detail> <time> [bar] [time color]
|
|
247
|
+
printf " %s %s ${BOLD}%s${RESET} %s${6:-$DIM}%4s${RESET}\n" "$(badge "$1")" "$(padr "$2" 11)" "$(padr "$3" 12)" "${5:+$5 }" "$4"
|
|
248
|
+
}
|
|
249
|
+
bar() { # <secs> <total secs> <badge>
|
|
250
|
+
local W=14 N=0 I C
|
|
251
|
+
if [ "$2" -gt 0 ]; then N=$(( ($1 * W + $2 - 1) / $2 )); fi
|
|
252
|
+
if [ "$N" -gt "$W" ]; then N=$W; fi
|
|
253
|
+
case "$3" in BUILD) C=75 ;; DIFF) C=177 ;; *) C=87 ;; esac
|
|
254
|
+
if [ -n "$RESET" ]; then printf "\033[38;5;%sm" "$C"; fi
|
|
255
|
+
for (( I = 0; I < N; I++ )); do printf "█"; done
|
|
256
|
+
printf "%s" "$RESET$DIM"
|
|
257
|
+
for (( I = N; I < W; I++ )); do printf "░"; done
|
|
258
|
+
printf "%s" "$RESET"
|
|
259
|
+
}
|
|
260
|
+
ROWS=() # "badge|label|detail|secs" of the current platform's finished steps
|
|
261
|
+
step() { # <label> <badge>
|
|
262
|
+
STEP_LABEL="$1"; STEP_BADGE="$2"; STEP_START=$(date +%s)
|
|
263
|
+
export STEP_LABEL STEP_BADGE STEP_START
|
|
264
|
+
step_progress
|
|
265
|
+
}
|
|
266
|
+
step_progress() { # [text]
|
|
267
|
+
[ -t 1 ] || return 0
|
|
268
|
+
local F=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏)
|
|
269
|
+
SPIN=$(( (${SPIN:-0} + 1) % 10 ))
|
|
270
|
+
printf "\r\033[K %s %s ${CYAN}%s${RESET} ${DIM}%s${RESET}" "$(badge "$STEP_BADGE")" "$(padr "$STEP_LABEL" 11)" "${F[$SPIN]}" "$1"
|
|
235
271
|
}
|
|
236
|
-
|
|
237
|
-
|
|
272
|
+
step_done() { # <detail>
|
|
273
|
+
local SECS=$(( $(date +%s) - STEP_START ))
|
|
274
|
+
ROWS+=("$STEP_BADGE|$STEP_LABEL|$1|$SECS")
|
|
275
|
+
if [ -t 1 ]; then printf "\r\033[K"; row "$STEP_BADGE" "$STEP_LABEL" "$1" "${SECS}s" "$(bar 0 0 "$STEP_BADGE")"
|
|
276
|
+
else row "$STEP_BADGE" "$STEP_LABEL" "$1" "${SECS}s"; fi
|
|
277
|
+
}
|
|
278
|
+
step_fail() { # [message]; error details go below
|
|
238
279
|
if [ -t 1 ]; then printf "\r\033[K"; fi
|
|
239
|
-
|
|
280
|
+
printf " %s %s ${RED}%s${RESET}\n" "$(badge FAIL)" "$(padr "$STEP_LABEL" 11)" "${1:-failed}"
|
|
281
|
+
}
|
|
282
|
+
timeline_redraw() { # rewrites the ROWS lines above the cursor with filled bars
|
|
283
|
+
[ -t 1 ] && [ ${#ROWS[@]} -gt 0 ] || return 0
|
|
284
|
+
local R B L D S TOTAL=0 MAX=0
|
|
285
|
+
for R in "${ROWS[@]}"; do
|
|
286
|
+
TOTAL=$(( TOTAL + ${R##*|} )); if [ "${R##*|}" -gt "$MAX" ]; then MAX=${R##*|}; fi
|
|
287
|
+
done
|
|
288
|
+
printf "\033[%dA" ${#ROWS[@]}
|
|
289
|
+
for R in "${ROWS[@]}"; do
|
|
290
|
+
IFS='|' read -r B L D S <<< "$R"
|
|
291
|
+
printf "\r\033[K"
|
|
292
|
+
# the slowest step stands out
|
|
293
|
+
if [ "$S" -eq "$MAX" ] && [ "$MAX" -gt 0 ]; then row "$B" "$L" "$D" "${S}s" "$(bar "$S" "$TOTAL" "$B")" "$BOLD$YELLOW"
|
|
294
|
+
else row "$B" "$L" "$D" "${S}s" "$(bar "$S" "$TOTAL" "$B")"; fi
|
|
295
|
+
done
|
|
240
296
|
}
|
|
241
|
-
|
|
242
|
-
|
|
297
|
+
detail() { printf "%10s%s %s\n" "" "$(padr "$1" 9)" "$2"; } # line under a step
|
|
298
|
+
vis_len() { # characters on screen: color codes don't count
|
|
299
|
+
local LC_ALL=en_US.UTF-8 S
|
|
300
|
+
S=$(printf "%s" "$1" | sed $'s/\033\\[[0-9;]*m//g')
|
|
301
|
+
printf "%s" "${#S}"
|
|
302
|
+
}
|
|
303
|
+
box() { # <line>… — rounded box sized to the widest line, border shading blue → green
|
|
304
|
+
local L W=0 N I EDGE="" PAL=(39 38 44 43 49 48 84 83 119 118)
|
|
305
|
+
for L in "$@"; do N=$(vis_len "$L"); if [ "$N" -gt "$W" ]; then W=$N; fi; done
|
|
306
|
+
local A="" Z=""; if [ -n "$RESET" ]; then A=$'\033[38;5;39m'; Z=$'\033[38;5;118m'; fi
|
|
307
|
+
for (( I = 0; I < W + 4; I++ )); do
|
|
308
|
+
if [ -n "$RESET" ]; then EDGE+=$'\033[38;5;'"${PAL[$(( I * 10 / (W + 4) ))]}m"; fi
|
|
309
|
+
EDGE+="─"
|
|
310
|
+
done
|
|
311
|
+
printf " ${A}╭%s${Z}╮${RESET}\n" "$EDGE"
|
|
312
|
+
for L in "$@"; do printf " ${A}│${RESET} %s%*s ${Z}│${RESET}\n" "$L" $((W - $(vis_len "$L"))) ""; done
|
|
313
|
+
printf " ${A}╰%s${Z}╯${RESET}\n" "$EDGE"
|
|
314
|
+
}
|
|
315
|
+
# [RUN_NOTE=…] run_logged LOG cmd… — output to LOG; in a terminal, spin with elapsed time + the log's last line
|
|
243
316
|
run_logged() {
|
|
244
317
|
local LOG="$1"; shift
|
|
245
318
|
"$@" > "$LOG" 2>&1 &
|
|
246
319
|
local PID=$!
|
|
247
320
|
if [ -t 1 ]; then
|
|
321
|
+
local W=$(( $(tput cols 2>/dev/null || echo 80) - 40 ))
|
|
248
322
|
while kill -0 "$PID" 2>/dev/null; do
|
|
249
|
-
local LAST=$(tail -n 1 "$LOG" 2>/dev/null | tr -d '\r\033' | cut -c1
|
|
250
|
-
step_progress "$((
|
|
251
|
-
sleep
|
|
323
|
+
local LAST=$(tail -n 1 "$LOG" 2>/dev/null | tr -d '\r\033' | cut -c1-$(( W > 10 ? W : 10 )))
|
|
324
|
+
step_progress "${RUN_NOTE:+$RUN_NOTE · }$(( $(date +%s) - STEP_START ))s${LAST:+ · $LAST}"
|
|
325
|
+
sleep 0.2
|
|
252
326
|
done
|
|
253
327
|
fi
|
|
254
328
|
wait "$PID"
|
|
255
329
|
}
|
|
256
|
-
|
|
330
|
+
# upload_ota.sh runs inside the Upload/Remote step: it spins, fails, and reports back through $BSPATCH_REPORT
|
|
331
|
+
export -f padr info badge human_size file_size step_progress step_fail run_logged
|
|
257
332
|
|
|
258
333
|
# ── Release version (native app version the OTA targets) ─────────────────────
|
|
259
334
|
android_version() {
|
|
@@ -294,6 +369,68 @@ if [ -z "$APP_VERSION" ]; then
|
|
|
294
369
|
fi
|
|
295
370
|
info release "$APP_VERSION"
|
|
296
371
|
|
|
372
|
+
# Release notes: shown in the summary and kept in release_*.json (public) for a dashboard
|
|
373
|
+
if [ "$ACTION" = "ota" ] && [ -t 0 ] && [ "$CONFIRM_VERSION" = true ] && [ -z "$NOTES_GIVEN" ]; then
|
|
374
|
+
read -r -p " ${BOLD}?${RESET} Release notes ${DIM}(optional, Enter to skip)${RESET}: " BSPATCH_NOTES
|
|
375
|
+
export BSPATCH_NOTES
|
|
376
|
+
fi
|
|
377
|
+
|
|
378
|
+
# ── Signing key: found, or created + embedded in the app (never replaces one in use) ──
|
|
379
|
+
if [ "$ACTION" = "ota" ]; then
|
|
380
|
+
KEY_FLAGS=(); if [ -t 0 ]; then KEY_FLAGS=(--interactive); fi
|
|
381
|
+
KEY_NOTE=$(node "$SCRIPT_DIR/ensure_signing_key.js" "$ROOT" "$REGISTRY_SHARED" "${KEY_FLAGS[@]}") || exit 1
|
|
382
|
+
info signing "$KEY_NOTE"
|
|
383
|
+
# Native wiring (resolver, fingerprint build step) kept current without a separate setup run
|
|
384
|
+
WIRED=$(node "$SCRIPT_DIR/setup_project.js" --root "$ROOT" --quiet | paste -sd ',' - | sed 's/,/, /g')
|
|
385
|
+
if [ -n "$WIRED" ]; then
|
|
386
|
+
info wiring "${YELLOW}updated: $WIRED${RESET} ${DIM}(commit; store builds from now on carry it)${RESET}"
|
|
387
|
+
else
|
|
388
|
+
info wiring "ok"
|
|
389
|
+
fi
|
|
390
|
+
fi
|
|
391
|
+
|
|
392
|
+
# ── Release controls, status, doctor (registry only, nothing is built) ──
|
|
393
|
+
if [ "$ACTION" = "admin" ]; then
|
|
394
|
+
if [ "$ADMIN_CMD" = "doctor" ]; then
|
|
395
|
+
exec node "$SCRIPT_DIR/doctor.js" "$ROOT" "$REGISTRY_SHARED" "$APP_VERSION" "$FLAVOR_LOWER" "$BASE_URL" "$BASE_FOLDER"
|
|
396
|
+
fi
|
|
397
|
+
RELEASES_DIR=$(mktemp -d)
|
|
398
|
+
if [ "$ADMIN_CMD" = "status" ]; then
|
|
399
|
+
# The live version's release_*.json per platform, for who/when/notes/sizes
|
|
400
|
+
for F in $(node -e '
|
|
401
|
+
const [reg, app, flavor] = process.argv.slice(1);
|
|
402
|
+
const g = JSON.parse(require("fs").readFileSync(reg, "utf8")).apps?.[app]?.flavors?.[flavor];
|
|
403
|
+
for (const p of new Set((g?.patches || []).map(e => e.platform)))
|
|
404
|
+
console.log(`release_${app}_v${g.latestOtaVersion}_${p}_${flavor}.json`);
|
|
405
|
+
' "$REGISTRY_SHARED" "$APP_VERSION" "$FLAVOR_LOWER"); do
|
|
406
|
+
download_remote "$BASE_FOLDER/$APP_VERSION/$FLAVOR_LOWER/$F" "$RELEASES_DIR/$F" || true
|
|
407
|
+
done
|
|
408
|
+
fi
|
|
409
|
+
ADMIN_FLAGS=(); if [ -n "$PLATFORM_ARG" ]; then ADMIN_FLAGS+=(--platform "$PLATFORM_ARG"); fi
|
|
410
|
+
if [ -n "$ADMIN_IDS" ]; then ADMIN_FLAGS+=(--ids "$ADMIN_IDS"); fi
|
|
411
|
+
if [ "$ADMIN_OFF" = true ]; then ADMIN_FLAGS+=(--off); fi
|
|
412
|
+
if [ "$JSON_OUT" = true ]; then ADMIN_FLAGS+=(--json); fi
|
|
413
|
+
printf "\n"
|
|
414
|
+
set +e
|
|
415
|
+
node "$SCRIPT_DIR/ota_admin.js" "$ADMIN_CMD" "$REGISTRY_SHARED" "$APP_VERSION" "$FLAVOR_LOWER" "$RELEASES_DIR" \
|
|
416
|
+
"${ADMIN_FLAGS[@]}" "${ADMIN_ARGS[@]}"
|
|
417
|
+
ADMIN_EXIT=$?
|
|
418
|
+
set -e
|
|
419
|
+
rm -rf "$RELEASES_DIR"
|
|
420
|
+
# Exit 3: the registry changed and goes back to storage
|
|
421
|
+
if [ $ADMIN_EXIT -eq 3 ]; then
|
|
422
|
+
if [ "$DRY_RUN" = true ]; then
|
|
423
|
+
info registry "not uploaded ${DIM}(dry run)${RESET}"
|
|
424
|
+
else
|
|
425
|
+
r2 -f -X PUT -T "$REGISTRY_SHARED" -H "Cache-Control: no-cache" "$R2_URL/$BASE_FOLDER/ota_registry.json" -o /dev/null \
|
|
426
|
+
|| { echo " ❌ Uploading the registry failed"; exit 1; }
|
|
427
|
+
info registry "uploaded · clients see it on their next check"
|
|
428
|
+
fi
|
|
429
|
+
exit 0
|
|
430
|
+
fi
|
|
431
|
+
exit $ADMIN_EXIT
|
|
432
|
+
fi
|
|
433
|
+
|
|
297
434
|
# ── Reset ─────────────────────────────────────────────────────────────────────
|
|
298
435
|
if [ "$ACTION" = "reset" ]; then
|
|
299
436
|
# Reset only the specific flavor in registry JSON if registry exists
|
|
@@ -326,7 +463,12 @@ if [ "$ACTION" = "reset" ]; then
|
|
|
326
463
|
info local "cleared $APP_VERSION/$FLAVOR_LOWER · next OTA is v$((LATEST + 1))"
|
|
327
464
|
|
|
328
465
|
if [ "$PUSH_REMOTE" = true ]; then
|
|
329
|
-
|
|
466
|
+
printf "\n"; step "Remote" SHIP
|
|
467
|
+
export BSPATCH_REPORT=$(mktemp)
|
|
468
|
+
bash "$SCRIPT_DIR/upload_ota.sh" "$APP_VERSION" "$FLAVOR_LOWER" "0" "" "ios" "--clean-remote" || exit 1
|
|
469
|
+
. "$BSPATCH_REPORT"; rm -f "$BSPATCH_REPORT"
|
|
470
|
+
step_done "$UP_SIZE"
|
|
471
|
+
if [ -n "$UP_NOTE" ]; then detail "" "${DIM}$UP_NOTE${RESET}"; fi
|
|
330
472
|
else
|
|
331
473
|
info remote "unchanged ${DIM}(add --push to delete on storage)${RESET}"
|
|
332
474
|
fi
|
|
@@ -369,8 +511,8 @@ build_platform() {
|
|
|
369
511
|
if [ "$PLATFORM" == "ios" ]; then OUTPUT="$ROOT/ios/output"; fi
|
|
370
512
|
local REGISTRY="$REGISTRY_SHARED"
|
|
371
513
|
|
|
372
|
-
printf "\n${BOLD}%s${RESET} · %s
|
|
373
|
-
|
|
514
|
+
printf "\n%s ${BOLD}%s${RESET} ${DIM}· v%s →${RESET} ${BOLD}${GREEN}v%s${RESET}\n\n" "$(badge "$(echo "$PLATFORM" | tr '[:lower:]' '[:upper:]')")" "$APP_VERSION" "$CURRENT" "$NEXT"
|
|
515
|
+
ROWS=()
|
|
374
516
|
local PLATFORM_START=$SECONDS
|
|
375
517
|
|
|
376
518
|
# ── Bundle ──
|
|
@@ -378,20 +520,60 @@ build_platform() {
|
|
|
378
520
|
local WORKSPACE="$OUTPUT/workspace"
|
|
379
521
|
mkdir -p "$WORKSPACE"
|
|
380
522
|
|
|
523
|
+
local BUNDLE_NAME="bundle_${APP_VERSION}_v${NEXT}_${PLATFORM}_${FLAVOR_LOWER}.hbc"
|
|
524
|
+
local NEW_BUNDLE_PATH="$OUTPUT/$BUNDLE_NAME"
|
|
525
|
+
local ASSET_MANIFEST_FILE="$OUTPUT/asset-manifest_${APP_VERSION}_v${NEXT}_${PLATFORM}_${FLAVOR_LOWER}.json"
|
|
526
|
+
local ASSET_STORE_DIR="$OUTPUT/asset-store"
|
|
527
|
+
# This version's assets only: upload checks each file, earlier versions' are already on storage
|
|
528
|
+
rm -rf "$ASSET_STORE_DIR"; mkdir -p "$ASSET_STORE_DIR"
|
|
529
|
+
unset BSPATCH_FINGERPRINT
|
|
530
|
+
|
|
531
|
+
if [ -n "$SOURCE_FLAVOR" ]; then
|
|
532
|
+
# ── Source: a bundle already on storage (promote / rollback), nothing is built ──
|
|
533
|
+
local SRC_V="${SOURCE_VERSION:-$(node -e '
|
|
534
|
+
const [reg, app, flavor] = process.argv.slice(1);
|
|
535
|
+
console.log(JSON.parse(require("fs").readFileSync(reg, "utf8")).apps?.[app]?.flavors?.[flavor]?.latestOtaVersion || "");
|
|
536
|
+
' "$REGISTRY_SHARED" "$APP_VERSION" "$SOURCE_FLAVOR")}"
|
|
537
|
+
local SRC_PREFIX="$BASE_FOLDER/$APP_VERSION/$SOURCE_FLAVOR"
|
|
538
|
+
local SRC_SUFFIX="${APP_VERSION}_v${SRC_V}_${PLATFORM}_${SOURCE_FLAVOR}"
|
|
539
|
+
step "Source" BUILD
|
|
540
|
+
if [ -z "$SRC_V" ]; then step_fail "$SOURCE_FLAVOR has no OTA for $APP_VERSION"; exit 1; fi
|
|
541
|
+
step_progress "fetching $SOURCE_FLAVOR v$SRC_V…"
|
|
542
|
+
if ! download_remote "$SRC_PREFIX/bundle_$SRC_SUFFIX.hbc.gz" "$WORKSPACE/source.hbc.gz"; then
|
|
543
|
+
step_fail "$SOURCE_FLAVOR v$SRC_V has no $PLATFORM bundle on storage"; exit 1
|
|
544
|
+
fi
|
|
545
|
+
gunzip -c "$WORKSPACE/source.hbc.gz" > "$NEW_BUNDLE_PATH"
|
|
546
|
+
# Assets are content-addressed and already on storage: its manifest is all this needs
|
|
547
|
+
if ! download_remote "$SRC_PREFIX/asset-manifest_$SRC_SUFFIX.json" "$ASSET_MANIFEST_FILE"; then
|
|
548
|
+
step_fail "$SOURCE_FLAVOR v$SRC_V has no asset manifest on storage"; exit 1
|
|
549
|
+
fi
|
|
550
|
+
# Its fingerprint, not this checkout's: the bundle was built from that commit
|
|
551
|
+
if download_remote "$SRC_PREFIX/release_$SRC_SUFFIX.json" "$WORKSPACE/source-release.json"; then
|
|
552
|
+
export BSPATCH_FINGERPRINT=$(node -p 'require(process.argv[1]).runtimeFingerprint || ""' "$WORKSPACE/source-release.json")
|
|
553
|
+
fi
|
|
554
|
+
if [ -z "$BSPATCH_NOTES" ]; then
|
|
555
|
+
if [ "$PUBLISH_MODE" = "rollback" ]; then export BSPATCH_NOTES="rollback to v$SRC_V"
|
|
556
|
+
else export BSPATCH_NOTES="promoted from $SOURCE_FLAVOR v$SRC_V"; fi
|
|
557
|
+
fi
|
|
558
|
+
step_done "$SOURCE_FLAVOR v$SRC_V"
|
|
559
|
+
else
|
|
560
|
+
|
|
381
561
|
# Determine source bundle
|
|
382
562
|
local BUILD_LOG="$WORKSPACE/bundle.log"
|
|
383
563
|
if [ "$PLATFORM" == "android" ]; then
|
|
384
|
-
step "Bundle JS"
|
|
564
|
+
step "Bundle JS" BUILD
|
|
385
565
|
# Gradle's config noise goes to a log; shown only on failure
|
|
386
566
|
if ! run_logged "$BUILD_LOG" bash -c 'cd "$1/android" && ./gradlew ":app:createBundle${2}ReleaseJsAndAssets" -Pkotlin.incremental=false --quiet' _ "$ROOT" "$FLAVOR_CAP"; then
|
|
387
|
-
step_fail; tail -n 40 "$BUILD_LOG"; echo "
|
|
567
|
+
step_fail "Gradle bundle failed"; tail -n 40 "$BUILD_LOG"; echo " full log: $BUILD_LOG"; exit 1
|
|
388
568
|
fi
|
|
389
569
|
BUNDLE_SOURCE="$ROOT/android/app/build/generated/assets/react/${FLAVOR_LOWER}Release/index.android.bundle"
|
|
390
570
|
else
|
|
391
|
-
step "Bundle JS"
|
|
571
|
+
step "Bundle JS" BUILD
|
|
572
|
+
# Fresh dir: leftovers of deleted/renamed assets would land in the manifest
|
|
573
|
+
rm -rf "$OUTPUT/assets"
|
|
392
574
|
if ! run_logged "$BUILD_LOG" npx react-native bundle --platform ios --dev false --entry-file index.js \
|
|
393
575
|
--bundle-output "$WORKSPACE/source.bundle" --assets-dest "$OUTPUT/assets"; then
|
|
394
|
-
step_fail; tail -n 40 "$BUILD_LOG"; echo "
|
|
576
|
+
step_fail "bundle failed"; tail -n 40 "$BUILD_LOG"; echo " full log: $BUILD_LOG"; exit 1
|
|
395
577
|
fi
|
|
396
578
|
BUNDLE_SOURCE="$WORKSPACE/source.bundle"
|
|
397
579
|
fi
|
|
@@ -406,35 +588,29 @@ build_platform() {
|
|
|
406
588
|
if [ "$FIRST_BYTE" == "c6" ]; then IS_HBC=true; fi
|
|
407
589
|
fi
|
|
408
590
|
|
|
409
|
-
step "Hermes"
|
|
410
|
-
local HERMES_NOTE="already bytecode"
|
|
591
|
+
step "Hermes" BUILD
|
|
411
592
|
if [ "$IS_HBC" != true ]; then
|
|
412
|
-
HERMES_NOTE="compiled"
|
|
413
593
|
if [ -z "$HERMESC_PATH" ] || [ ! -f "$HERMESC_PATH" ]; then
|
|
414
|
-
step_fail
|
|
594
|
+
step_fail "hermesc not found"; exit 1
|
|
595
|
+
fi
|
|
596
|
+
# -O matches what Xcode/Gradle use for release; -w: undeclared-global warnings are expected noise
|
|
597
|
+
if ! run_logged "$WORKSPACE/hermes.log" "$HERMESC_PATH" -emit-binary -O -w -out "$WORKSPACE/source.hbc" "$BUNDLE_SOURCE"; then
|
|
598
|
+
step_fail "hermesc failed"; tail -n 40 "$WORKSPACE/hermes.log"; exit 1
|
|
415
599
|
fi
|
|
416
|
-
# -O matches what Xcode/Gradle use for release
|
|
417
|
-
"$HERMESC_PATH" -emit-binary -O -out "$WORKSPACE/source.hbc" "$BUNDLE_SOURCE"
|
|
418
600
|
mv "$WORKSPACE/source.hbc" "$BUNDLE_SOURCE"
|
|
419
601
|
fi
|
|
420
602
|
|
|
421
|
-
local BUNDLE_NAME="bundle_${APP_VERSION}_v${NEXT}_${PLATFORM}_${FLAVOR_LOWER}.hbc"
|
|
422
|
-
local NEW_BUNDLE_PATH="$OUTPUT/$BUNDLE_NAME"
|
|
423
|
-
|
|
424
603
|
cp "$BUNDLE_SOURCE" "$NEW_BUNDLE_PATH"
|
|
425
|
-
step_done "$
|
|
604
|
+
step_done "$(human_size "$(file_size "$NEW_BUNDLE_PATH")")"
|
|
426
605
|
|
|
427
606
|
# ── Assets ──
|
|
428
|
-
local ASSET_MANIFEST_FILE="$OUTPUT/asset-manifest_${APP_VERSION}_v${NEXT}_${PLATFORM}_${FLAVOR_LOWER}.json"
|
|
429
|
-
local ASSET_STORE_DIR="$OUTPUT/asset-store"
|
|
430
|
-
mkdir -p "$ASSET_STORE_DIR"
|
|
431
607
|
local ASSETS_SRC_DIR="$ROOT/android/app/build/generated/res/react/${FLAVOR_LOWER}Release"
|
|
432
608
|
if [ "$PLATFORM" == "ios" ]; then ASSETS_SRC_DIR="$OUTPUT/assets"; fi
|
|
433
609
|
|
|
434
610
|
local ASSET_JSON_LIST="["
|
|
435
611
|
local FIRST=true
|
|
436
612
|
local ASSET_COUNT=0
|
|
437
|
-
step "Assets"
|
|
613
|
+
step "Assets" BUILD
|
|
438
614
|
if [ -d "$ASSETS_SRC_DIR" ]; then
|
|
439
615
|
while IFS= read -r -d '' ASSET_FILE; do
|
|
440
616
|
local HASH=$(sha256 "$ASSET_FILE")
|
|
@@ -458,6 +634,7 @@ build_platform() {
|
|
|
458
634
|
cat > "$ASSET_MANIFEST_FILE" << EOF
|
|
459
635
|
{ "version": $NEXT, "appVersion": "$APP_VERSION", "platform": "$PLATFORM", "flavor": "$FLAVOR_LOWER", "assets": $ASSET_JSON_LIST }
|
|
460
636
|
EOF
|
|
637
|
+
fi
|
|
461
638
|
|
|
462
639
|
# ── Patches ──
|
|
463
640
|
# From the last 5 OTA versions only (never v1 = store build). Clients on the
|
|
@@ -465,46 +642,96 @@ EOF
|
|
|
465
642
|
local NEW_HASH=$(sha256 "$NEW_BUNDLE_PATH")
|
|
466
643
|
local PATCHES_JSON="["
|
|
467
644
|
local FIRST_FROM=$((NEXT - 5)); if [ $FIRST_FROM -lt 2 ]; then FIRST_FROM=2; fi
|
|
468
|
-
local
|
|
469
|
-
step "Patches"
|
|
645
|
+
local PATCH_SIZES=() PATCH_MISSING="" # "from|bytes"
|
|
646
|
+
step "Patches" DIFF
|
|
470
647
|
for (( i=FIRST_FROM; i<NEXT; i++ )); do
|
|
471
648
|
local OLD_B="bundle_${APP_VERSION}_v${i}_${PLATFORM}_${FLAVOR_LOWER}.hbc"
|
|
472
649
|
local OLD_P="$OUTPUT/$OLD_B"
|
|
473
650
|
if [ ! -f "$OLD_P" ]; then
|
|
474
651
|
step_progress "v$i: fetching bundle…"
|
|
475
|
-
|
|
476
|
-
|
|
652
|
+
if download_remote "$BASE_FOLDER/$APP_VERSION/$FLAVOR_LOWER/$OLD_B.gz" "$OLD_P.gz"; then
|
|
653
|
+
gunzip -c "$OLD_P.gz" > "$OLD_P" || rm -f "$OLD_P"
|
|
654
|
+
rm -f "$OLD_P.gz"
|
|
655
|
+
fi
|
|
477
656
|
fi
|
|
478
657
|
if [ ! -f "$OLD_P" ]; then
|
|
479
658
|
PATCH_MISSING="${PATCH_MISSING:+$PATCH_MISSING, }v$i"
|
|
480
659
|
continue
|
|
481
660
|
fi
|
|
482
661
|
local PNAME="patch_${APP_VERSION}_v${i}_to_v${NEXT}_${FLAVOR_LOWER}_${PLATFORM}.patch"
|
|
483
|
-
local PATCH_START=$SECONDS
|
|
484
662
|
step_progress "v$i → v$NEXT…"
|
|
485
663
|
"$BSDIFF" "$OLD_P" "$NEW_BUNDLE_PATH" "$OUTPUT/$PNAME"
|
|
486
|
-
|
|
487
|
-
PATCH_MADE=$((PATCH_MADE + 1))
|
|
664
|
+
PATCH_SIZES+=("$i|$(file_size "$OUTPUT/$PNAME")")
|
|
488
665
|
if [ "$PATCHES_JSON" != "[" ]; then PATCHES_JSON+=", "; fi
|
|
489
666
|
PATCHES_JSON+="{\"from\": $i, \"to\": $NEXT, \"file\": \"$PNAME\", \"baseHash\": \"$(sha256 "$OLD_P")\", \"bundleHash\": \"$NEW_HASH\"}"
|
|
490
667
|
done
|
|
491
668
|
PATCHES_JSON+="]"
|
|
492
|
-
|
|
493
|
-
step_done "none
|
|
494
|
-
|
|
495
|
-
step_done "$
|
|
496
|
-
|
|
497
|
-
local L
|
|
498
|
-
for L in "${PATCH_LINES[@]}"; do sub "${L%%|*}" "${L#*|}"; done
|
|
499
|
-
if [ -n "$PATCH_MISSING" ]; then sub "$PATCH_MISSING" "${DIM}no bundle on storage → full download${RESET}"; fi
|
|
669
|
+
case ${#PATCH_SIZES[@]} in
|
|
670
|
+
0) step_done "none" ;;
|
|
671
|
+
1) step_done "1 × $(human_size "${PATCH_SIZES[0]#*|}")" ;;
|
|
672
|
+
*) step_done "${#PATCH_SIZES[@]} patches" ;;
|
|
673
|
+
esac
|
|
500
674
|
|
|
501
675
|
# ── Finalize ──
|
|
502
|
-
step "
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
node "$SCRIPT_DIR/update_registry.js" "$REGISTRY_SHARED" "$APP_VERSION" "$NEXT" "$PATCHES_JSON" "$PLATFORM" "$ASSET_MANIFEST_FILE" "$FLAVOR_LOWER"
|
|
506
|
-
|
|
507
|
-
|
|
676
|
+
step "Upload" SHIP
|
|
677
|
+
export BSPATCH_REPORT="$WORKSPACE/upload.report"; : > "$BSPATCH_REPORT"
|
|
678
|
+
local REGISTRY_NOTE
|
|
679
|
+
if ! REGISTRY_NOTE=$(node "$SCRIPT_DIR/update_registry.js" "$REGISTRY_SHARED" "$APP_VERSION" "$NEXT" "$PATCHES_JSON" "$PLATFORM" "$ASSET_MANIFEST_FILE" "$FLAVOR_LOWER"); then
|
|
680
|
+
step_fail "registry update failed"; exit 1
|
|
681
|
+
fi
|
|
682
|
+
local UP_SIZE="" UP_DEST="" UP_ASSETS="" UP_GZIP_BYTES="" NATIVE_CHANGED="" VERIFY_NOTE=""
|
|
683
|
+
if [ "$DRY_RUN" = true ]; then
|
|
684
|
+
step_done "skipped"
|
|
685
|
+
else
|
|
686
|
+
bash "$SCRIPT_DIR/upload_ota.sh" "$APP_VERSION" "$FLAVOR_LOWER" "$NEXT" "$NEW_BUNDLE_PATH" "$PLATFORM" || exit 1
|
|
687
|
+
. "$BSPATCH_REPORT"
|
|
688
|
+
step_done "$UP_SIZE"
|
|
689
|
+
# What the app will read: its own public URL, not the bucket API
|
|
690
|
+
step "Verify" SHIP
|
|
691
|
+
if ! VERIFY_NOTE=$(node "$SCRIPT_DIR/verify_publish.js" "$ROOT" "$BASE_URL" "$BASE_FOLDER" "$APP_VERSION" "$FLAVOR_LOWER" "$PLATFORM" "$NEXT" 2>"$WORKSPACE/verify.log"); then
|
|
692
|
+
step_fail "$(head -n 1 "$WORKSPACE/verify.log")"; tail -n +2 "$WORKSPACE/verify.log"; exit 1
|
|
693
|
+
fi
|
|
694
|
+
step_done "$VERIFY_NOTE"
|
|
695
|
+
fi
|
|
696
|
+
. "$BSPATCH_REPORT"
|
|
697
|
+
timeline_redraw
|
|
698
|
+
|
|
699
|
+
# ── Summary: what each client downloads ──
|
|
700
|
+
# What a client downloads for the full bundle: the gzip size on the wire
|
|
701
|
+
local RAW_BYTES=$(file_size "$NEW_BUNDLE_PATH") FULL_BYTES=${UP_GZIP_BYTES:-$(file_size "$NEW_BUNDLE_PATH")} L CARD=()
|
|
702
|
+
kv() { CARD+=("${CYAN}$(padr "$1" 10)${RESET}$2"); }
|
|
703
|
+
if [ "$DRY_RUN" = true ]; then
|
|
704
|
+
CARD+=("$(badge DONE) ${BOLD}$PLATFORM v$NEXT built${RESET} in ${YELLOW}$((SECONDS - PLATFORM_START))s${RESET} ${DIM}· dry run, nothing uploaded${RESET}" "")
|
|
705
|
+
else
|
|
706
|
+
CARD+=("$(badge DONE) ${BOLD}$PLATFORM ${GREEN}v$NEXT${RESET}${BOLD} is live${RESET} in ${YELLOW}$((SECONDS - PLATFORM_START))s${RESET} ${DIM}· $UP_DEST${RESET}" "")
|
|
707
|
+
fi
|
|
708
|
+
for L in "${PATCH_SIZES[@]}"; do
|
|
709
|
+
kv "v${L%%|*} → v$NEXT" "${BOLD}$(human_size "${L#*|}")${RESET} patch ${BOLD}${GREEN}$(awk -v p="${L#*|}" -v f="$FULL_BYTES" 'BEGIN { printf "−%.1f%%", 100 - p * 100 / f }')${RESET}"
|
|
710
|
+
done
|
|
711
|
+
kv "v1 → v$NEXT" "${BOLD}$(human_size "$FULL_BYTES")${RESET} full bundle ${DIM}(gzip · $(human_size "$RAW_BYTES") raw)${RESET}"
|
|
712
|
+
if [ -n "$PATCH_MISSING" ]; then kv "$PATCH_MISSING" "${DIM}no bundle on storage → full bundle${RESET}"; fi
|
|
713
|
+
if [ -n "$UP_ASSETS" ]; then kv "assets" "$UP_ASSETS"; fi
|
|
714
|
+
local COMMIT=$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null)
|
|
715
|
+
if [ -n "$COMMIT" ]; then
|
|
716
|
+
local DIRTY=""; if [ -n "$(git -C "$ROOT" status --porcelain --untracked-files=no 2>/dev/null)" ]; then DIRTY=" ${YELLOW}+ uncommitted changes${RESET}"; fi
|
|
717
|
+
kv "commit" "$COMMIT ${DIM}$(git -C "$ROOT" rev-parse --abbrev-ref HEAD 2>/dev/null)${RESET}$DIRTY"
|
|
718
|
+
fi
|
|
719
|
+
if [ -n "$BSPATCH_NOTES" ]; then kv "notes" "$BSPATCH_NOTES"; fi
|
|
720
|
+
case "$REGISTRY_NOTE" in
|
|
721
|
+
*unsigned*) kv "signing" "${YELLOW}unsigned · no ota_private_key.pem${RESET}" ;;
|
|
722
|
+
*) kv "signing" "${GREEN}✓${RESET} signed" ;;
|
|
723
|
+
esac
|
|
724
|
+
local RELEASE_FILE="$OUTPUT/release_${APP_VERSION}_v${NEXT}_${PLATFORM}_${FLAVOR_LOWER}.json"
|
|
725
|
+
local FP=$(node -p 'require(process.argv[1]).runtimeFingerprint' "$RELEASE_FILE" 2>/dev/null)
|
|
726
|
+
if [ -n "$NATIVE_CHANGED" ]; then
|
|
727
|
+
kv "native" "$FP ${YELLOW}changed since $NATIVE_CHANGED: only store builds of this code take it${RESET}"
|
|
728
|
+
else
|
|
729
|
+
kv "native" "$FP"
|
|
730
|
+
fi
|
|
731
|
+
printf "\n"; box "${CARD[@]}"
|
|
732
|
+
if [ "$JSON_OUT" = true ]; then
|
|
733
|
+
node -e 'process.stdout.write(JSON.stringify(require(process.argv[1])) + "\n")' "$RELEASE_FILE"
|
|
734
|
+
fi
|
|
508
735
|
}
|
|
509
736
|
|
|
510
737
|
# ── Main ───────────────────────────────────────────────────────────────────
|