Add APK release flow with R2 redirects and updater support
This commit is contained in:
69
scripts/apk/build-release.sh
Executable file
69
scripts/apk/build-release.sh
Executable 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}"
|
||||
78
scripts/apk/make-version-json.sh
Executable file
78
scripts/apk/make-version-json.sh
Executable 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
115
scripts/apk/publish-r2.sh
Executable 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
67
scripts/apk/verify-release.sh
Executable 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"
|
||||
Reference in New Issue
Block a user