1- name : Build Android APK
1+ Name : Build Android APK
22
33on :
44 workflow_dispatch :
@@ -33,45 +33,45 @@ jobs:
3333
3434 steps :
3535 - name : Checkout
36- uses : actions/checkout@v5
36+ uses : actions/checkout@v4
37+
38+ - name : Setup Java
39+ uses : actions/setup-java@v4
40+ with :
41+ distribution : ' zulu'
42+ java-version : ' 17'
3743
3844 - name : Setup Flutter
3945 uses : subosito/flutter-action@v2
4046 with :
4147 channel : stable
4248 cache : true
4349
44- - name : Create Flutter skeleton (Android only)
50+ - name : Create Flutter skeleton
4551 run : |
52+ # 每次全新生成项目结构
4653 flutter create --platforms=android --org com.linkweb linkweb_build
4754
4855 - name : Copy sources into skeleton
4956 run : |
57+ # 将你的代码覆盖进去
5058 rsync -a app/ linkweb_build/
5159
52- - name : Debug - Check file structure
53- run : |
54- echo "📂 Checking Android build files:"
55- find linkweb_build/android -name "build.gradle*" -type f
56- echo ""
57- echo "📂 App directory structure:"
58- ls -la linkweb_build/android/app/ || true
59-
6060 - name : Force Android minSdk = 24
6161 run : |
6262 set -euo pipefail
6363 GROOVY="linkweb_build/android/app/build.gradle"
6464 KTS="linkweb_build/android/app/build.gradle.kts"
6565
6666 if [ -f "$GROOVY" ]; then
67- echo "✅ Found build.gradle"
67+ echo "✅ Found build.gradle (Groovy) "
6868 sed -i -E 's/minSdkVersion[[:space:]]+[^[:space:]]+/minSdkVersion 24/' "$GROOVY"
6969 elif [ -f "$KTS" ]; then
70- echo "✅ Found build.gradle.kts"
70+ echo "✅ Found build.gradle.kts (Kotlin)"
71+ # Kotlin DSL 语法稍有不同
7172 sed -i -E 's/minSdk[[:space:]]*=[[:space:]]*[^[:space:]]+/minSdk = 24/' "$KTS"
7273 else
73- echo "❌ No Gradle file found, checking all locations:"
74- find linkweb_build/android -maxdepth 5 -type f -name "build.gradle*"
74+ echo "❌ No Gradle file found"
7575 exit 1
7676 fi
7777
9494 with open(manifest_path, 'r', encoding='utf-8') as f:
9595 content = f.read()
9696
97+ # 替换 label
9798 content = re.sub(
9899 r'android:label="[^"]*"',
99100 f'android:label="{app_name}"',
@@ -102,105 +103,127 @@ jobs:
102103
103104 with open(manifest_path, 'w', encoding='utf-8') as f:
104105 f.write(content)
105-
106106 print(f"✅ App name set to: {app_name}")
107107 except Exception as e:
108- print(f"❌ Error: {e}")
108+ print(f"❌ Error setting app name : {e}")
109109 exit(1)
110110 PYTHON_SCRIPT
111111
112- - name : Verify Manifest
113- run : |
114- echo "📄 Checking AndroidManifest.xml:"
115- grep "android:label" linkweb_build/android/app/src/main/AndroidManifest.xml || true
116-
117112 - name : Prepare Keystore
118113 run : |
119114 set -euo pipefail
120115
121- [ -n "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" ] || { echo "❌ Missing ANDROID_KEYSTORE_BASE64"; exit 1; }
122- [ -n "${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" ] || { echo "❌ Missing ANDROID_KEYSTORE_PASSWORD"; exit 1; }
123- [ -n "${{ secrets.ANDROID_KEY_ALIAS }}" ] || { echo "❌ Missing ANDROID_KEY_ALIAS"; exit 1; }
124- [ -n "${{ secrets.ANDROID_KEY_PASSWORD }}" ] || { echo "❌ Missing ANDROID_KEY_PASSWORD"; exit 1; }
125-
126- echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > keystore.jks
127- KEYSTORE_PATH="$(realpath keystore.jks)"
128-
129- if [ ! -f "$KEYSTORE_PATH" ] || [ ! -s "$KEYSTORE_PATH" ]; then
130- echo "❌ Keystore file is empty or invalid"
131- ls -lh keystore.jks || true
116+ # 检查 Secrets 是否存在
117+ if [ -z "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" ]; then
118+ echo "❌ Error: ANDROID_KEYSTORE_BASE64 secret is missing."
132119 exit 1
133120 fi
134121
135- KEYSTORE_SIZE=$(stat -c%s "$KEYSTORE_PATH" 2>/dev/null || stat -f%z "$KEYSTORE_PATH" 2>/dev/null || echo "unknown")
136- echo "✅ Keystore size: ${KEYSTORE_SIZE} bytes"
122+ # 解码 Keystore
123+ echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > keystore.jks
124+ KEYSTORE_PATH="$(realpath keystore.jks)"
137125
126+ # 创建 key.properties 供 Gradle 读取
138127 cat > linkweb_build/android/key.properties << EOF
139128 storePassword=${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
140129 keyPassword=${{ secrets.ANDROID_KEY_PASSWORD }}
141130 keyAlias=${{ secrets.ANDROID_KEY_ALIAS }}
142131 storeFile=$KEYSTORE_PATH
143132 EOF
144133
145- echo "✅ Keystore prepared at: $KEYSTORE_PATH"
146- echo "🔑 Key alias: ${{ secrets.ANDROID_KEY_ALIAS }}"
134+ echo "✅ Keystore prepared."
147135
148- - name : Configure Gradle for signing
136+ - name : Configure Gradle for signing (Fixed for Kotlin DSL)
149137 run : |
150138 set -euo pipefail
151139
152- # 查找 build.gradle 文件
153140 GROOVY="linkweb_build/android/app/build.gradle"
154141 KTS="linkweb_build/android/app/build.gradle.kts"
155142
156- if [ -f "$GROOVY" ]; then
157- BUILD_FILE="$GROOVY"
158- echo "✅ Using build.gradle"
159- elif [ -f "$KTS" ]; then
160- BUILD_FILE="$KTS"
161- echo "✅ Using build.gradle.kts"
162- else
163- echo "❌ No build.gradle file found!"
164- echo "Searching for gradle files..."
165- find linkweb_build/android -name "build.gradle*" -type f
166- exit 1
167- fi
143+ # === 分支 1: 处理 Kotlin DSL (新版 Flutter 默认) ===
144+ if [ -f "$KTS" ]; then
145+ echo "✅ Detected Kotlin DSL (build.gradle.kts)"
146+
147+ # 1. 在文件头部添加 Keystore 加载逻辑
148+ cat > /tmp/header.txt << 'EOF'
149+ import java.io.FileInputStream
150+ import java.util.Properties
151+
152+ val keystoreProperties = Properties()
153+ val keystorePropertiesFile = rootProject.file("key.properties")
154+ if (keystorePropertiesFile.exists()) {
155+ keystoreProperties.load(FileInputStream(keystorePropertiesFile))
156+ }
157+
158+ EOF
159+ cat "$KTS" >> /tmp/header.txt
160+ mv /tmp/header.txt "$KTS"
161+
162+ # 2. 使用 Python 注入 signingConfigs
163+ python3 << 'PYTHON_SCRIPT'
164+ import re
168165
169- # 检查是否已配置签名
170- if grep -q "signingConfigs" "$BUILD_FILE"; then
171- echo "⚠️ Signing config already exists, skipping..."
172- exit 0
173- fi
166+ gradle_file = 'linkweb_build/android/app/build.gradle.kts'
174167
175- # 为 build.gradle 添加签名配置
176- if [ -f "$GROOVY" ]; then
177- # 创建备份
178- cp "$GROOVY" "${GROOVY}.backup"
168+ with open(gradle_file, 'r') as f:
169+ content = f.read()
170+
171+ # Kotlin 格式的签名配置
172+ signing_config_block = '''
173+ signingConfigs {
174+ create("release") {
175+ keyAlias = keystoreProperties["keyAlias"] as String
176+ keyPassword = keystoreProperties["keyPassword"] as String
177+ storeFile = file(keystoreProperties["storeFile"] as String)
178+ storePassword = keystoreProperties["storePassword"] as String
179+ }
180+ }
181+ '''
182+
183+ # 插入 signingConfigs (在 buildTypes 之前)
184+ if "signingConfigs {" not in content:
185+ content = re.sub(
186+ r'(\s+buildTypes\s*\{)',
187+ signing_config_block + r'\1',
188+ content
189+ )
190+
191+ # 应用签名配置到 release 构建类型
192+ # 查找 buildTypes { release { ... } } 结构并注入
193+ content = re.sub(
194+ r'(buildTypes\s*\{[\s\S]*?release\s*\{)',
195+ r'\1\n signingConfig = signingConfigs.getByName("release")',
196+ content
197+ )
198+
199+ with open(gradle_file, 'w') as f:
200+ f.write(content)
201+ print("✅ Injected signing config into build.gradle.kts")
202+ PYTHON_SCRIPT
203+
204+ # === 分支 2: 处理 Groovy DSL (旧版 Flutter) ===
205+ elif [ -f "$GROOVY" ]; then
206+ echo "✅ Detected Groovy DSL (build.gradle)"
179207
180- # 在文件开头添加 keystore 属性加载代码
208+ # 加载 Properties
181209 cat > /tmp/prepend.txt << 'GRADLE_HEADER'
182210 def keystoreProperties = new Properties()
183211 def keystorePropertiesFile = rootProject.file("key.properties")
184212 if (keystorePropertiesFile.exists()) {
185213 keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
186214 }
187-
188215 GRADLE_HEADER
189216
190- # 合并文件
191217 cat /tmp/prepend.txt "$GROOVY" > /tmp/build.gradle.new
192218 mv /tmp/build.gradle.new "$GROOVY"
193219
194- # 在 android { 后添加 signingConfigs
220+ # 注入配置
195221 python3 << 'PYTHON_SCRIPT'
196222 import re
197-
198223 gradle_file = 'linkweb_build/android/app/build.gradle'
199-
200224 with open(gradle_file, 'r') as f:
201225 content = f.read()
202226
203- # 在 android { 块内添加 signingConfigs
204227 signing_config = '''
205228 signingConfigs {
206229 release {
@@ -211,31 +234,16 @@ jobs:
211234 }
212235 }
213236 '''
214-
215- # 在 buildTypes 之前插入 signingConfigs
216- content = re.sub(
217- r'(\s+buildTypes\s*\{)',
218- signing_config + r'\1',
219- content
220- )
221-
222- # 在 release buildType 中添加 signingConfig
223- content = re.sub(
224- r'(release\s*\{)',
225- r'\1\n signingConfig signingConfigs.release',
226- content
227- )
228-
237+ content = re.sub(r'(\s+buildTypes\s*\{)', signing_config + r'\1', content)
238+ content = re.sub(r'(release\s*\{)', r'\1\n signingConfig signingConfigs.release', content)
229239 with open(gradle_file, 'w') as f:
230240 f.write(content)
231-
232- print("✅ Gradle signing configuration added")
233241 PYTHON_SCRIPT
234242
243+ else
244+ echo "❌ No Gradle file found!"
245+ exit 1
235246 fi
236-
237- echo "📄 Checking signing config:"
238- grep -A 8 "signingConfigs" "$BUILD_FILE" || echo "⚠️ Could not verify signingConfigs"
239247
240248 - name : Get dependencies
241249 working-directory : linkweb_build
@@ -253,13 +261,13 @@ jobs:
253261 - name : Verify APK signature
254262 run : |
255263 echo "🔍 Verifying APK signatures..."
256- for apk in linkweb_build/build/app/outputs/flutter-apk/app-*-release.apk; do
257- if [ -f "$ apk" ]; then
258- echo "Checking: $(basename "$apk")"
259- jarsigner -verify -verbose -certs "$apk" 2>&1 | head -20 || true
260- echo "---"
261- fi
262- done
264+ # 只要验证一个就能确认签名是否生效
265+ APK_FILE=$(find linkweb_build/build/app/outputs/flutter-apk/ -name "app-*-release. apk" | head -n 1)
266+ if [ -f "$APK_FILE" ]; then
267+ echo "Checking: $APK_FILE"
268+ # 检查是否包含你的 Alias 名称 (grep 搜索)
269+ jarsigner -verify -verbose -certs "$APK_FILE" | grep "X.509" -A 2 || true
270+ fi
263271
264272 - name : Rename and collect APKs
265273 run : |
@@ -268,16 +276,14 @@ jobs:
268276
269277 VERSION="${{ github.event.inputs.version_name }}"
270278 APP_NAME="${{ github.event.inputs.app_name }}"
271- # 移除特殊字符,保留字母数字、下划线和连字符
279+ # 清理文件名
272280 SAFE_APP_NAME=$(echo "$APP_NAME" | sed 's/[^[:alnum:]_-]//g')
273281 [ -z "$SAFE_APP_NAME" ] && SAFE_APP_NAME="LinkWeb"
274282
275- echo "📦 Collecting APKs with name: ${SAFE_APP_NAME} "
283+ echo "📦 Collecting APKs... "
276284
277285 for apk in linkweb_build/build/app/outputs/flutter-apk/app-*-release.apk; do
278- if [ ! -f "$apk" ]; then
279- continue
280- fi
286+ [ -f "$apk" ] || continue
281287
282288 basename=$(basename "$apk")
283289 if [[ $basename == *"arm64-v8a"* ]]; then
@@ -291,37 +297,30 @@ jobs:
291297 fi
292298 done
293299
294- echo "✅ APKs ready:"
295300 ls -lh dist/
296301
297302 - name : Upload APKs as artifact
298303 uses : actions/upload-artifact@v4
299304 with :
300305 name : APKs-${{ github.run_number }}-v${{ github.event.inputs.version_name }}
301306 path : dist/*.apk
302- if-no-files-found : error
303307
304308 - name : Create GitHub Release
305309 if : github.event.inputs.upload_to_release == 'true'
306310 env :
307311 GH_TOKEN : ${{ github.token }}
308312 run : |
309313 set -euo pipefail
310-
311314 TAG="v${{ github.event.inputs.version_name }}"
312315
316+ # 检查 Tag 是否存在
313317 if gh release view "$TAG" >/dev/null 2>&1; then
314- echo "Release $TAG already exists, updating..."
315- gh release edit "$TAG" \
316- --title "$TAG - ${{ github.event.inputs.app_name }}" \
317- --notes "${{ github.event.inputs.release_notes }}"
318+ echo "Updating existing release $TAG"
319+ gh release upload "$TAG" dist/*.apk --clobber
318320 else
319321 echo "Creating new release $TAG"
320322 gh release create "$TAG" \
321323 --title "$TAG - ${{ github.event.inputs.app_name }}" \
322- --notes "${{ github.event.inputs.release_notes }}"
324+ --notes "${{ github.event.inputs.release_notes }}" \
325+ dist/*.apk
323326 fi
324-
325- gh release upload "$TAG" dist/*.apk --clobber
326-
327- echo "✅ Release created/updated: https://github.com/${{ github.repository }}/releases/tag/${TAG}"
0 commit comments