Expo 项目在 GitHub Actions 上打包 Android Release
最近把 Expo 项目的 Android Release 打包流程迁移到了 GitHub Actions 上。原本以为只是写个 workflow 的事,结果一路踩了不少坑。这里把整个过程里遇到的问题和解决方案整理出来,供有类似需求的同学参考。
背景与目标
项目是一个使用 Expo 管理的 React Native 应用,采用 monorepo 结构(pnpm workspace)。希望实现:
- 推送 tag 触发 release 时,自动构建并上传 APK
- 支持手动触发 workflow_dispatch,方便日常测试
- 构建产物既能通过 GitHub Release 下载,也能在 Actions 页面直接拿 Artifacts
Node.js 版本警告
一开始用的是 actions/setup-node@v3,没多久 GitHub Actions 开始发通知:Node 20 即将被弃用,建议迁移到 Node 22 或 24。
当时第一反应是去改 setup-node 的 node-version:
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: "20"
但这里有个容易混淆的点:actions/setup-node 里的 node-version 只影响 workflow 里后续执行的 node / pnpm 命令(比如 pnpm install、Expo CLI 等),不影响 action 自身的运行时。
真正导致 warning 的原因是:android-actions/setup-android@v3 这个 action 自身内部还基于 Node 20 运行。这是 action 作者发布时绑定的运行时,不会因为你在 setup-node 里改成 22 或 24 就自动变掉。
解决方案有两层:
- 短期兼容:添加环境变量
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true,强制 GitHub 上的 JavaScript actions 自身用 Node 24 跑 - 长期修复:升级 action 到支持 Node 24 的版本
android-actions/setup-android 版本升级
顺着上面的方向查下去,发现 android-actions/setup-android 其实已经有 v4.0.1 了,而我一直停留在 v3。
升级很简单,直接改版本号:
- name: Setup Android SDK
uses: android-actions/setup-android@v4
升级到 v4 后,Node 版本的 warning 就彻底消失了,FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true 这个兼容开关也可以去掉了。
APK 架构与体积优化
第一次构建出来的 APK 只有 71MB,感觉还不错。但仔细看配置,发现 gradle.properties 里写的是:
reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
这意味着构建时会把 armeabi-v7a、arm64-v8a、x86、x86_64 全部打包进去。其中 x86 / x86_64 主要是给模拟器用的,真机发行包其实不需要它们。
如果你的目标是「发给真机安装,尽量小」,通常建议改成:
reactNativeArchitectures=arm64-v8a
如果还想兼容少量老 32 位 Android 真机,可以用:
reactNativeArchitectures=armeabi-v7a,arm64-v8a
改完之后 APK 体积会明显下降,因为不再浪费空间在模拟器 ABI 上。
另外,还可以在 gradle.properties 里开启混淆和资源收缩,进一步减小体积:
android.enableMinifyInReleaseBuilds=true
android.enableShrinkResourcesInReleaseBuilds=true
以及,如果项目里没用到 GIF 或 WebP,可以把 app.json 里对应的解码支持关掉,也能省一点点体积:
{
"expo": {
"gif": { "enabled": false },
"webp": { "enabled": false }
}
}
GitHub Release 权限问题
在配置 softprops/action-gh-release@v2 上传 APK 到 Release 时,遇到了这样一个错误:
⚠️ Unexpected error fetching GitHub release for tag refs/tags/v0.1.0-alpha:
HttpError: Resource not accessible by integration
原因是:action-gh-release 需要 contents: write 权限来更新 Release 并上传附件,但 workflow 默认拿到的 GITHUB_TOKEN 权限是只读的。
解决方法是在 workflow 文件顶部添加权限声明:
permissions:
contents: write
加完之后,Release 的更新和附件上传就能正常进行了。
非 Release 触发时的产物获取
除了通过 Release 触发构建,日常测试中更常用的是手动触发 workflow_dispatch。这种情况下,构建产物不会附加到 GitHub Release,但可以通过 Artifacts 下载。
在 workflow 里加上这一步:
- name: Upload APK artifact
uses: actions/upload-artifact@v7
with:
name: app-release-apk
path: apps/mobile/android/app/build/outputs/apk/release/app-release.apk
if-no-files-found: error
构建成功后,在 GitHub 仓库页面 → Actions → 点进对应的 workflow run → 页面底部或右侧 Artifacts 里就能看到并下载 app-release-apk。
最终的 Workflow 文件
经过上面这些调整,最终的 android-release-build.yml 是这样的:
name: android-release-build
on:
workflow_dispatch:
release:
types: [published]
permissions:
contents: write
jobs:
build-release-apk:
runs-on: ubuntu-latest
env:
EXPO_NO_METRO_WORKSPACE_ROOT: "1"
defaults:
run:
working-directory: apps/mobile/android
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"
cache: gradle
- name: Setup Android SDK
uses: android-actions/setup-android@v4
- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
version: 10.17.1
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: "20"
cache: "pnpm"
cache-dependency-path: pnpm-lock.yaml
- name: Install JS dependencies
run: pnpm install --frozen-lockfile
working-directory: ${{ github.workspace }}
- name: Grant Gradle wrapper permission
run: chmod +x gradlew
- name: Build release APK
run: EXPO_NO_METRO_WORKSPACE_ROOT=1 ./gradlew assembleRelease --no-daemon
- name: Upload APK artifact
uses: actions/upload-artifact@v7
with:
name: app-release-apk
path: apps/mobile/android/app/build/outputs/apk/release/app-release.apk
if-no-files-found: error
- name: Upload APK to GitHub Release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v2
with:
files: apps/mobile/android/app/build/outputs/apk/release/app-release.apk
几个值得注意的点:
EXPO_NO_METRO_WORKSPACE_ROOT=1是 Expo 在 monorepo 环境下需要的变量,避免 Metro 找不到正确的根目录working-directory设为apps/mobile/android,因为 Gradle 构建是在 Android 原生目录下执行的cache-dependency-path指向根目录的pnpm-lock.yaml,确保 pnpm 的缓存能正确命中if-no-files-found: error保证如果 APK 没构建出来,workflow 会直接失败,而不是静默跳过