Skip to content

chore: 更新版本号至 v3.8.2 并添加更新日志 #161

chore: 更新版本号至 v3.8.2 并添加更新日志

chore: 更新版本号至 v3.8.2 并添加更新日志 #161

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 }}