chore: 更新版本号至 v3.8.2 并添加更新日志 #161
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release on Version Bump | |
| on: | |
| push: | |
| branches: ["main", "master"] | |
| paths: | |
| - "app/build.gradle" | |
| - "app/build.gradle.kts" | |
| - "gradle.properties" | |
| - "app/src/main/AndroidManifest.xml" | |
| workflow_dispatch: {} | |
| concurrency: | |
| group: release-on-version-bump | |
| cancel-in-progress: false | |
| jobs: | |
| release: | |
| name: Build and Release | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout with full history | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # 获取完整历史以便生成 changelog | |
| - name: Set up Java 21 | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: "temurin" | |
| java-version: "21" | |
| cache: "gradle" | |
| - name: Validate Gradle Wrapper | |
| uses: gradle/wrapper-validation-action@v2 | |
| continue-on-error: true | |
| timeout-minutes: 2 | |
| - name: Set up Gradle | |
| uses: gradle/actions/setup-gradle@v3 | |
| - name: Detect version change | |
| id: version | |
| env: | |
| GITHUB_EVENT_BEFORE: ${{ github.event.before }} | |
| run: | | |
| python3 - <<'PY' | |
| import os, re, subprocess | |
| def read_file(path): | |
| try: | |
| with open(path, 'r', encoding='utf-8') as f: | |
| return f.read() | |
| except FileNotFoundError: | |
| return None | |
| def parse_versions(text): | |
| if not text: | |
| return None, None | |
| vn = None | |
| vc = None | |
| m = re.search(r'versionName\s*[= ]\s*"?([0-9A-Za-z\.-_\+]+)"?', text) | |
| if m: | |
| vn = m.group(1) | |
| m = re.search(r'versionCode\s*[= ]\s*"?([0-9]+)"?', text) | |
| if m: | |
| vc = m.group(1) | |
| if vn is None: | |
| m = re.search(r'(?im)^\s*(?:VERSION_NAME|versionName)\s*=\s*([^\s#]+)', text) | |
| if m: | |
| vn = m.group(1).strip() | |
| if vc is None: | |
| m = re.search(r'(?im)^\s*(?:VERSION_CODE|versionCode)\s*=\s*([0-9]+)', text) | |
| if m: | |
| vc = m.group(1).strip() | |
| if vn is None: | |
| m = re.search(r'android:versionName\s*=\s*"([^"]+)"', text) | |
| if m: | |
| vn = m.group(1) | |
| if vc is None: | |
| m = re.search(r'android:versionCode\s*=\s*"([^"]+)"', text) | |
| if m: | |
| vc = m.group(1) | |
| return vn, vc | |
| def get_file_at_rev(path, rev=None): | |
| if rev is None: | |
| return read_file(path) | |
| try: | |
| out = subprocess.check_output(['git', 'show', f'{rev}:{path}'], stderr=subprocess.STDOUT) | |
| return out.decode('utf-8', errors='ignore') | |
| except subprocess.CalledProcessError: | |
| return None | |
| paths = ['app/build.gradle', 'app/build.gradle.kts', 'gradle.properties', 'app/src/main/AndroidManifest.xml'] | |
| text_head = None | |
| for p in paths: | |
| content = get_file_at_rev(p, None) | |
| if content: | |
| text_head = content | |
| break | |
| vn_head, vc_head = parse_versions(text_head) | |
| prev_rev = os.environ.get('GITHUB_EVENT_BEFORE') or 'HEAD^' | |
| text_prev = None | |
| for p in paths: | |
| content = get_file_at_rev(p, prev_rev) | |
| if content: | |
| text_prev = content | |
| break | |
| vn_prev, vc_prev = parse_versions(text_prev) | |
| # 只检查 versionCode 是否变化 | |
| changed = False | |
| if vc_head and (vc_head != (vc_prev or '')): | |
| changed = True | |
| with open(os.environ['GITHUB_OUTPUT'], 'a') as f: | |
| f.write(f'version_name={vn_head or ""}\n') | |
| f.write(f'version_code={vc_head or ""}\n') | |
| f.write(f'prev_version_name={vn_prev or ""}\n') | |
| f.write(f'prev_version_code={vc_prev or ""}\n') | |
| f.write(f'changed={"true" if changed else "false"}\n') | |
| PY | |
| - name: Check if version changed | |
| if: steps.version.outputs.changed != 'true' | |
| run: | | |
| echo "::notice::versionCode 未变化 (当前: ${{ steps.version.outputs.version_code }}),跳过构建和发布流程" | |
| echo "ℹ️ versionCode 未变化,跳过 release" | |
| echo "📌 当前版本: ${{ steps.version.outputs.version_name }} (versionCode: ${{ steps.version.outputs.version_code }})" | |
| echo "📌 上一版本: ${{ steps.version.outputs.prev_version_name }} (versionCode: ${{ steps.version.outputs.prev_version_code }})" | |
| echo "💡 提示: 只有当 versionCode 发生变化时才会触发构建和发布" | |
| - name: Get previous tag | |
| if: steps.version.outputs.changed == 'true' | |
| id: prev_tag | |
| run: | | |
| # 获取上一个 tag | |
| PREV_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") | |
| echo "prev_tag=$PREV_TAG" >> "$GITHUB_OUTPUT" | |
| echo "上一个 tag: $PREV_TAG" | |
| - name: Compose current tag | |
| if: steps.version.outputs.changed == 'true' | |
| id: tag | |
| run: | | |
| V="${{ steps.version.outputs.version_name }}" | |
| if [ -z "$V" ]; then V="${{ steps.version.outputs.version_code }}"; fi | |
| if [ -z "$V" ]; then V="$(date +%Y%m%d)-${{ github.sha }}"; fi | |
| echo "tag=v$V" >> "$GITHUB_OUTPUT" | |
| echo "version_display=$V" >> "$GITHUB_OUTPUT" | |
| echo "当前 tag: v$V" | |
| - name: Generate changelog between tags | |
| if: steps.version.outputs.changed == 'true' | |
| id: changelog | |
| run: | | |
| PREV_TAG="${{ steps.prev_tag.outputs.prev_tag }}" | |
| CURRENT_TAG="${{ steps.tag.outputs.tag }}" | |
| if [ -z "$PREV_TAG" ]; then | |
| echo "## 📝 更新日志" > changelog.md | |
| echo "" >> changelog.md | |
| echo "这是首个发布版本。" >> changelog.md | |
| echo "" >> changelog.md | |
| echo "### 所有提交" >> changelog.md | |
| # 过滤 chore/docs;提交哈希可点击;去掉时间;单行展示 | |
| REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" | |
| git log --no-merges \ | |
| --grep='^chore' --grep='^docs' --invert-grep \ | |
| --pretty=format:"- %s | 提交:[%h]($REPO_URL/commit/%H) | 作者:%an" >> changelog.md | |
| # 生成简短版本用于 version.json | |
| echo "首个发布版本" > release_notes_short.txt | |
| else | |
| echo "## 📝 更新日志 ($PREV_TAG → $CURRENT_TAG)" > changelog.md | |
| echo "" >> changelog.md | |
| echo "### 📋 本次更新内容" >> changelog.md | |
| echo "" >> changelog.md | |
| # 获取详细的 commit 信息(过滤 chore/docs;哈希可点击;去掉时间;单行展示) | |
| REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" | |
| git log $PREV_TAG..HEAD --no-merges \ | |
| --grep='^chore' --grep='^docs' --invert-grep \ | |
| --pretty=format:"- %s | 提交:[%h]($REPO_URL/commit/%H) | 作者:%an" >> changelog.md | |
| echo "" >> changelog.md | |
| echo "" >> changelog.md | |
| echo "### 📊 统计信息" >> changelog.md | |
| # 统计同样基于过滤后的提交集 | |
| COMMIT_COUNT=$(git log $PREV_TAG..HEAD --no-merges --grep='^chore' --grep='^docs' --invert-grep --pretty=oneline | wc -l | tr -d ' ') | |
| echo "- 提交数量: $COMMIT_COUNT" >> changelog.md | |
| FILES_CHANGED=$(git diff --shortstat $PREV_TAG..HEAD | sed 's/^[[:space:]]*//') | |
| if [ -n "$FILES_CHANGED" ]; then | |
| echo "- 文件变更: $FILES_CHANGED" >> changelog.md | |
| fi | |
| # 生成简短版本用于 version.json(只包含 commit 标题) | |
| echo "本次更新包含 $COMMIT_COUNT 个提交:" > release_notes_short.txt | |
| git log $PREV_TAG..HEAD --no-merges \ | |
| --grep='^chore' --grep='^docs' --invert-grep \ | |
| --pretty=format:"- %s" >> release_notes_short.txt | |
| fi | |
| # 不再通过 $GITHUB_OUTPUT 传递多行内容,以避免分隔符解析问题。 | |
| # 后续步骤直接读取 changelog.md 与 release_notes_short.txt 文件。 | |
| echo "✅ changelog 与简短更新说明已生成到工作区文件。" | |
| - name: Set up Android SDK | |
| if: steps.version.outputs.changed == 'true' | |
| uses: android-actions/setup-android@v3 | |
| with: | |
| api-level: 36 | |
| build-tools: 36.0.0 | |
| - name: Check if keystore is configured | |
| if: steps.version.outputs.changed == 'true' | |
| id: check_keystore | |
| env: | |
| KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} | |
| run: | | |
| if [ -n "$KEYSTORE_BASE64" ]; then | |
| echo "has_keystore=true" >> "$GITHUB_OUTPUT" | |
| echo "✅ 检测到签名密钥配置" | |
| else | |
| echo "has_keystore=false" >> "$GITHUB_OUTPUT" | |
| echo "⚠️ 未配置签名密钥,将构建 Debug APK" | |
| fi | |
| - name: Decode and setup keystore | |
| if: steps.version.outputs.changed == 'true' && steps.check_keystore.outputs.has_keystore == 'true' | |
| env: | |
| KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} | |
| run: | | |
| echo "解码签名密钥..." | |
| echo "$KEYSTORE_BASE64" | base64 -d > release.keystore | |
| echo "KEYSTORE_FILE=${{ github.workspace }}/release.keystore" >> $GITHUB_ENV | |
| ls -lh release.keystore | |
| - name: Make Gradle wrapper executable | |
| if: steps.version.outputs.changed == 'true' | |
| run: | | |
| chmod +x ./gradlew || true | |
| sed -i -e 's/\r$//' ./gradlew || true | |
| - name: Build Release APK with signing | |
| if: steps.version.outputs.changed == 'true' && steps.check_keystore.outputs.has_keystore == 'true' | |
| env: | |
| KEYSTORE_FILE: ${{ env.KEYSTORE_FILE }} | |
| KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }} | |
| KEY_ALIAS: ${{ secrets.KEY_ALIAS }} | |
| KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} | |
| SF_FREE_API_KEY: ${{ secrets.SF_FREE_API_KEY }} | |
| run: | | |
| echo "使用签名密钥构建 Release APK..." | |
| ./gradlew --no-daemon clean :app:assembleRelease | |
| ls -lah app/build/outputs/apk/release/ || true | |
| - name: Build Debug APK (fallback) | |
| if: steps.version.outputs.changed == 'true' && steps.check_keystore.outputs.has_keystore != 'true' | |
| env: | |
| SF_FREE_API_KEY: ${{ secrets.SF_FREE_API_KEY }} | |
| run: | | |
| echo "未配置签名密钥,构建 Debug APK..." | |
| ./gradlew --no-daemon clean :app:assembleDebug | |
| - name: Locate built APK | |
| if: steps.version.outputs.changed == 'true' | |
| id: locate_apk | |
| run: | | |
| # 优先查找 release APK | |
| RELEASE_APK=$(ls app/build/outputs/apk/release/*.apk 2>/dev/null | grep -v unaligned | head -n 1 || true) | |
| DEBUG_APK=$(ls app/build/outputs/apk/debug/*.apk 2>/dev/null | head -n 1 || true) | |
| if [ -n "$RELEASE_APK" ]; then | |
| APK_PATH="$RELEASE_APK" | |
| APK_TYPE="release" | |
| elif [ -n "$DEBUG_APK" ]; then | |
| APK_PATH="$DEBUG_APK" | |
| APK_TYPE="debug" | |
| else | |
| APK_PATH="" | |
| APK_TYPE="none" | |
| fi | |
| echo "apk_path=$APK_PATH" >> "$GITHUB_OUTPUT" | |
| echo "apk_type=$APK_TYPE" >> "$GITHUB_OUTPUT" | |
| if [ -n "$APK_PATH" ]; then | |
| echo "找到 APK ($APK_TYPE): $APK_PATH" | |
| ls -lh "$APK_PATH" | |
| else | |
| echo "未找到 APK 文件" | |
| fi | |
| - name: Rename APK | |
| if: steps.version.outputs.changed == 'true' && steps.locate_apk.outputs.apk_path != '' | |
| id: rename_apk | |
| run: | | |
| APK_PATH="${{ steps.locate_apk.outputs.apk_path }}" | |
| APK_TYPE="${{ steps.locate_apk.outputs.apk_type }}" | |
| VERSION="${{ steps.version.outputs.version_name }}" | |
| # 生成新文件名 | |
| NEW_NAME="lexisharp-keyboard-${VERSION}-${APK_TYPE}.apk" | |
| NEW_PATH="app/build/outputs/apk/${NEW_NAME}" | |
| cp "$APK_PATH" "$NEW_PATH" | |
| echo "renamed_apk_path=$NEW_PATH" >> "$GITHUB_OUTPUT" | |
| echo "重命名 APK: $NEW_NAME" | |
| ls -lh "$NEW_PATH" | |
| - name: Update version.json | |
| if: steps.version.outputs.changed == 'true' | |
| id: update_version_json | |
| run: | | |
| echo "📝 更新 version.json 文件..." | |
| # 使用 Python 生成 JSON(确保格式正确) | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| from datetime import datetime | |
| version = os.environ.get('VERSION', '0.0.0') | |
| version_code = os.environ.get('VERSION_CODE', '0') | |
| tag = os.environ.get('TAG', 'v0.0.0') | |
| # 优先使用 env 中的 RELEASE_NOTES;若为空,则读取文件 release_notes_short.txt | |
| release_notes = os.environ.get('RELEASE_NOTES') | |
| if not release_notes: | |
| try: | |
| with open('release_notes_short.txt', 'r', encoding='utf-8') as f: | |
| release_notes = f.read().strip() | |
| except FileNotFoundError: | |
| release_notes = f'版本 {version} 已发布' | |
| # 使用 ISO 8601 格式的时间戳(UTC) | |
| current_timestamp = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') | |
| # 读取现有的 version.json(如果存在) | |
| try: | |
| with open('version.json', 'r', encoding='utf-8') as f: | |
| data = json.load(f) | |
| except (FileNotFoundError, json.JSONDecodeError): | |
| data = {} | |
| # 更新版本信息 | |
| data['version'] = version | |
| data['version_code'] = int(version_code) if version_code.isdigit() else 0 | |
| data['download_url'] = f"https://github.com/BryceWG/LexiSharp-Keyboard/releases/tag/{tag}" | |
| data['release_notes'] = release_notes | |
| data['min_supported_version'] = data.get('min_supported_version', '2.6.5') | |
| data['update_time'] = current_timestamp | |
| # 写入文件 | |
| with open('version.json', 'w', encoding='utf-8') as f: | |
| json.dump(data, f, ensure_ascii=False, indent=2) | |
| print(f"✅ version.json 已更新为版本 {version}") | |
| PY | |
| # 显示更新后的内容 | |
| echo "📄 version.json 内容:" | |
| cat version.json | |
| # 检查文件是否有变化 | |
| if git diff --quiet version.json; then | |
| echo "version_changed=false" >> "$GITHUB_OUTPUT" | |
| echo "ℹ️ version.json 无变化" | |
| else | |
| echo "version_changed=true" >> "$GITHUB_OUTPUT" | |
| echo "✅ version.json 已更新" | |
| fi | |
| env: | |
| VERSION: ${{ steps.version.outputs.version_name }} | |
| VERSION_CODE: ${{ steps.version.outputs.version_code }} | |
| TAG: ${{ steps.tag.outputs.tag }} | |
| # RELEASE_NOTES 通过文件回退,不再依赖多行 step 输出 | |
| RELEASE_NOTES: "" | |
| - name: Commit and push version.json | |
| if: steps.version.outputs.changed == 'true' && steps.update_version_json.outputs.version_changed == 'true' | |
| run: | | |
| echo "📤 提交 version.json 更新..." | |
| git config --local user.email "github-actions[bot]@users.noreply.github.com" | |
| git config --local user.name "github-actions[bot]" | |
| # 最多重试 3 次 | |
| MAX_RETRIES=3 | |
| RETRY_COUNT=0 | |
| while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do | |
| echo "🔄 尝试推送 (第 $((RETRY_COUNT + 1)) 次)..." | |
| # 拉取最新的远程更改 | |
| git fetch origin ${{ github.ref_name }} | |
| git reset --hard origin/${{ github.ref_name }} | |
| # 重新应用 version.json 更改 | |
| python3 - <<'PY' | |
| import json | |
| import os | |
| from datetime import datetime | |
| version = os.environ.get('VERSION', '0.0.0') | |
| version_code = os.environ.get('VERSION_CODE', '0') | |
| tag = os.environ.get('TAG', 'v0.0.0') | |
| release_notes = os.environ.get('RELEASE_NOTES') | |
| if not release_notes: | |
| try: | |
| with open('release_notes_short.txt', 'r', encoding='utf-8') as f: | |
| release_notes = f.read().strip() | |
| except FileNotFoundError: | |
| release_notes = f'版本 {version} 已发布' | |
| current_timestamp = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') | |
| try: | |
| with open('version.json', 'r', encoding='utf-8') as f: | |
| data = json.load(f) | |
| except (FileNotFoundError, json.JSONDecodeError): | |
| data = {} | |
| data['version'] = version | |
| data['version_code'] = int(version_code) if version_code.isdigit() else 0 | |
| data['download_url'] = f"https://github.com/BryceWG/LexiSharp-Keyboard/releases/tag/{tag}" | |
| data['release_notes'] = release_notes | |
| data['min_supported_version'] = data.get('min_supported_version', '2.6.5') | |
| data['update_time'] = current_timestamp | |
| with open('version.json', 'w', encoding='utf-8') as f: | |
| json.dump(data, f, ensure_ascii=False, indent=2) | |
| PY | |
| # 检查是否有变化 | |
| if git diff --quiet version.json; then | |
| echo "ℹ️ version.json 无变化,无需推送" | |
| break | |
| fi | |
| # 提交更改 | |
| git add version.json | |
| git commit -m "chore: auto-update version.json to ${{ steps.version.outputs.version_name }} [skip ci]" | |
| # 尝试推送 | |
| if git push origin HEAD:${{ github.ref_name }}; then | |
| echo "✅ version.json 已成功推送" | |
| break | |
| else | |
| echo "⚠️ 推送失败,可能存在冲突" | |
| RETRY_COUNT=$((RETRY_COUNT + 1)) | |
| if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then | |
| echo "⏳ 等待 5 秒后重试..." | |
| sleep 5 | |
| else | |
| echo "❌ 达到最大重试次数,推送失败" | |
| exit 1 | |
| fi | |
| fi | |
| done | |
| env: | |
| VERSION: ${{ steps.version.outputs.version_name }} | |
| VERSION_CODE: ${{ steps.version.outputs.version_code }} | |
| TAG: ${{ steps.tag.outputs.tag }} | |
| RELEASE_NOTES: "" | |
| - name: Create GitHub Release | |
| if: steps.version.outputs.changed == 'true' | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.tag.outputs.tag }} | |
| name: "Release ${{ steps.tag.outputs.version_display }}" | |
| body_path: changelog.md | |
| files: ${{ steps.rename_apk.outputs.renamed_apk_path }} | |
| draft: false | |
| prerelease: false | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} |