Skip to content

chore: bump version to 2.3.11 (241) (#148) #45

chore: bump version to 2.3.11 (241) (#148)

chore: bump version to 2.3.11 (241) (#148) #45

Workflow file for this run

name: Release
on:
push:
branches:
- main
paths:
- 'config.gradle'
workflow_dispatch:
inputs:
version:
description: 'Version to release (e.g., v1.0.0)'
required: true
type: string
track:
description: 'Play Store release track'
required: false
type: choice
default: 'beta'
options:
- internal
- alpha
- beta
- production
status:
description: 'Play Store release status'
required: false
type: choice
default: 'draft'
options:
- draft
- completed
permissions:
contents: write
jobs:
prepare:
name: Prepare Release
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
version_code: ${{ steps.version_info.outputs.version_code }}
should_release: ${{ steps.check_version.outputs.should_release }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 2 # Need history for version comparison
token: ${{ secrets.GITHUB_TOKEN }}
- name: Check version change
id: check_version
if: github.event_name == 'push'
run: |
# Get current version from config.gradle with error handling
CURRENT_VERSION=$(grep versionName config.gradle | sed -E 's/.*versionName:\s*"([^"]+)".*/\1/' || echo "")
CURRENT_CODE=$(grep versionCode config.gradle | sed -E 's/.*versionCode:\s*([0-9]+).*/\1/' || echo "")
# Validate extraction was successful
if [ -z "$CURRENT_VERSION" ] || [ -z "$CURRENT_CODE" ]; then
echo "❌ Error: Failed to extract version information from config.gradle"
echo "CURRENT_VERSION='$CURRENT_VERSION', CURRENT_CODE='$CURRENT_CODE'"
echo "should_release=false" >> $GITHUB_OUTPUT
exit 1
fi
# Validate CURRENT_CODE is a valid number
if ! [[ "$CURRENT_CODE" =~ ^[0-9]+$ ]]; then
echo "❌ Error: Version code is not a valid number: $CURRENT_CODE"
echo "should_release=false" >> $GITHUB_OUTPUT
exit 1
fi
# Get previous version - handle first commit case
if git rev-parse HEAD~1 >/dev/null 2>&1; then
git show HEAD~1:config.gradle > prev_config.gradle 2>/dev/null || echo "No previous config.gradle"
if [ -f prev_config.gradle ]; then
PREV_VERSION=$(grep versionName prev_config.gradle | sed -E 's/.*versionName:\s*"([^"]+)".*/\1/' || echo "")
PREV_CODE=$(grep versionCode prev_config.gradle | sed -E 's/.*versionCode:\s*([0-9]+).*/\1/' || echo "0")
rm prev_config.gradle
else
PREV_VERSION=""
PREV_CODE="0"
fi
else
# First commit in repo
PREV_VERSION=""
PREV_CODE="0"
fi
# Validate PREV_CODE is a valid number (default to 0 if not)
if ! [[ "$PREV_CODE" =~ ^[0-9]+$ ]]; then
PREV_CODE="0"
fi
echo "Previous: v$PREV_VERSION (code: $PREV_CODE)"
echo "Current: v$CURRENT_VERSION (code: $CURRENT_CODE)"
# Check if version increased
if [ "$CURRENT_CODE" -gt "$PREV_CODE" ]; then
echo "✅ Version increased from $PREV_VERSION to $CURRENT_VERSION"
echo "should_release=true" >> $GITHUB_OUTPUT
echo "version=v$CURRENT_VERSION" >> $GITHUB_OUTPUT
echo "version_code=$CURRENT_CODE" >> $GITHUB_OUTPUT
else
echo "ℹ️ No version increase detected"
echo "should_release=false" >> $GITHUB_OUTPUT
exit 0
fi
- name: Determine version
id: version
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
VERSION="${{ github.event.inputs.version }}"
echo "should_release=true" >> $GITHUB_OUTPUT
elif [ "${{ steps.check_version.outputs.should_release }}" = "true" ]; then
VERSION="${{ steps.check_version.outputs.version }}"
else
echo "No release needed"
exit 0
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- name: Extract version code
id: version_info
run: |
# Use version code from check_version step if available, otherwise extract it
if [ -n "${{ steps.check_version.outputs.version_code }}" ]; then
VERSION_CODE="${{ steps.check_version.outputs.version_code }}"
else
# Only extract if not already available (for workflow_dispatch)
VERSION_CODE=$(grep "versionCode:" config.gradle | sed -E 's/.*versionCode:\s*([0-9]+).*/\1/')
fi
echo "version_code=$VERSION_CODE" >> $GITHUB_OUTPUT
echo "Version Code: $VERSION_CODE"
- name: Create and push tag
if: steps.check_version.outputs.should_release == 'true' && github.event_name == 'push'
id: create_tag
run: |
TAG_NAME="${{ steps.version.outputs.version }}"
# Check if tag already exists
if git ls-remote --tags origin | grep -q "refs/tags/${TAG_NAME}$"; then
echo "⚠️ Tag $TAG_NAME already exists, skipping tag creation"
echo "tag_created=false" >> $GITHUB_OUTPUT
echo "tag_exists=true" >> $GITHUB_OUTPUT
else
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Create annotated tag
git tag -a "${TAG_NAME}" -m "Release ${TAG_NAME} (version code: ${{ steps.version_info.outputs.version_code }})"
git push origin "${TAG_NAME}"
echo "🏷️ Created and pushed tag: ${TAG_NAME}"
echo "tag_created=true" >> $GITHUB_OUTPUT
echo "tag_exists=false" >> $GITHUB_OUTPUT
fi
build-apk:
name: Build Release APK
needs: prepare
if: needs.prepare.outputs.should_release == 'true' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Decode Keystore
if: ${{ vars.ENABLE_SIGNING == 'true' && env.KEYSTORE_BASE64 != '' }}
env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
run: |
echo "$KEYSTORE_BASE64" | base64 --decode > app/keystore.jks
- name: Debug keystore info
if: ${{ vars.ENABLE_SIGNING == 'true' }}
run: |
echo "Keystore file exists: $([ -f app/keystore.jks ] && echo 'Yes' || echo 'No')"
echo "Keystore size: $([ -f app/keystore.jks ] && ls -la app/keystore.jks | awk '{print $5}' || echo 'N/A')"
echo "Key alias configured: ${{ secrets.KEY_ALIAS != '' && 'Yes' || 'No' }}"
- name: Build release APK
run: |
if [ "${{ vars.ENABLE_SIGNING }}" = "true" ] && [ -f "app/keystore.jks" ]; then
echo "Building signed release APK"
echo "Using key alias: ${{ secrets.KEY_ALIAS }}"
./gradlew assembleRelease \
-Pandroid.injected.signing.store.file=${{ github.workspace }}/app/keystore.jks \
-Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} \
-Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} \
-Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }}
else
echo "Building unsigned release APK"
./gradlew assembleRelease --stacktrace
fi
- name: Clean up keystore
if: always()
run: |
rm -f app/keystore.jks
- name: Upload release APK
uses: actions/upload-artifact@v4
with:
name: release-apk
path: app/build/outputs/apk/**/*.apk
retention-days: 30
- name: APK Summary
run: |
echo "## APK Build Results :package:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
APK_PATH=$(find app/build/outputs/apk -name "*.apk" | grep -E "release" | head -1)
if [ -f "$APK_PATH" ]; then
APK_SIZE=$(du -h "$APK_PATH" | cut -f1)
echo "- APK Size: $APK_SIZE" >> $GITHUB_STEP_SUMMARY
echo "- APK Name: \`$(basename "$APK_PATH")\`" >> $GITHUB_STEP_SUMMARY
echo "- Signed: ${{ vars.ENABLE_SIGNING == 'true' && 'Yes' || 'No' }}" >> $GITHUB_STEP_SUMMARY
else
echo "No APK found" >> $GITHUB_STEP_SUMMARY
fi
build-aab:
name: Build Release Bundle
needs: prepare
if: needs.prepare.outputs.should_release == 'true' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Decode Keystore
if: ${{ vars.ENABLE_SIGNING == 'true' && env.KEYSTORE_BASE64 != '' }}
env:
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
run: |
echo "$KEYSTORE_BASE64" | base64 --decode > app/keystore.jks
- name: Build release bundle
run: |
if [ "${{ vars.ENABLE_SIGNING }}" = "true" ] && [ -f "app/keystore.jks" ]; then
echo "Building signed release bundle"
./gradlew bundleRelease \
-Pandroid.injected.signing.store.file=${{ github.workspace }}/app/keystore.jks \
-Pandroid.injected.signing.store.password=${{ secrets.KEYSTORE_PASSWORD }} \
-Pandroid.injected.signing.key.alias=${{ secrets.KEY_ALIAS }} \
-Pandroid.injected.signing.key.password=${{ secrets.KEY_PASSWORD }}
else
echo "Skipping bundle build - signing not configured"
fi
- name: Generate debug symbols
if: ${{ vars.ENABLE_SIGNING == 'true' }}
run: |
echo "Checking for debug symbols..."
find app/build/outputs -name "*.zip" -type f | grep -i debug || echo "No debug symbol zips found"
find app/build/outputs -name "*symbols*" -type f || echo "No symbol files found"
ls -la app/build/outputs/bundle/release/ || true
- name: Clean up keystore
if: always()
run: |
rm -f app/keystore.jks
- name: Upload release bundle
if: ${{ vars.ENABLE_SIGNING == 'true' }}
uses: actions/upload-artifact@v4
with:
name: release-bundle
path: |
app/build/outputs/bundle/**/*.aab
app/build/outputs/mapping/release/mapping.txt
retention-days: 30
release:
name: Create GitHub Release
needs: [prepare, build-apk, build-aab]
if: needs.prepare.outputs.should_release == 'true' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download APK artifact
uses: actions/download-artifact@v4
with:
name: release-apk
path: release-artifacts/
- name: Download AAB artifact
if: ${{ vars.ENABLE_SIGNING == 'true' }}
uses: actions/download-artifact@v4
with:
name: release-bundle
path: release-artifacts/
continue-on-error: true
- name: Prepare release assets
id: assets
run: |
# Find APK
APK_PATH=$(find release-artifacts -name "*.apk" | grep -E "release" | head -1)
if [ -f "$APK_PATH" ]; then
APK_NAME="v2er-${{ needs.prepare.outputs.version }}.apk"
mv "$APK_PATH" "$APK_NAME"
echo "apk_path=$APK_NAME" >> $GITHUB_OUTPUT
fi
# Find AAB
AAB_PATH=$(find release-artifacts -name "*.aab" 2>/dev/null | head -1)
if [ -f "$AAB_PATH" ]; then
AAB_NAME="v2er-${{ needs.prepare.outputs.version }}.aab"
mv "$AAB_PATH" "$AAB_NAME"
echo "aab_path=$AAB_NAME" >> $GITHUB_OUTPUT
fi
- name: Generate changelog
id: changelog
env:
GH_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
# Define shared functions that will be used in both changelog generation steps
cat > /tmp/release_functions.sh << 'FUNCTIONS_EOF'
# Function to categorize commit message
categorize_commit() {
local msg="$1"
local category=""
local cleaned_msg=""
if [[ "$msg" =~ ^fix(\(.*\))?:\ (.*)$ ]] || [[ "$msg" =~ ^fix\ (.*)$ ]]; then
category="bug"
cleaned_msg="${BASH_REMATCH[-1]}"
elif [[ "$msg" =~ ^feat(\(.*\))?:\ (.*)$ ]] || [[ "$msg" =~ ^feat\ (.*)$ ]]; then
category="feature"
cleaned_msg="${BASH_REMATCH[-1]}"
elif [[ "$msg" =~ ^perf(\(.*\))?:\ (.*)$ ]]; then
category="performance"
cleaned_msg="${BASH_REMATCH[2]}"
elif [[ "$msg" =~ ^refactor(\(.*\))?:\ (.*)$ ]]; then
category="improvement"
cleaned_msg="${BASH_REMATCH[2]}"
elif [[ "$msg" =~ ^chore(\(.*\))?:\ (.*)$ ]]; then
category="maintenance"
cleaned_msg="${BASH_REMATCH[2]}"
elif [[ "$msg" =~ ^docs(\(.*\))?:\ (.*)$ ]]; then
category="documentation"
cleaned_msg="${BASH_REMATCH[2]}"
else
category="other"
cleaned_msg="$msg"
fi
# Remove PR numbers from the end
cleaned_msg=$(echo "$cleaned_msg" | sed 's/ (#[0-9]*)//')
# Capitalize first letter
cleaned_msg="$(echo "${cleaned_msg:0:1}" | tr '[:lower:]' '[:upper:]')${cleaned_msg:1}"
echo "$category:$cleaned_msg"
}
# Function to get GitHub username from commit
get_github_username() {
local commit_sha="$1"
local github_repo="${GITHUB_REPOSITORY}"
# Try to get the GitHub username from the commit using gh api
local username=$(gh api "repos/${github_repo}/commits/${commit_sha}" --jq '.author.login // empty' 2>/dev/null || echo "")
if [ -n "$username" ]; then
echo "@$username"
else
# Fallback: try to get committer login if author login is not available
local committer=$(gh api "repos/${github_repo}/commits/${commit_sha}" --jq '.committer.login // empty' 2>/dev/null || echo "")
if [ -n "$committer" ]; then
echo "@$committer"
else
# Last resort: use hardcoded mapping for known authors
local git_author=$(git show -s --format='%an' $commit_sha)
case "$git_author" in
"Gray Zhang" | "gray" | "Gray")
echo "@graycreate"
;;
"github-actions[bot]")
echo "@github-actions[bot]"
;;
*)
# If no mapping found, use git author name without @
echo "$git_author"
;;
esac
fi
fi
}
FUNCTIONS_EOF
# Source the functions
source /tmp/release_functions.sh
# Get commits since last tag
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
CURRENT_TAG="${{ needs.prepare.outputs.version }}"
# Collect commits and categorize them
declare -A features
declare -A bugs
declare -A improvements
declare -A performance
declare -A maintenance
if [ -n "$LAST_TAG" ]; then
RANGE="$LAST_TAG..HEAD"
else
RANGE="HEAD"
fi
# Process commits
while IFS= read -r line; do
sha=$(echo "$line" | cut -d' ' -f1)
msg=$(echo "$line" | cut -d' ' -f2-)
# Skip version bump and merge commits
if [[ "$msg" =~ "bump version" ]] || [[ "$msg" =~ "Merge pull request" ]] || [[ "$msg" =~ "Merge branch" ]]; then
continue
fi
categorized=$(categorize_commit "$msg")
category=$(echo "$categorized" | cut -d':' -f1)
clean_msg=$(echo "$categorized" | cut -d':' -f2-)
author=$(get_github_username "$sha")
# Store in associative arrays
case "$category" in
feature)
features["$clean_msg"]="$author"
;;
bug)
bugs["$clean_msg"]="$author"
;;
improvement)
improvements["$clean_msg"]="$author"
;;
performance)
performance["$clean_msg"]="$author"
;;
maintenance)
maintenance["$clean_msg"]="$author"
;;
esac
done < <(git log --oneline --no-merges $RANGE)
# Generate GitHub Release Notes
echo "## What's Changed" > CHANGELOG.md
echo "" >> CHANGELOG.md
if [ ${#features[@]} -gt 0 ]; then
echo "### 🚀 New Features" >> CHANGELOG.md
for msg in "${!features[@]}"; do
author="${features[$msg]}"
if [ -n "$author" ]; then
echo "* $msg by $author" >> CHANGELOG.md
else
echo "* $msg" >> CHANGELOG.md
fi
done
echo "" >> CHANGELOG.md
fi
if [ ${#bugs[@]} -gt 0 ]; then
echo "### 🐛 Bug Fixes" >> CHANGELOG.md
for msg in "${!bugs[@]}"; do
author="${bugs[$msg]}"
if [ -n "$author" ]; then
echo "* $msg by $author" >> CHANGELOG.md
else
echo "* $msg" >> CHANGELOG.md
fi
done
echo "" >> CHANGELOG.md
fi
if [ ${#improvements[@]} -gt 0 ]; then
echo "### 💪 Improvements" >> CHANGELOG.md
for msg in "${!improvements[@]}"; do
author="${improvements[$msg]}"
if [ -n "$author" ]; then
echo "* $msg by $author" >> CHANGELOG.md
else
echo "* $msg" >> CHANGELOG.md
fi
done
echo "" >> CHANGELOG.md
fi
if [ ${#performance[@]} -gt 0 ]; then
echo "### ⚡ Performance" >> CHANGELOG.md
for msg in "${!performance[@]}"; do
author="${performance[$msg]}"
if [ -n "$author" ]; then
echo "* $msg by $author" >> CHANGELOG.md
else
echo "* $msg" >> CHANGELOG.md
fi
done
echo "" >> CHANGELOG.md
fi
echo "" >> CHANGELOG.md
echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${LAST_TAG}...${{ needs.prepare.outputs.version }}" >> CHANGELOG.md
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.prepare.outputs.version }}
name: Release ${{ needs.prepare.outputs.version }}
body_path: CHANGELOG.md
draft: false
prerelease: false
files: |
${{ steps.assets.outputs.apk_path }}
${{ steps.assets.outputs.aab_path }}
fail_on_unmatched_files: false
play-store-upload:
name: Upload to Play Store
needs: [prepare, build-aab]
if: ${{ (needs.prepare.outputs.should_release == 'true' || github.event_name == 'workflow_dispatch') && vars.ENABLE_PLAY_STORE_UPLOAD == 'true' && vars.ENABLE_SIGNING == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Download AAB artifact
uses: actions/download-artifact@v4
with:
name: release-bundle
path: release-artifacts/
- name: Find bundle and symbols
id: find-files
run: |
AAB_PATH=$(find release-artifacts -name "*.aab" | head -1)
echo "aab_path=$AAB_PATH" >> $GITHUB_OUTPUT
# Look for debug symbols
SYMBOLS_PATH=$(find release-artifacts -name "native-debug-symbols.zip" 2>/dev/null | head -1)
if [ -n "$SYMBOLS_PATH" ]; then
echo "symbols_path=$SYMBOLS_PATH" >> $GITHUB_OUTPUT
echo "Found debug symbols at: $SYMBOLS_PATH"
else
echo "No debug symbols found"
fi
# Look for ProGuard/R8 mapping file
MAPPING_PATH=$(find release-artifacts -name "mapping.txt" 2>/dev/null | head -1)
if [ -n "$MAPPING_PATH" ]; then
echo "mapping_path=$MAPPING_PATH" >> $GITHUB_OUTPUT
echo "Found ReTrace mapping file at: $MAPPING_PATH"
else
echo "No ReTrace mapping file found"
fi
- name: Determine release track and status
id: release-config
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
TRACK="${{ github.event.inputs.track }}"
STATUS="${{ github.event.inputs.status }}"
else
# Default for tag pushes - use beta (public test track)
TRACK="beta"
STATUS="completed"
fi
echo "track=$TRACK" >> $GITHUB_OUTPUT
echo "status=$STATUS" >> $GITHUB_OUTPUT
echo "Deploying to track: $TRACK with status: $STATUS"
- name: Create whatsnew directory
env:
GH_TOKEN: ${{ github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
run: |
mkdir -p whatsnew
# Define shared functions (duplicated from release job since jobs run in isolation)
cat > /tmp/release_functions.sh << 'FUNCTIONS_EOF'
# Function to categorize commit message
categorize_commit() {
local msg="$1"
local category=""
local cleaned_msg=""
if [[ "$msg" =~ ^fix(\(.*\))?:\ (.*)$ ]] || [[ "$msg" =~ ^fix\ (.*)$ ]]; then
category="bug"
cleaned_msg="${BASH_REMATCH[-1]}"
elif [[ "$msg" =~ ^feat(\(.*\))?:\ (.*)$ ]] || [[ "$msg" =~ ^feat\ (.*)$ ]]; then
category="feature"
cleaned_msg="${BASH_REMATCH[-1]}"
elif [[ "$msg" =~ ^perf(\(.*\))?:\ (.*)$ ]]; then
category="performance"
cleaned_msg="${BASH_REMATCH[2]}"
elif [[ "$msg" =~ ^refactor(\(.*\))?:\ (.*)$ ]]; then
category="improvement"
cleaned_msg="${BASH_REMATCH[2]}"
elif [[ "$msg" =~ ^chore(\(.*\))?:\ (.*)$ ]]; then
category="maintenance"
cleaned_msg="${BASH_REMATCH[2]}"
elif [[ "$msg" =~ ^docs(\(.*\))?:\ (.*)$ ]]; then
category="documentation"
cleaned_msg="${BASH_REMATCH[2]}"
else
category="other"
cleaned_msg="$msg"
fi
# Remove PR numbers from the end
cleaned_msg=$(echo "$cleaned_msg" | sed 's/ (#[0-9]*)//')
# Capitalize first letter
cleaned_msg="$(echo "${cleaned_msg:0:1}" | tr '[:lower:]' '[:upper:]')${cleaned_msg:1}"
echo "$category:$cleaned_msg"
}
# Function to get GitHub username from commit
get_github_username() {
local commit_sha="$1"
local github_repo="${GITHUB_REPOSITORY}"
# Try to get the GitHub username from the commit using gh api
local username=$(gh api "repos/${github_repo}/commits/${commit_sha}" --jq '.author.login // empty' 2>/dev/null || echo "")
if [ -n "$username" ]; then
echo "@$username"
else
# Fallback: try to get committer login if author login is not available
local committer=$(gh api "repos/${github_repo}/commits/${commit_sha}" --jq '.committer.login // empty' 2>/dev/null || echo "")
if [ -n "$committer" ]; then
echo "@$committer"
else
# Last resort: use hardcoded mapping for known authors
local git_author=$(git show -s --format='%an' $commit_sha)
case "$git_author" in
"Gray Zhang" | "gray" | "Gray")
echo "@graycreate"
;;
"github-actions[bot]")
echo "@github-actions[bot]"
;;
*)
# If no mapping found, use git author name without @
echo "$git_author"
;;
esac
fi
fi
}
FUNCTIONS_EOF
# Source the shared functions
source /tmp/release_functions.sh
# Get commits since last tag for categorization
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
# Collect and categorize commits
declare -A features
declare -A bugs
declare -A improvements
declare -A performance
declare -A contributors
if [ -n "$LAST_TAG" ]; then
RANGE="$LAST_TAG..HEAD"
else
RANGE="HEAD~5..HEAD"
fi
# Process commits
while IFS= read -r line; do
sha=$(echo "$line" | cut -d' ' -f1)
msg=$(echo "$line" | cut -d' ' -f2-)
# Skip version bump and merge commits
if [[ "$msg" =~ "bump version" ]] || [[ "$msg" =~ "Merge pull request" ]] || [[ "$msg" =~ "Merge branch" ]]; then
continue
fi
categorized=$(categorize_commit "$msg")
category=$(echo "$categorized" | cut -d':' -f1)
clean_msg=$(echo "$categorized" | cut -d':' -f2-)
# Get author for this commit
author=$(get_github_username "$sha")
if [ -n "$author" ]; then
contributors["$author"]="1"
fi
case "$category" in
feature)
features["$clean_msg"]="$author"
;;
bug)
bugs["$clean_msg"]="$author"
;;
improvement)
improvements["$clean_msg"]="$author"
;;
performance)
performance["$clean_msg"]="$author"
;;
esac
done < <(git log --oneline --no-merges $RANGE)
# Generate English release notes
echo "V2er ${{ needs.prepare.outputs.version }}" > whatsnew/whatsnew-en-US
echo "" >> whatsnew/whatsnew-en-US
if [ ${#features[@]} -gt 0 ]; then
echo "🚀 New Features:" >> whatsnew/whatsnew-en-US
for msg in "${!features[@]}"; do
author="${features[$msg]}"
if [ -n "$author" ]; then
echo "• $msg (by $author)" >> whatsnew/whatsnew-en-US
else
echo "• $msg" >> whatsnew/whatsnew-en-US
fi
done
echo "" >> whatsnew/whatsnew-en-US
fi
if [ ${#bugs[@]} -gt 0 ]; then
echo "🐛 Bug Fixes:" >> whatsnew/whatsnew-en-US
for msg in "${!bugs[@]}"; do
author="${bugs[$msg]}"
if [ -n "$author" ]; then
echo "• $msg (by $author)" >> whatsnew/whatsnew-en-US
else
echo "• $msg" >> whatsnew/whatsnew-en-US
fi
done
echo "" >> whatsnew/whatsnew-en-US
fi
if [ ${#improvements[@]} -gt 0 ]; then
echo "💪 Improvements:" >> whatsnew/whatsnew-en-US
for msg in "${!improvements[@]}"; do
author="${improvements[$msg]}"
if [ -n "$author" ]; then
echo "• $msg (by $author)" >> whatsnew/whatsnew-en-US
else
echo "• $msg" >> whatsnew/whatsnew-en-US
fi
done
echo "" >> whatsnew/whatsnew-en-US
fi
if [ ${#performance[@]} -gt 0 ]; then
echo "⚡ Performance:" >> whatsnew/whatsnew-en-US
for msg in "${!performance[@]}"; do
author="${performance[$msg]}"
if [ -n "$author" ]; then
echo "• $msg (by $author)" >> whatsnew/whatsnew-en-US
else
echo "• $msg" >> whatsnew/whatsnew-en-US
fi
done
echo "" >> whatsnew/whatsnew-en-US
fi
# Add contributors section if there are any
if [ ${#contributors[@]} -gt 0 ]; then
echo "👥 Contributors: ${!contributors[@]}" >> whatsnew/whatsnew-en-US
echo "" >> whatsnew/whatsnew-en-US
fi
echo "Thank you for using V2er! Please report any issues on GitHub." >> whatsnew/whatsnew-en-US
# Generate Chinese release notes
echo "V2er ${{ needs.prepare.outputs.version }}" > whatsnew/whatsnew-zh-CN
echo "" >> whatsnew/whatsnew-zh-CN
if [ ${#features[@]} -gt 0 ]; then
echo "🚀 新功能:" >> whatsnew/whatsnew-zh-CN
for msg in "${!features[@]}"; do
author="${features[$msg]}"
if [ -n "$author" ]; then
echo "• $msg (贡献者 $author)" >> whatsnew/whatsnew-zh-CN
else
echo "• $msg" >> whatsnew/whatsnew-zh-CN
fi
done
echo "" >> whatsnew/whatsnew-zh-CN
fi
if [ ${#bugs[@]} -gt 0 ]; then
echo "🐛 问题修复:" >> whatsnew/whatsnew-zh-CN
for msg in "${!bugs[@]}"; do
author="${bugs[$msg]}"
if [ -n "$author" ]; then
echo "• $msg (贡献者 $author)" >> whatsnew/whatsnew-zh-CN
else
echo "• $msg" >> whatsnew/whatsnew-zh-CN
fi
done
echo "" >> whatsnew/whatsnew-zh-CN
fi
if [ ${#improvements[@]} -gt 0 ]; then
echo "💪 改进优化:" >> whatsnew/whatsnew-zh-CN
for msg in "${!improvements[@]}"; do
author="${improvements[$msg]}"
if [ -n "$author" ]; then
echo "• $msg (贡献者 $author)" >> whatsnew/whatsnew-zh-CN
else
echo "• $msg" >> whatsnew/whatsnew-zh-CN
fi
done
echo "" >> whatsnew/whatsnew-zh-CN
fi
if [ ${#performance[@]} -gt 0 ]; then
echo "⚡ 性能优化:" >> whatsnew/whatsnew-zh-CN
for msg in "${!performance[@]}"; do
author="${performance[$msg]}"
if [ -n "$author" ]; then
echo "• $msg (贡献者 $author)" >> whatsnew/whatsnew-zh-CN
else
echo "• $msg" >> whatsnew/whatsnew-zh-CN
fi
done
echo "" >> whatsnew/whatsnew-zh-CN
fi
# Add contributors section if there are any
if [ ${#contributors[@]} -gt 0 ]; then
echo "👥 贡献者:${!contributors[@]}" >> whatsnew/whatsnew-zh-CN
echo "" >> whatsnew/whatsnew-zh-CN
fi
echo "感谢您使用 V2er!如遇问题请在 GitHub 上反馈。" >> whatsnew/whatsnew-zh-CN
- name: Upload to Play Store (with debug symbols)
if: steps.find-files.outputs.symbols_path != ''
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }}
packageName: me.ghui.v2er
releaseFiles: ${{ steps.find-files.outputs.aab_path }}
track: ${{ steps.release-config.outputs.track }}
status: ${{ steps.release-config.outputs.status }}
debugSymbols: ${{ steps.find-files.outputs.symbols_path }}
mappingFile: ${{ steps.find-files.outputs.mapping_path }}
whatsNewDirectory: whatsnew/
continue-on-error: true
id: upload-with-symbols
- name: Upload to Play Store (without debug symbols)
if: steps.find-files.outputs.symbols_path == '' || steps.upload-with-symbols.outcome == 'failure'
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }}
packageName: me.ghui.v2er
releaseFiles: ${{ steps.find-files.outputs.aab_path }}
track: ${{ steps.release-config.outputs.track }}
status: ${{ steps.release-config.outputs.status }}
mappingFile: ${{ steps.find-files.outputs.mapping_path }}
whatsNewDirectory: whatsnew/
- name: Play Store Upload Summary
if: success()
run: |
echo "## Play Store Upload Complete :rocket:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Version**: ${{ needs.prepare.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "- **Track**: ${{ steps.release-config.outputs.track }}" >> $GITHUB_STEP_SUMMARY
echo "- **Status**: ${{ steps.release-config.outputs.status }}" >> $GITHUB_STEP_SUMMARY
echo "- **Package**: me.ghui.v2er" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.release-config.outputs.track }}" = "beta" ]; then
echo "### Beta Testing" >> $GITHUB_STEP_SUMMARY
echo "This release is now available for beta testing on Google Play." >> $GITHUB_STEP_SUMMARY
echo "Beta testers can install it from: [Google Play Beta](https://play.google.com/store/apps/details?id=me.ghui.v2er)" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "[View in Play Console](https://play.google.com/console/u/0/app/me.ghui.v2er)" >> $GITHUB_STEP_SUMMARY
download-signed-apk:
name: Download Google Play Signed APK
needs: [prepare, play-store-upload]
if: ${{ (needs.prepare.outputs.should_release == 'true' || github.event_name == 'workflow_dispatch') && vars.ENABLE_PLAY_STORE_UPLOAD == 'true' && vars.ENABLE_SIGNING == 'true' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
pip install google-api-python-client google-auth-httplib2 google-auth-oauthlib requests
- name: Check and wait for Google Play processing
env:
PLAY_STORE_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }}
run: |
VERSION_CODE="${{ needs.prepare.outputs.version_code }}"
PACKAGE_NAME="me.ghui.v2er"
# Create Python script to check if APK exists
cat > check_apk_exists.py << 'EOF'
import json
import os
import sys
import time
from google.oauth2 import service_account
from googleapiclient.discovery import build
def check_apk_exists():
try:
# Load service account credentials
service_account_info = json.loads(os.environ['PLAY_STORE_SERVICE_ACCOUNT_JSON'])
credentials = service_account.Credentials.from_service_account_info(
service_account_info,
scopes=['https://www.googleapis.com/auth/androidpublisher']
)
# Build the service
service = build('androidpublisher', 'v3', credentials=credentials)
package_name = os.environ['PACKAGE_NAME']
version_code = int(os.environ['VERSION_CODE'])
print(f"Checking if signed APK exists for {package_name} version {version_code}")
# Try to get the generated APKs list
result = service.generatedapks().list(
packageName=package_name,
versionCode=version_code
).execute()
if 'generatedApks' not in result or not result['generatedApks']:
print(f"No generated APKs found for version {version_code}")
return False
print(f"Found {len(result['generatedApks'])} generated APK groups")
# Check if we can find a universal APK
for apk in result['generatedApks']:
if 'generatedUniversalApk' in apk:
universal_apk = apk['generatedUniversalApk']
download_id = universal_apk.get('downloadId')
if download_id:
print(f"✅ Universal APK found with downloadId: {download_id}")
return True
print("❌ No universal APK found")
return False
except Exception as e:
print(f"Error checking APK: {str(e)}")
return False
if __name__ == "__main__":
exists = check_apk_exists()
sys.exit(0 if exists else 1)
EOF
# Set environment variables for the script
export PACKAGE_NAME="$PACKAGE_NAME"
export VERSION_CODE="$VERSION_CODE"
# Check if APK already exists
echo "Checking if Google Play signed APK is ready..."
if python3 check_apk_exists.py; then
echo "✅ APK is already available, skipping wait"
else
echo "⏳ APK not ready yet, waiting for Google Play to process..."
# Smart waiting with periodic checks
MAX_WAIT=600 # Maximum 10 minutes
CHECK_INTERVAL=30 # Check every 30 seconds
elapsed=0
while [ $elapsed -lt $MAX_WAIT ]; do
sleep $CHECK_INTERVAL
elapsed=$((elapsed + CHECK_INTERVAL))
echo "⏱️ Waited ${elapsed}s, checking again..."
if python3 check_apk_exists.py; then
echo "✅ APK is now available after ${elapsed}s"
break
fi
if [ $elapsed -ge $MAX_WAIT ]; then
echo "⚠️ Maximum wait time (${MAX_WAIT}s) reached"
echo "APK may still be processing, will attempt download anyway"
fi
done
fi
# AAB artifact not needed for Google Play signed APK download
- name: Download Google Play Signed APK
id: download-apk
env:
PLAY_STORE_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT_JSON }}
run: |
VERSION_NAME="${{ needs.prepare.outputs.version }}"
VERSION_CODE="${{ needs.prepare.outputs.version_code }}"
PACKAGE_NAME="me.ghui.v2er"
# Create Python script to download signed universal APK
cat > download_signed_apk.py << 'EOF'
import json
import os
import sys
import requests
from google.oauth2 import service_account
from googleapiclient.discovery import build
def download_signed_apk():
try:
# Load service account credentials
service_account_info = json.loads(os.environ['PLAY_STORE_SERVICE_ACCOUNT_JSON'])
credentials = service_account.Credentials.from_service_account_info(
service_account_info,
scopes=['https://www.googleapis.com/auth/androidpublisher']
)
# Build the service
service = build('androidpublisher', 'v3', credentials=credentials)
package_name = os.environ['PACKAGE_NAME']
version_code = int(os.environ['VERSION_CODE'])
print(f"Attempting to download signed APK for {package_name} version {version_code}")
# Step 1: Get the generated APKs list to find downloadId
print("Getting generated APKs list...")
result = service.generatedapks().list(
packageName=package_name,
versionCode=version_code
).execute()
if 'generatedApks' not in result or not result['generatedApks']:
print("No generated APKs found. App may not be processed yet by Google Play.")
return False
print(f"Found {len(result['generatedApks'])} generated APKs")
# Debug: Print all APK structures
for i, apk in enumerate(result['generatedApks']):
print(f"APK {i} structure:")
for key, value in apk.items():
print(f" {key}: {value}")
print()
# Find universal APK using the correct API structure
download_id = None
universal_apk = None
# First, try to find a universal APK in generatedUniversalApk
for apk in result['generatedApks']:
if 'generatedUniversalApk' in apk:
universal_apk = apk['generatedUniversalApk']
download_id = universal_apk.get('downloadId')
print(f"Found universal APK: {universal_apk}")
break
if not download_id:
print("No universal APK found")
print("Available APK structure:")
print(json.dumps(result['generatedApks'], indent=2))
return False
print(f"Found universal APK with downloadId: {download_id}")
# Step 2: Download the APK using the downloadId
print("Downloading APK binary...")
# Use alt=media to get the actual binary content instead of metadata
download_request = service.generatedapks().download(
packageName=package_name,
versionCode=version_code,
downloadId=download_id
)
# Add alt=media parameter correctly (URL already has query params, so use &)
if '?' in download_request.uri:
download_request.uri += '&alt=media'
else:
download_request.uri += '?alt=media'
output_filename = f"v2er-{os.environ['VERSION_NAME']}_google_play_signed.apk"
# Use media download with googleapiclient.http to handle binary content
import io
from googleapiclient.http import MediaIoBaseDownload
file_io = io.BytesIO()
downloader = MediaIoBaseDownload(file_io, download_request)
done = False
while done is False:
status, done = downloader.next_chunk()
if status:
print(f"Download progress: {int(status.progress() * 100)}%")
# Write to file
with open(output_filename, 'wb') as f:
f.write(file_io.getvalue())
print(f"Successfully downloaded: {output_filename}")
print(f"apk_path={output_filename}")
return True
except Exception as e:
print(f"Error downloading signed APK: {str(e)}")
print("This may be because:")
print("1. The app hasn't been processed by Google Play yet")
print("2. The version hasn't been released to any track")
print("3. API permissions are insufficient")
return False
if __name__ == "__main__":
success = download_signed_apk()
sys.exit(0 if success else 1)
EOF
# Set environment variables for the script
export PACKAGE_NAME="$PACKAGE_NAME"
export VERSION_CODE="$VERSION_CODE"
export VERSION_NAME="$VERSION_NAME"
# Run the download script
echo "Attempting to download Google Play signed APK..."
if python3 download_signed_apk.py > download_output.txt 2>&1; then
echo "Successfully downloaded Google Play signed APK"
cat download_output.txt
# Extract the APK path from output
APK_PATH=$(grep "apk_path=" download_output.txt | cut -d'=' -f2)
if [ -f "$APK_PATH" ]; then
echo "apk_path=$APK_PATH" >> $GITHUB_OUTPUT
echo "found=true" >> $GITHUB_OUTPUT
else
echo "APK file not found after download"
echo "found=false" >> $GITHUB_OUTPUT
fi
else
echo "Failed to download Google Play signed APK"
cat download_output.txt
echo "found=false" >> $GITHUB_OUTPUT
fi
- name: Upload signed APK to GitHub Release
if: steps.download-apk.outputs.found == 'true'
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.prepare.outputs.version }}
files: |
${{ steps.download-apk.outputs.apk_path }}
fail_on_unmatched_files: false
- name: Summary
if: always()
run: |
echo "## Google Play Signed APK :package:" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.download-apk.outputs.found }}" = "true" ]; then
echo "✅ **Universal APK generated successfully**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Version**: ${{ needs.prepare.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "- **Version Code**: ${{ needs.prepare.outputs.version_code }}" >> $GITHUB_STEP_SUMMARY
echo "- **File**: v2er-${{ needs.prepare.outputs.version }}_google_play_signed.apk" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Notes" >> $GITHUB_STEP_SUMMARY
echo "- This APK is generated from the AAB uploaded to Google Play" >> $GITHUB_STEP_SUMMARY
echo "- When installed from Play Store, it will use Google Play's signing certificate" >> $GITHUB_STEP_SUMMARY
echo "- The APK has been uploaded to the GitHub Release" >> $GITHUB_STEP_SUMMARY
else
echo "⚠️ **Google Play signed APK download failed**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "This may be because:" >> $GITHUB_STEP_SUMMARY
echo "- Google Play is still processing the upload" >> $GITHUB_STEP_SUMMARY
echo "- The version hasn't been released to any track yet" >> $GITHUB_STEP_SUMMARY
echo "- API permissions are insufficient" >> $GITHUB_STEP_SUMMARY
fi