#!/usr/bin/env bash # # publish-mobile-update.sh # ------------------------ # Publishes the LogShip Mobile Android build into the ERP's server-side release channel # (data/mobile-releases/ on the app host, served via app.logship.de — see # server/utils/mobileReleases.ts) WITHOUT an ERP deployment: # # android/update-files/logship-mobile.apk + version.json # --POST multipart--> /api/settings/mobile-app/publish # # The server archives the replaced APK into history/, merges history.json, prunes old builds # and writes version.json last. Auth = the non-expiring service token (IDEMPIERETOKEN) read # from .env-prod (never printed). The same endpoint backs the upload form at # Settings → Mobile App in the ERP UI. # # Usage: # scripts/publish-mobile-update.sh # publish to https://app.logship.de # scripts/publish-mobile-update.sh --force # allow same/lower versionCode (re-publish) # ERP_BASE=http://localhost:3010 scripts/publish-mobile-update.sh # local dev server # ERP_ENV_FILE=.env scripts/publish-mobile-update.sh # token from another env file # set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" UPDATE_DIR="$REPO_ROOT/android/update-files" APK_LOCAL="$UPDATE_DIR/logship-mobile.apk" VERSION_LOCAL="$UPDATE_DIR/version.json" ERP_BASE="${ERP_BASE:-https://app.logship.de}" ERP_ENV_FILE="${ERP_ENV_FILE:-$REPO_ROOT/.env-prod}" bold() { printf '\033[1m%s\033[0m\n' "$*"; } err() { printf '\033[31mERROR:\033[0m %s\n' "$*" >&2; } ok() { printf '\033[32m✔\033[0m %s\n' "$*"; } FORCE=0 for a in "$@"; do case "$a" in --force) FORCE=1 ;; -h|--help) sed -n '3,22p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; *) err "unknown option: $a"; exit 1 ;; esac done for f in "$APK_LOCAL" "$VERSION_LOCAL"; do [ -f "$f" ] || { err "Missing $f — run scripts/release-app.sh (or stage the APK) first"; exit 1; } done [ -f "$ERP_ENV_FILE" ] || { err "Env file $ERP_ENV_FILE not found (set ERP_ENV_FILE)"; exit 1; } # Service token, read without echoing it anywhere. TOKEN="$(grep '^IDEMPIERETOKEN=' "$ERP_ENV_FILE" | head -1 | sed "s/^IDEMPIERETOKEN=//; s/^'//; s/'\$//; s/^\"//; s/\"\$//")" [ -n "$TOKEN" ] || { err "IDEMPIERETOKEN not found in $ERP_ENV_FILE"; exit 1; } json_field() { node -e 'const j=JSON.parse(require("fs").readFileSync(process.argv[1],"utf8"));process.stdout.write(String(j[process.argv[2]]??""))' "$1" "$2"; } new_vn="$(json_field "$VERSION_LOCAL" versionName)" new_vc="$(json_field "$VERSION_LOCAL" versionCode)" # APK ↔ version.json sanity check (the updater keys off version.json) find_aapt() { local base cand for base in "${ANDROID_HOME:-}" "${ANDROID_SDK_ROOT:-}" "$HOME/Library/Android/sdk" "$HOME/Android/Sdk"; do [ -n "$base" ] || continue cand="$(ls -1 "$base"/build-tools/*/aapt 2>/dev/null | sort -V | tail -1 || true)" [ -n "$cand" ] && { printf '%s' "$cand"; return 0; } done command -v aapt >/dev/null 2>&1 && { command -v aapt; return 0; } return 1 } AAPT="$(find_aapt || true)" if [ -n "${AAPT:-}" ]; then badging="$("$AAPT" dump badging "$APK_LOCAL" 2>/dev/null | grep '^package:' || true)" apk_vn="$(printf '%s' "$badging" | sed -n "s/.*versionName='\([^']*\)'.*/\1/p")" apk_vc="$(printf '%s' "$badging" | sed -n "s/.*versionCode='\([^']*\)'.*/\1/p")" bold "Version check" printf ' APK : versionName=%s versionCode=%s\n' "$apk_vn" "$apk_vc" printf ' version.json: versionName=%s versionCode=%s\n' "$new_vn" "$new_vc" if [ "$apk_vn" != "$new_vn" ] || [ "$apk_vc" != "$new_vc" ]; then err "version.json does NOT match the APK — devices compare version.json, a mismatch breaks updates." read -rp "Publish anyway? [y/N] " ans case "$ans" in y|Y) ;; *) exit 1;; esac else ok "version.json matches the APK" fi else printf 'aapt not found — skipping APK/version.json match check.\n' fi bold "Publishing $new_vn (code $new_vc) → $ERP_BASE/api/settings/mobile-app/publish" RESP="$(curl -sS --fail-with-body --max-time 600 \ -H "Authorization: Bearer $TOKEN" \ -F "apk=@$APK_LOCAL;type=application/vnd.android.package-archive" \ -F "version=@$VERSION_LOCAL;type=application/json" \ $( [ "$FORCE" = "1" ] && printf -- '-F force=1' ) \ "$ERP_BASE/api/settings/mobile-app/publish" 2>&1)" || { err "Publish request failed:"; printf '%s\n' "$RESP" >&2; exit 1; } STATUS="$(printf '%s' "$RESP" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{const j=JSON.parse(s);console.log(j.status??"")}catch{console.log("")}})')" if [ "$STATUS" != "200" ]; then err "Server rejected the release:"; printf '%s\n' "$RESP" >&2; exit 1 fi printf '%s\n' "$RESP" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{const j=JSON.parse(s);const m=j.manifest||{};console.log(" "+j.message);if(j.archived)console.log(" archived: "+j.archived);console.log(" sha256 : "+(m.sha256||"?"));console.log(" size : "+((m.size||0)/1048576).toFixed(1)+" MB");console.log(" apkUrl : "+(m.apkUrl||"?").replace(/\/api\/public\/mobile\/[^/]+\//,"/api/public/mobile//"))})' ok "published $new_vn (code $new_vc)" # Verify what devices will see now. LIVE="$(curl -sS -H "Authorization: Bearer $TOKEN" "$ERP_BASE/api/mobile/app-version" 2>/dev/null || true)" live_vc="$(printf '%s' "$LIVE" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{try{console.log(JSON.parse(s).versionCode??"")}catch{console.log("")}})')" if [ "$live_vc" = "$new_vc" ]; then ok "live manifest reports versionCode $live_vc"; else err "live manifest reports versionCode '$live_vc' (expected $new_vc)"; exit 1; fi