Add APK release flow with R2 redirects and updater support

This commit is contained in:
Dwindi Ramadhana
2026-02-21 21:28:40 +07:00
parent 3d4a753be7
commit efc013f498
14 changed files with 865 additions and 120 deletions

69
scripts/apk/build-release.sh Executable file
View File

@@ -0,0 +1,69 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
ANDROID_DIR="${ROOT_DIR}/dewemoji-capacitor/android"
APP_GRADLE="${ANDROID_DIR}/app/build.gradle"
DIST_DIR="${ROOT_DIR}/dewemoji-capacitor/dist/apk"
if [[ ! -f "${APP_GRADLE}" ]]; then
echo "error: missing ${APP_GRADLE}" >&2
exit 1
fi
version_name="$(awk '/versionName /{gsub(/"/, "", $2); print $2; exit}' "${APP_GRADLE}")"
version_code="$(awk '/versionCode /{print $2; exit}' "${APP_GRADLE}")"
if [[ -z "${version_name}" || -z "${version_code}" ]]; then
echo "error: failed to read versionName/versionCode from ${APP_GRADLE}" >&2
exit 1
fi
mkdir -p "${DIST_DIR}"
echo "== Build release APK =="
(
cd "${ANDROID_DIR}"
./gradlew clean assembleRelease
)
unsigned_apk="${ANDROID_DIR}/app/build/outputs/apk/release/app-release-unsigned.apk"
signed_apk_default="${ANDROID_DIR}/app/build/outputs/apk/release/app-release.apk"
input_apk=""
if [[ -f "${signed_apk_default}" ]]; then
input_apk="${signed_apk_default}"
elif [[ -f "${unsigned_apk}" ]]; then
input_apk="${unsigned_apk}"
else
echo "error: release APK not found under app/build/outputs/apk/release" >&2
exit 1
fi
output_apk="${DIST_DIR}/dewemoji-v${version_name}-${version_code}.apk"
if [[ -n "${ANDROID_KEYSTORE_PATH:-}" && -n "${ANDROID_KEYSTORE_PASSWORD:-}" && -n "${ANDROID_KEY_ALIAS:-}" && -n "${ANDROID_KEY_PASSWORD:-}" ]]; then
if ! command -v apksigner >/dev/null 2>&1; then
echo "error: apksigner is required for signing but not found" >&2
exit 1
fi
echo "== Sign APK =="
apksigner sign \
--ks "${ANDROID_KEYSTORE_PATH}" \
--ks-pass "pass:${ANDROID_KEYSTORE_PASSWORD}" \
--ks-key-alias "${ANDROID_KEY_ALIAS}" \
--key-pass "pass:${ANDROID_KEY_PASSWORD}" \
--out "${output_apk}" \
"${input_apk}"
apksigner verify --verbose "${output_apk}" >/dev/null
else
echo "warning: signing env vars are not fully set; copying unsigned/gradle output as-is"
cp "${input_apk}" "${output_apk}"
fi
sha256="$(shasum -a 256 "${output_apk}" | awk '{print $1}')"
echo "Built APK: ${output_apk}"
echo "Version: ${version_name} (${version_code})"
echo "SHA256: ${sha256}"

View File

@@ -0,0 +1,78 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<USAGE
Usage:
scripts/apk/make-version-json.sh \
--version-name 1.1.2 \
--version-code 112 \
--sha256 <hex> \
--notes "Release notes" \
[--out ./version.json] \
[--apk-url https://dewemoji.com/downloads/dewemoji-latest.apk] \
[--app-id com.dewemoji.app] \
[--channel stable] \
[--min-supported-version-code 100] \
[--force false]
USAGE
}
out="./version.json"
apk_url="https://dewemoji.com/downloads/dewemoji-latest.apk"
app_id="com.dewemoji.app"
channel="stable"
min_supported_version_code="100"
force="false"
version_name=""
version_code=""
sha256=""
notes=""
published_at="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
while [[ $# -gt 0 ]]; do
case "$1" in
--version-name) version_name="$2"; shift 2 ;;
--version-code) version_code="$2"; shift 2 ;;
--sha256) sha256="$2"; shift 2 ;;
--notes) notes="$2"; shift 2 ;;
--out) out="$2"; shift 2 ;;
--apk-url) apk_url="$2"; shift 2 ;;
--app-id) app_id="$2"; shift 2 ;;
--channel) channel="$2"; shift 2 ;;
--min-supported-version-code) min_supported_version_code="$2"; shift 2 ;;
--force) force="$2"; shift 2 ;;
--published-at) published_at="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "error: unknown argument '$1'" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${version_name}" || -z "${version_code}" || -z "${sha256}" ]]; then
echo "error: --version-name, --version-code, and --sha256 are required" >&2
usage
exit 1
fi
python3 - <<PY
import json
from pathlib import Path
payload = {
"appId": "${app_id}",
"channel": "${channel}",
"versionName": "${version_name}",
"versionCode": int("${version_code}"),
"minSupportedVersionCode": int("${min_supported_version_code}"),
"apkUrl": "${apk_url}",
"sha256": "${sha256}",
"publishedAt": "${published_at}",
"notes": "${notes}",
"force": "${force}".lower() == "true",
}
out = Path("${out}")
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps(payload, ensure_ascii=True, indent=2) + "\n", encoding="utf-8")
print(out)
PY

115
scripts/apk/publish-r2.sh Executable file
View File

@@ -0,0 +1,115 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<USAGE
Usage:
scripts/apk/publish-r2.sh \
--apk /path/to/dewemoji-v1.1.2-112.apk \
--version-name 1.1.2 \
--version-code 112 \
[--notes "Release notes"] \
[--min-supported-version-code 100] \
[--force false]
Required env:
R2_ACCOUNT_ID
R2_ACCESS_KEY_ID
R2_SECRET_ACCESS_KEY
R2_BUCKET
Optional env:
R2_PUBLIC_BASE_URL (example: https://downloads.dewemoji.com)
DEWEMOJI_APK_URL (default: https://dewemoji.com/downloads/dewemoji-latest.apk)
USAGE
}
for required in R2_ACCOUNT_ID R2_ACCESS_KEY_ID R2_SECRET_ACCESS_KEY R2_BUCKET; do
if [[ -z "${!required:-}" ]]; then
echo "error: missing env ${required}" >&2
exit 1
fi
done
if ! command -v aws >/dev/null 2>&1; then
echo "error: aws cli is required" >&2
exit 1
fi
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
MAKE_VERSION_SCRIPT="${ROOT_DIR}/scripts/apk/make-version-json.sh"
apk=""
version_name=""
version_code=""
notes=""
min_supported_version_code="100"
force="false"
while [[ $# -gt 0 ]]; do
case "$1" in
--apk) apk="$2"; shift 2 ;;
--version-name) version_name="$2"; shift 2 ;;
--version-code) version_code="$2"; shift 2 ;;
--notes) notes="$2"; shift 2 ;;
--min-supported-version-code) min_supported_version_code="$2"; shift 2 ;;
--force) force="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "error: unknown argument '$1'" >&2; usage; exit 1 ;;
esac
done
if [[ -z "${apk}" || -z "${version_name}" || -z "${version_code}" ]]; then
echo "error: --apk, --version-name, and --version-code are required" >&2
usage
exit 1
fi
if [[ ! -f "${apk}" ]]; then
echo "error: apk file not found: ${apk}" >&2
exit 1
fi
endpoint="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com"
export AWS_ACCESS_KEY_ID="${R2_ACCESS_KEY_ID}"
export AWS_SECRET_ACCESS_KEY="${R2_SECRET_ACCESS_KEY}"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "${tmp_dir}"' EXIT
sha256="$(shasum -a 256 "${apk}" | awk '{print $1}')"
versioned_key="apk/dewemoji-v${version_name}-${version_code}.apk"
latest_key="apk/dewemoji-latest.apk"
version_json_key="apk/version.json"
apk_url="${DEWEMOJI_APK_URL:-https://dewemoji.com/downloads/dewemoji-latest.apk}"
version_json_path="${tmp_dir}/version.json"
"${MAKE_VERSION_SCRIPT}" \
--version-name "${version_name}" \
--version-code "${version_code}" \
--sha256 "${sha256}" \
--notes "${notes}" \
--apk-url "${apk_url}" \
--min-supported-version-code "${min_supported_version_code}" \
--force "${force}" \
--out "${version_json_path}"
echo "== Upload versioned APK =="
aws --endpoint-url "${endpoint}" s3 cp "${apk}" "s3://${R2_BUCKET}/${versioned_key}" --content-type application/vnd.android.package-archive
echo "== Upload latest APK alias =="
aws --endpoint-url "${endpoint}" s3 cp "${apk}" "s3://${R2_BUCKET}/${latest_key}" --content-type application/vnd.android.package-archive
echo "== Upload version metadata =="
aws --endpoint-url "${endpoint}" s3 cp "${version_json_path}" "s3://${R2_BUCKET}/${version_json_key}" --content-type application/json --cache-control no-store
echo "Published to R2 bucket: ${R2_BUCKET}"
echo "Versioned APK key: ${versioned_key}"
echo "Latest APK key: ${latest_key}"
echo "Version JSON key: ${version_json_key}"
if [[ -n "${R2_PUBLIC_BASE_URL:-}" ]]; then
base="${R2_PUBLIC_BASE_URL%/}"
echo "Public versioned APK URL: ${base}/${versioned_key}"
echo "Public latest APK URL: ${base}/${latest_key}"
echo "Public version JSON URL: ${base}/${version_json_key}"
fi

67
scripts/apk/verify-release.sh Executable file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<USAGE
Usage:
scripts/apk/verify-release.sh [--base-url https://dewemoji.com/downloads]
USAGE
}
base_url="https://dewemoji.com/downloads"
while [[ $# -gt 0 ]]; do
case "$1" in
--base-url) base_url="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "error: unknown argument '$1'" >&2; usage; exit 1 ;;
esac
done
version_url="${base_url%/}/version.json"
apk_url="${base_url%/}/dewemoji-latest.apk"
tmp_dir="$(mktemp -d)"
trap 'rm -rf "${tmp_dir}"' EXIT
version_file="${tmp_dir}/version.json"
apk_file="${tmp_dir}/dewemoji-latest.apk"
echo "== Fetch version metadata =="
curl -fsSL "${version_url}" -o "${version_file}"
python3 - <<PY
import json
from pathlib import Path
obj = json.loads(Path("${version_file}").read_text(encoding="utf-8"))
required = ["appId", "channel", "versionName", "versionCode", "apkUrl", "sha256", "publishedAt"]
missing = [k for k in required if k not in obj]
if missing:
raise SystemExit(f"error: missing fields in version.json: {', '.join(missing)}")
print(f"versionName={obj['versionName']}")
print(f"versionCode={obj['versionCode']}")
print(f"apkUrl={obj['apkUrl']}")
print(f"sha256={obj['sha256']}")
PY
echo "== Download latest APK =="
curl -fL "${apk_url}" -o "${apk_file}"
local_sha="$(shasum -a 256 "${apk_file}" | awk '{print $1}')"
expected_sha="$(python3 - <<PY
import json
from pathlib import Path
obj = json.loads(Path("${version_file}").read_text(encoding="utf-8"))
print(obj["sha256"])
PY
)"
echo "local_sha=${local_sha}"
echo "expected_sha=${expected_sha}"
if [[ "${local_sha}" != "${expected_sha}" ]]; then
echo "error: checksum mismatch" >&2
exit 1
fi
echo "OK: release metadata and APK checksum are consistent"