Files
2026-07-13 13:33:03 +08:00

415 lines
13 KiB
Bash
Executable File

#!/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 "$@"