Build APK / build (push) Failing after 38s
Probe(run 343)实测结论: - job 跑在 Docker 容器内,容器里没有 java,也没有 /opt/jdk17、/opt/android-sdk、/opt/gradle - 127.0.0.1 不可达(容器内 127 指向自身);172.17.0.1(Docker 网关)17.6 MB/s;公网 370 KB/s 改动: - 素材地址顺序改为 172.17.0.1 → 127.0.0.1 → server_url(按容器/host 两种模式的实际可达性排) - 构建改用探测到的系统 gradle:本项目没有 gradlew(build.sh 用的是 /opt/gradle/gradle-8.5) - 找不到 SDK 时的报错补上「容器看不到宿主机 /opt」这一层,并给出改 host 模式的具体命令
235 lines
12 KiB
YAML
235 lines
12 KiB
YAML
name: Build APK
|
||
|
||
# 触发方式:
|
||
# · push tag(v*) → 构建并发布到 Gitea Release(App 内更新机制读的就是这里)
|
||
# · workflow_dispatch → 手动构建,只上传 artifact,不发布
|
||
#
|
||
# 关键设计:**代码取当前 ref,资源目录从 full-assets 分支单独检出**
|
||
# main 分支为保持 ~8MB 不追踪 assets_hq / assets_small / assets_net(见 README「分支约定」),
|
||
# 而这三个目录是宠物动画与表情包本体,缺了它们构建出来的包宠物是空白的。
|
||
# 所以构建时把 full-assets 的这三个目录叠加到工作树,而不是让代码跟着 650MB 分支走。
|
||
|
||
on:
|
||
workflow_dispatch:
|
||
inputs:
|
||
variant:
|
||
description: 'asset 变体(hq=640x360 原画质 / small=320x180 小体积)'
|
||
required: false
|
||
default: 'hq'
|
||
push:
|
||
tags:
|
||
- 'v*'
|
||
|
||
jobs:
|
||
build:
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: 检出代码
|
||
run: |
|
||
git clone --depth 1 --branch "${{ github.ref_name }}" \
|
||
"${{ github.server_url }}/${{ github.repository }}.git" .
|
||
git log --oneline -1
|
||
|
||
- name: 叠加完整素材(full-assets 分支)
|
||
run: |
|
||
set -e
|
||
# 地址选择(实测数据,见 Probe 探测):
|
||
# 172.17.0.1:3000(Docker 网关 → 宿主机) 17.6 MB/s ← 容器模式下的正确地址
|
||
# 127.0.0.1:3000 不可达(容器内 127 是容器自己)
|
||
# 123.206.124.151:3000 370 KB/s(走公网绕行,慢 47 倍)
|
||
# 顺序按"容器模式优先、host 模式兜底"排:先试网关,再试回环,最后才回公网。
|
||
GH_REPO="${{ github.repository }}"
|
||
GIT_URL=""
|
||
for cand in "http://172.17.0.1:3000" "http://127.0.0.1:3000" "${{ github.server_url }}"; do
|
||
if timeout 8 curl -sf -o /dev/null "$cand/api/v1/version" 2>/dev/null; then
|
||
GIT_URL="$cand/$GH_REPO.git"
|
||
echo "选用 Gitea 地址:$cand"
|
||
break
|
||
fi
|
||
done
|
||
[ -n "$GIT_URL" ] || { echo "::error::所有 Gitea 地址都不可达"; exit 1; }
|
||
|
||
# 优先用宿主机上的素材缓存:构建机本来就有完整仓库,没必要每次重下 650MB。
|
||
# 预置方式:把 assets_hq / assets_small / assets_net 放到 /opt/petfloat-assets/ 下。
|
||
ASSET_CACHE="${PET_ASSET_CACHE:-/opt/petfloat-assets}"
|
||
if [ -d "$ASSET_CACHE" ] && [ -d "$ASSET_CACHE/assets_hq" ]; then
|
||
echo "使用本地素材缓存:$ASSET_CACHE"
|
||
for d in assets_hq assets_small assets_net; do
|
||
rm -rf "app/src/main/$d"
|
||
if [ -d "$ASSET_CACHE/$d" ]; then
|
||
cp -r "$ASSET_CACHE/$d" "app/src/main/$d"
|
||
echo "✓ $d (缓存) $(find "app/src/main/$d" -type f | wc -l) 个文件 $(du -sh "app/src/main/$d" | cut -f1)"
|
||
else
|
||
echo "::error::$ASSET_CACHE 缺少 $d"; exit 1
|
||
fi
|
||
done
|
||
else
|
||
echo "无本地缓存($ASSET_CACHE 不存在),改从 full-assets 分支取(超时保护 900s)"
|
||
git remote add fa "$GIT_URL" 2>/dev/null || true
|
||
if ! timeout 900 git fetch --depth 1 fa full-assets; then
|
||
echo "::error::拉取 full-assets 超时或失败。"
|
||
echo " 650MB 素材经公网下载极慢,建议在 runner 机器上预置缓存:"
|
||
echo " mkdir -p /opt/petfloat-assets && cp -r <仓库>/app/src/main/assets_{hq,small,net} /opt/petfloat-assets/"
|
||
exit 1
|
||
fi
|
||
for d in assets_hq assets_small assets_net; do
|
||
if git ls-tree -d --name-only FETCH_HEAD "app/src/main/$d" | grep -q .; then
|
||
rm -rf "app/src/main/$d"
|
||
git checkout FETCH_HEAD -- "app/src/main/$d"
|
||
echo "✓ $d $(find "app/src/main/$d" -type f | wc -l) 个文件 $(du -sh "app/src/main/$d" | cut -f1)"
|
||
else
|
||
echo "::error::full-assets 分支缺少 app/src/main/$d"
|
||
exit 1
|
||
fi
|
||
done
|
||
fi
|
||
|
||
- name: 准备构建环境
|
||
run: |
|
||
set -e
|
||
# JDK 17
|
||
if ! java -version 2>&1 | grep -qE '"(17|2[1-9])'; then
|
||
for c in /opt/jdk17 /usr/lib/jvm/java-17-openjdk-amd64 /usr/lib/jvm/java-21-openjdk-amd64; do
|
||
[ -x "$c/bin/java" ] && export JAVA_HOME="$c" && break
|
||
done
|
||
fi
|
||
[ -n "${JAVA_HOME:-}" ] && echo "JAVA_HOME=$JAVA_HOME" >> "$GITHUB_ENV"
|
||
java -version 2>&1 | head -2
|
||
# Android SDK
|
||
if [ -z "${ANDROID_HOME:-}" ] || [ ! -d "${ANDROID_HOME:-}" ]; then
|
||
for p in /opt/android-sdk /usr/local/android-sdk "$HOME/Android/Sdk" /root/android-sdk; do
|
||
[ -d "$p" ] && export ANDROID_HOME="$p" && break
|
||
done
|
||
fi
|
||
if [ -z "${ANDROID_HOME:-}" ] || [ ! -d "$ANDROID_HOME" ]; then
|
||
echo "::error::找不到 Android SDK(ANDROID_HOME 未设置且常见路径均不存在)"
|
||
echo " 环境线索:java = $(java -version 2>&1 | head -1 || echo '未安装')"
|
||
if [ -f /.dockerenv ]; then
|
||
echo " ⚠ 本 job 运行在 Docker 容器内 —— 容器**看不到**宿主机的 /opt/jdk17、/opt/android-sdk。"
|
||
echo " (Probe 实测:容器内 java 不存在、/opt/* 全空、127.0.0.1 不可达、172.17.0.1 达 17.6MB/s)"
|
||
fi
|
||
echo " 两条出路:"
|
||
echo " 1)(推荐,最快)把 runner 改成 host 模式,直接复用宿主机现成环境:"
|
||
echo " sed -i 's|\"ubuntu-latest:docker://[^\"]*\"|\"ubuntu-latest:host\"|' /data/.runner"
|
||
echo " 然后重启 runner 容器。host 模式下 /opt/jdk17 与 /opt/android-sdk 直接可用。"
|
||
echo " 2) 给 runner 换一个自带 JDK17 + Android SDK 的镜像(约 +3GB)。"
|
||
exit 1
|
||
fi
|
||
echo "ANDROID_HOME=$ANDROID_HOME" >> "$GITHUB_ENV"
|
||
echo "ANDROID_SDK_ROOT=$ANDROID_HOME" >> "$GITHUB_ENV"
|
||
echo "SDK: $ANDROID_HOME"
|
||
ls "$ANDROID_HOME/platforms" 2>/dev/null | head -3
|
||
|
||
- name: 准备签名密钥
|
||
env:
|
||
KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
|
||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||
run: |
|
||
set -e
|
||
if [ -z "${KEYSTORE_BASE64:-}" ]; then
|
||
echo "::warning::未配置 KEYSTORE_BASE64,本次用 debug 签名构建(仅验证链路,不可分发)。"
|
||
echo " ⚠ debug 签名每台机器不同,手机**无法覆盖安装**,更新机制会失效。"
|
||
echo " 按 README「发布与更新」配好 secrets 后重跑,即可得到正式签名包。"
|
||
exit 0
|
||
fi
|
||
printf '%s' "$KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/release.jks"
|
||
echo "KEYSTORE_PATH=$RUNNER_TEMP/release.jks" >> "$GITHUB_ENV"
|
||
echo "KEY_ALIAS=${{ secrets.KEY_ALIAS || 'petfloat' }}" >> "$GITHUB_ENV"
|
||
|
||
- name: 构建 Release APK
|
||
env:
|
||
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
|
||
VERSION_CODE: ${{ github.run_number }}
|
||
VARIANT: ${{ inputs.variant || 'hq' }}
|
||
run: |
|
||
set -e
|
||
# versionCode 需严格递增:run_number 天然单调,但设备上已装的是 1,
|
||
# 这里加基准 1000 避免与历史调试包冲突,也留出回退空间。
|
||
export VERSION_CODE=$((1000 + VERSION_CODE))
|
||
export VERSION_NAME="1.0.${GITHUB_RUN_NUMBER}"
|
||
echo "构建版本:versionCode=$VERSION_CODE versionName=$VERSION_NAME variant=$VARIANT"
|
||
|
||
ARGS=""
|
||
[ "$VARIANT" = "hq" ] && ARGS="-Phq"
|
||
|
||
# 本项目**不带 gradlew**(build.sh 走的是系统 gradle),所以按优先级探测可执行文件
|
||
GRADLE_BIN=""
|
||
for c in /opt/gradle/gradle-8.5/bin/gradle /opt/gradle/bin/gradle ./gradlew; do
|
||
[ -x "$c" ] && GRADLE_BIN="$c" && break
|
||
done
|
||
if [ -z "$GRADLE_BIN" ]; then
|
||
GRADLE_BIN=$(command -v gradle 2>/dev/null || true)
|
||
fi
|
||
[ -n "$GRADLE_BIN" ] || {
|
||
echo "::error::找不到 gradle —— 项目无 gradlew,必须用系统 gradle"
|
||
echo " 期望路径:/opt/gradle/gradle-8.5/bin/gradle(build.sh 里用的就是它)"
|
||
exit 1
|
||
}
|
||
echo "gradle: $GRADLE_BIN"
|
||
|
||
"$GRADLE_BIN" :app:assembleRelease $ARGS --no-daemon --stacktrace
|
||
|
||
APK=app/build/outputs/apk/release/app-release.apk
|
||
[ -f "$APK" ] || { echo "::error::构建产物不存在:$APK"; exit 1; }
|
||
|
||
# 产物登记:体积 + 指纹 + 签名摘要(更新机制靠 versionCode,人靠 SHA 核对)
|
||
APKSHA=$(sha256sum "$APK" | cut -d' ' -f1)
|
||
SIZE=$(stat -c%s "$APK")
|
||
echo "APK_SHA=$APKSHA" >> "$GITHUB_ENV"
|
||
echo "APK_SIZE=$SIZE" >> "$GITHUB_ENV"
|
||
echo "APK_NAME=petfloat-${VERSION_NAME}-${VARIANT}.apk" >> "$GITHUB_ENV"
|
||
cp "$APK" "$RUNNER_TEMP/petfloat-${VERSION_NAME}-${VARIANT}.apk"
|
||
|
||
# version.json:App 端更新检查优先读它取 versionCode / sha256 / 体积,
|
||
# 比从 tag 名解析可靠(tag 是人写的,这个文件由构建产物自己写)。
|
||
printf '{"versionCode":%s,"versionName":"%s","apk":"%s","sha256":"%s","size":%s}\n' \
|
||
"$VERSION_CODE" "$VERSION_NAME" "$APK_NAME" "$APK_SHA" "$SIZE" \
|
||
> "$RUNNER_TEMP/version.json"
|
||
cat "$RUNNER_TEMP/version.json"
|
||
|
||
echo "APK: $APK_SHA ${SIZE} bytes"
|
||
|
||
# 签名核验:确保用的是发布密钥而不是 debug 密钥(debug 密钥每台机器不同,装不上)
|
||
APKSIGNER=$(find "$ANDROID_HOME"/build-tools -name apksigner 2>/dev/null | sort -V | tail -1)
|
||
if [ -n "$APKSIGNER" ]; then
|
||
"$APKSIGNER" verify --print-certs "$APK" | head -3
|
||
fi
|
||
|
||
- name: 上传构建产物
|
||
run: echo "$RUNNER_TEMP/$APK_NAME"
|
||
|
||
- name: 发布到 Gitea Release
|
||
if: startsWith(github.ref, 'refs/tags/')
|
||
env:
|
||
TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||
run: |
|
||
set -e
|
||
API="${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
|
||
TAG="${{ github.ref_name }}"
|
||
|
||
# 幂等:tag 已存在 release 就复用它,避免重跑流水线时 409
|
||
RID=$(curl -s -H "Authorization: token $TOKEN" "$API/releases/tags/$TAG" \
|
||
| python3 -c "import sys,json;print(json.load(sys.stdin).get('id',''))" 2>/dev/null || true)
|
||
if [ -z "$RID" ]; then
|
||
RID=$(curl -s -X POST -H "Authorization: token $TOKEN" -H 'Content-Type: application/json' \
|
||
-d "{\"tag_name\":\"$TAG\",\"name\":\"PetFloat $TAG\",\"body\":\"versionCode=$VERSION_CODE\\nSHA256=$APK_SHA\",\"target_commitish\":\"main\"}" \
|
||
"$API/releases" \
|
||
| python3 -c "import sys,json;print(json.load(sys.stdin).get('id',''))")
|
||
fi
|
||
echo "release id=$RID"
|
||
|
||
# 先删同名附件(重跑时避免重名失败),再上传
|
||
for aid in $(curl -s -H "Authorization: token $TOKEN" "$API/releases/$RID/assets" \
|
||
| python3 -c "import sys,json;[print(a['id']) for a in json.load(sys.stdin)]" 2>/dev/null); do
|
||
curl -s -X DELETE -H "Authorization: token $TOKEN" "$API/releases/$RID/assets/$aid" || true
|
||
done
|
||
# 上传 APK 与 version.json(App 端靠后者判版本与校验下载完整性)
|
||
for f in "$RUNNER_TEMP/$APK_NAME" "$RUNNER_TEMP/version.json"; do
|
||
n=$(basename "$f")
|
||
curl -s -X POST -H "Authorization: token $TOKEN" \
|
||
-F "attachment=@$f" \
|
||
"$API/releases/$RID/assets?name=$n" | head -c 200
|
||
echo " ← $n"
|
||
done
|
||
echo "已发布:${{ github.server_url }}/${{ github.repository }}/releases/tag/$TAG"
|