#!/bin/bash # Release script for MnnLlmChat # This script builds and publishes: # 1. Standard flavor debug APK for CDN upload # 2. Google Play flavor release AAB for Google Play Store set -e # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Configuration PROJECT_NAME="MnnLlmChat" VERSION_NAME=$(awk ' /defaultConfig[[:space:]]*\{/ { in_default=1; next } in_default && /versionName[[:space:]]*"/ { match($0, /versionName[[:space:]]*"[^"]+"/) print substr($0, RSTART + 13, RLENGTH - 14) exit } in_default && /\}/ { in_default=0 } ' app/build.gradle) VERSION_CODE=$(awk ' /defaultConfig[[:space:]]*\{/ { in_default=1; next } in_default && /versionCode[[:space:]]*[0-9]+/ { match($0, /versionCode[[:space:]]*[0-9]+/) value=substr($0, RSTART, RLENGTH) sub(/versionCode[[:space:]]*/, "", value) print value exit } in_default && /\}/ { in_default=0 } ' app/build.gradle) BUILD_DATE=$(date +"%Y%m%d_%H%M%S") RELEASE_HIGHLIGHTS="${RELEASE_HIGHLIGHTS:-}" PREVIOUS_VERSION="${PREVIOUS_VERSION:-}" # Directories SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_DIR="$(dirname "$SCRIPT_DIR")" BUILD_DIR="$PROJECT_DIR/app/build" OUTPUT_DIR="$PROJECT_DIR/release_outputs" CDN_UPLOAD_DIR="$OUTPUT_DIR/cdn" GOOGLE_PLAY_DIR="$OUTPUT_DIR/googleplay" # Environment variables for signing (should be set in CI/CD or locally) KEYSTORE_FILE="${KEYSTORE_FILE:-}" KEYSTORE_PASSWORD="${KEYSTORE_PASSWORD:-}" KEY_ALIAS="${KEY_ALIAS:-}" KEY_PASSWORD="${KEY_PASSWORD:-}" # CDN configuration CDN_ENDPOINT="${CDN_ENDPOINT:-}" CDN_ACCESS_KEY="${CDN_ACCESS_KEY:-}" CDN_SECRET_KEY="${CDN_SECRET_KEY:-}" CDN_BUCKET="${CDN_BUCKET:-}" # Google Play configuration GOOGLE_PLAY_SERVICE_ACCOUNT="${GOOGLE_PLAY_SERVICE_ACCOUNT:-}" GOOGLE_PLAY_PACKAGE_NAME="${GOOGLE_PLAY_PACKAGE_NAME:-com.alibaba.mnnllm.android.googleplay}" ENABLE_FIREBASE="${ENABLE_FIREBASE:-true}" MNN_FIREBASE_CONFIG_DIR="${MNN_FIREBASE_CONFIG_DIR:-$HOME/.mnn}" MNN_FIREBASE_DEV_CONFIG="${MNN_FIREBASE_DEV_CONFIG:-$MNN_FIREBASE_CONFIG_DIR/google-services-dev.json}" MNN_FIREBASE_RELEASE_CONFIG="${MNN_FIREBASE_RELEASE_CONFIG:-$MNN_FIREBASE_CONFIG_DIR/google-services-release.json}" STANDARD_GRADLE_ARGS=() GOOGLEPLAY_GRADLE_ARGS=() # Functions log_info() { echo -e "${BLUE}[INFO]${NC} $1" } log_success() { echo -e "${GREEN}[SUCCESS]${NC} $1" } log_warning() { echo -e "${YELLOW}[WARNING]${NC} $1" } log_error() { echo -e "${RED}[ERROR]${NC} $1" } has_google_services_config() { [[ -f "app/google-services.json" ]] || [[ -f "app/src/googleplay/google-services.json" ]] || [[ -f "app/src/standard/google-services.json" ]] } detect_previous_version() { local readme_file="README.md" if [[ ! -f "$readme_file" ]]; then return fi awk -v current="$VERSION_NAME" ' /^## Version / { version=$3 if (seen_current && version != current) { print version exit } if (version == current) { seen_current=1 } } ' "$readme_file" } prepare_google_services_config() { if [[ "$ENABLE_FIREBASE" != "true" ]]; then return fi if [[ ! -f "$MNN_FIREBASE_DEV_CONFIG" ]]; then log_error "Missing standard/debug Firebase config: $MNN_FIREBASE_DEV_CONFIG" exit 1 fi if [[ ! -f "$MNN_FIREBASE_RELEASE_CONFIG" ]]; then log_error "Missing Google Play/release Firebase config: $MNN_FIREBASE_RELEASE_CONFIG" exit 1 fi mkdir -p "app/src/standard" "app/src/googleplay" cp "$MNN_FIREBASE_DEV_CONFIG" "app/src/standard/google-services.json" cp "$MNN_FIREBASE_RELEASE_CONFIG" "app/src/googleplay/google-services.json" log_info "Prepared Firebase configs for standard(debug) and googleplay(release) flavors." } check_requirements() { log_info "Checking requirements..." # Check if we're in the right directory if [[ ! -f "app/build.gradle" ]]; then log_error "This script must be run from the project root directory" exit 1 fi # Check Java version java_version=$(java -version 2>&1 | head -n 1 | cut -d'"' -f2 | cut -d'.' -f1) if [[ "$java_version" -lt 11 ]]; then log_error "Java 11 or higher is required. Current version: $java_version" exit 1 fi # Check if Gradle wrapper exists if [[ ! -f "gradlew" ]]; then log_error "Gradle wrapper not found. Please run 'gradle wrapper' first." exit 1 fi # Check signing configuration for Google Play upload. # Build of Google Play AAB can still proceed without signing env vars. if [[ -z "$KEYSTORE_FILE" || -z "$KEYSTORE_PASSWORD" || -z "$KEY_ALIAS" || -z "$KEY_PASSWORD" ]]; then log_warning "Signing configuration not found. Google Play upload will be skipped." SKIP_GOOGLE_PLAY_UPLOAD=true unset KEYSTORE_FILE KEYSTORE_PASSWORD KEY_ALIAS KEY_PASSWORD elif [[ ! -f "$KEYSTORE_FILE" ]]; then log_warning "Signing keystore not found at $KEYSTORE_FILE. Building local AAB without signing and skipping Google Play upload." SKIP_GOOGLE_PLAY_UPLOAD=true unset KEYSTORE_FILE KEYSTORE_PASSWORD KEY_ALIAS KEY_PASSWORD else SKIP_GOOGLE_PLAY_UPLOAD=false fi if [[ "$ENABLE_FIREBASE" == "true" ]]; then prepare_google_services_config if has_google_services_config; then STANDARD_GRADLE_ARGS+=("-PENABLE_FIREBASE=true") GOOGLEPLAY_GRADLE_ARGS+=("-PENABLE_FIREBASE=true") log_info "Firebase enabled for standardDebug and Google Play bundle builds." else log_error "ENABLE_FIREBASE=true but no google-services.json found." log_error "Add app/google-services.json (or flavor-specific config) before release." exit 1 fi else log_info "Firebase disabled by configuration for local release build." fi log_success "Requirements check completed" } clean_build() { log_info "Cleaning previous builds..." ./gradlew clean rm -rf "$OUTPUT_DIR" mkdir -p "$OUTPUT_DIR" "$CDN_UPLOAD_DIR" "$GOOGLE_PLAY_DIR" log_success "Clean completed" } build_standard_debug() { log_info "Building standard flavor debug version..." ./gradlew "${STANDARD_GRADLE_ARGS[@]}" assembleStandardDebug # Generate version-based filename (replace dots with underscores) VERSION_FILENAME=$(echo "$VERSION_NAME" | sed 's/\./_/g') APK_FILENAME="mnn_chat_${VERSION_FILENAME}.apk" # Copy APK to output directory with version-based name APK_PATH="$BUILD_DIR/outputs/apk/standard/debug/app-standard-debug.apk" if [[ -f "$APK_PATH" ]]; then cp "$APK_PATH" "$CDN_UPLOAD_DIR/$APK_FILENAME" log_success "Standard debug APK built: $CDN_UPLOAD_DIR/$APK_FILENAME" else log_error "Standard debug APK not found at $APK_PATH" exit 1 fi } build_googleplay_release_bundle() { log_info "Building Google Play flavor release AAB..." ./gradlew "${GOOGLEPLAY_GRADLE_ARGS[@]}" bundleGoogleplayRelease # Copy AAB to output directory AAB_PATH="$BUILD_DIR/outputs/bundle/googleplayRelease/app-googleplay-release.aab" if [[ -f "$AAB_PATH" ]]; then cp "$AAB_PATH" "$GOOGLE_PLAY_DIR/" log_success "Google Play release AAB built: $GOOGLE_PLAY_DIR/app-googleplay-release.aab" else log_warning "Google Play release AAB not found at $AAB_PATH" fi } upload_to_cdn() { if [[ -z "$CDN_ENDPOINT" || -z "$CDN_ACCESS_KEY" || -z "$CDN_SECRET_KEY" || -z "$CDN_BUCKET" ]]; then log_warning "CDN configuration not found. Skipping CDN upload." return fi log_info "Uploading to CDN (ali-oss SDK)..." # Ensure Node.js and dependencies are available if ! command -v node &> /dev/null; then log_warning "Node.js not found. Install Node.js to enable automatic CDN upload." return fi if [[ ! -d "node_modules/ali-oss" ]]; then log_info "Installing ali-oss for CDN upload..." npm install --no-save ali-oss fi VERSION_FILENAME=$(echo "$VERSION_NAME" | sed 's/\./_/g') APK_FILENAME="mnn_chat_${VERSION_FILENAME}.apk" APK_FILE="$CDN_UPLOAD_DIR/$APK_FILENAME" if [[ ! -f "$APK_FILE" ]]; then log_error "APK file not found for CDN upload: $APK_FILE" return fi if node scripts/upload-cdn.mjs --apk "$APK_FILE" --version "$VERSION_NAME"; then OSS_PREFIX="${CDN_OSS_PREFIX:-data/mnn/apks}" log_success "APK uploaded to CDN: oss://$CDN_BUCKET/$OSS_PREFIX/$APK_FILENAME" else log_error "CDN upload failed" fi } upload_to_google_play() { if [[ "$SKIP_GOOGLE_PLAY_UPLOAD" == "true" ]]; then log_warning "Skipping Google Play upload due to missing signing configuration" return fi if [[ -z "$GOOGLE_PLAY_SERVICE_ACCOUNT" ]]; then log_warning "Google Play service account not configured. Skipping Google Play upload." return fi log_info "Uploading to Google Play..." # Check if fastlane is available if ! command -v fastlane &> /dev/null; then log_warning "fastlane not found. Please install it to upload to Google Play." log_info "You can install fastlane with: gem install fastlane" return fi # Create fastlane configuration if it doesn't exist FASTLANE_DIR="$PROJECT_DIR/fastlane" if [[ ! -d "$FASTLANE_DIR" ]]; then mkdir -p "$FASTLANE_DIR" cat > "$FASTLANE_DIR/Appfile" << EOF json_key_file("$GOOGLE_PLAY_SERVICE_ACCOUNT") package_name("$GOOGLE_PLAY_PACKAGE_NAME") EOF cat > "$FASTLANE_DIR/Fastfile" << EOF default_platform(:android) platform :android do desc "Upload to Google Play" lane :upload do upload_to_play_store( track: 'internal', aab: '../release_outputs/googleplay/app-googleplay-release.aab' ) end end EOF fi # Upload to Google Play cd "$FASTLANE_DIR" fastlane upload cd "$PROJECT_DIR" log_success "App uploaded to Google Play" } generate_release_notes() { log_info "Generating release notes..." # Generate version-based filename for documentation VERSION_FILENAME=$(echo "$VERSION_NAME" | sed 's/\./_/g') APK_FILENAME="mnn_chat_${VERSION_FILENAME}.apk" RELEASE_NOTES_FILE="$OUTPUT_DIR/release_notes.md" local previous_version_resolved="${PREVIOUS_VERSION:-$(detect_previous_version)}" cat > "$RELEASE_NOTES_FILE" << EOF # Release Notes - $PROJECT_NAME v$VERSION_NAME ## Version Information - **Version Name**: $VERSION_NAME - **Version Code**: $VERSION_CODE - **Build Date**: $BUILD_DATE - **Build Type**: Release ## Build Outputs ### Standard Flavor (Debug) - **APK**: \`$APK_FILENAME\` - **Purpose**: CDN distribution - **Location**: \`$CDN_UPLOAD_DIR/\` ### Google Play Flavor (Release) - **AAB**: \`app-googleplay-release.aab\` - **Purpose**: Google Play Store distribution - **Location**: \`$GOOGLE_PLAY_DIR/\` ## Build Configuration - **Min SDK**: 26 - **Target SDK**: 35 - **Compile SDK**: 35 - **ABI**: arm64-v8a ## Notes - Standard flavor includes debug features and is suitable for testing - Google Play flavor is optimized for production and follows Google Play guidelines - Local Google Play AAB build can run without signing env vars; store upload still requires signing + service account EOF echo "" >> "$RELEASE_NOTES_FILE" echo "## Release Highlights" >> "$RELEASE_NOTES_FILE" if [[ -n "$RELEASE_HIGHLIGHTS" ]]; then IFS='|' read -r -a highlight_items <<< "$RELEASE_HIGHLIGHTS" for item in "${highlight_items[@]}"; do echo "- $item" >> "$RELEASE_NOTES_FILE" done else echo "- Release highlights were not provided." >> "$RELEASE_NOTES_FILE" if [[ -n "$previous_version_resolved" ]]; then log_warning "RELEASE_HIGHLIGHTS is empty. Provide highlights that summarize the delta from v$previous_version_resolved." else log_warning "RELEASE_HIGHLIGHTS is empty. Release notes were generated without a curated change summary." fi fi log_success "Release notes generated: $RELEASE_NOTES_FILE" } main() { log_info "Starting release process for $PROJECT_NAME v$VERSION_NAME" # Check requirements check_requirements # Clean previous builds clean_build # Build standard debug version build_standard_debug # Build Google Play release AAB build_googleplay_release_bundle # Upload to CDN upload_to_cdn # Upload to Google Play upload_to_google_play # Generate release notes generate_release_notes log_success "Release process completed successfully!" log_info "Output directory: $OUTPUT_DIR" log_info "CDN uploads: $CDN_UPLOAD_DIR" log_info "Google Play uploads: $GOOGLE_PLAY_DIR" } # Run main function main "$@"