init: Wardrobe 衣物管理系统 - 项目首页、OTA 升级、Android CI/CD
- 响应式项目首页 (Landing, 含 R2 APK 下载) + 路由 /home 调整 - OTA 升级: tauri-plugin-hotswap (lib.rs/main.rs 拆分, 客户端热替换) - OTA 服务端 (check/bundle/manifest-web) + build-ota/genkey/upload 脚本 - 密钥/凭据统一存配置中心 (signing + ota + cloudflare 分组) - Android CI (android-* 分支) + OTA CI (手动触发), 产物上传 R2 - 修复 tsc/构建阻塞问题
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
name: Android Build & Publish
|
||||
|
||||
# 仅在 android-* 分支 push 时触发 (如 android/release, android-1.0)
|
||||
# 构建 release APK (keystore 签名) 并上传到 Cloudflare R2 (S3 兼容 API)
|
||||
# R2 凭据从配置注册中心 (bh.vps.honor3.com:8008, CommonExternalService, 分组 cloudflare) 读取
|
||||
# 参考: D:\workbench\iboard\.gitea\workflows\android-build.yml
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'android-*'
|
||||
|
||||
jobs:
|
||||
build-android:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
# 固定工具链版本, 便于 actions/cache 命中
|
||||
NDK_VERSION: '27.2.12479018'
|
||||
# 配置中心地址与 token (Gitea secrets; URL 未设时回退默认地址)
|
||||
CONFIG_CENTER_URL: '${{ secrets.CONFIG_CENTER_URL }}'
|
||||
CONFIG_CENTER_TOKEN: '${{ secrets.CONFIG_CENTER_TOKEN }}'
|
||||
REGISTRY_URL_FALLBACK: 'http://bh.vps.honor3.com:8008'
|
||||
# 配置中心项目 ID: 签名密钥在 service-wardrobe/signing, R2 在 CommonExternalService/cloudflare
|
||||
CFG_PROJECT_SIGNING: 'proj-mtwjnvir-k52zvp'
|
||||
CFG_PROJECT_R2: 'proj-mts0s5jo-fb3rz7'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://gitea.com/actions/checkout@v4
|
||||
|
||||
- name: Setup mise
|
||||
uses: https://gitea.com/actions/mise@v2
|
||||
with:
|
||||
tools: "node:24.21.0,rust:stable"
|
||||
cache: true
|
||||
|
||||
- name: Setup JDK 17
|
||||
uses: https://gitea.com/actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: '17'
|
||||
|
||||
# ---------- 尽量复用 Android SDK ----------
|
||||
# 优先复用 runner 预装的 SDK (/usr/local/lib/android/sdk), 其次命中 CI 缓存,
|
||||
# 最后才重新下载。NDK 不在预装中, 单独用缓存复用。
|
||||
- name: Locate reusable Android SDK
|
||||
id: sdk
|
||||
run: |
|
||||
CANDIDATES=(
|
||||
"$ANDROID_HOME"
|
||||
"/usr/local/lib/android/sdk" # GitHub ubuntu-latest 预装
|
||||
"$HOME/android-sdk"
|
||||
)
|
||||
for c in "${CANDIDATES[@]}"; do
|
||||
if [ -n "$c" ] && [ -d "$c/platforms" ]; then
|
||||
echo "ANDROID_ROOT=${c}" >> "$GITHUB_ENV"
|
||||
echo "found=${c}" >> "$GITHUB_OUTPUT"
|
||||
echo "reusing existing Android SDK: ${c}"
|
||||
break
|
||||
fi
|
||||
done
|
||||
echo "found=${found:-}"
|
||||
|
||||
- name: Cache Android NDK
|
||||
id: cache-ndk
|
||||
uses: https://gitea.com/actions/cache@v4
|
||||
with:
|
||||
path: ~/android-sdk/ndk
|
||||
key: android-ndk-${{ env.NDK_VERSION }}-${{ runner.os }}
|
||||
|
||||
- name: Cache Rust toolchain
|
||||
id: cache-rust
|
||||
uses: https://gitea.com/actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.mise
|
||||
~/.local/share/mise
|
||||
key: mise-rust-android-v1-${{ runner.os }}
|
||||
|
||||
- name: Cache Tauri cargo target
|
||||
uses: https://gitea.com/actions/cache@v4
|
||||
with:
|
||||
path: src-tauri/target
|
||||
key: tauri-target-android-${{ runner.os }}
|
||||
|
||||
# ---------- 安装缺失组件 (仅未复用/未命中时) ----------
|
||||
- name: Ensure Android NDK
|
||||
if: steps.sdk.outputs.found == '' || steps.cache-ndk.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
if [ -z "${ANDROID_ROOT:-}" ]; then
|
||||
export ANDROID_HOME="$HOME/android-sdk"
|
||||
mkdir -p "$ANDROID_HOME/cmdline-tools"
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends unzip
|
||||
wget -q https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip -O /tmp/cmdline-tools.zip
|
||||
unzip -q /tmp/cmdline-tools.zip -d /tmp/
|
||||
mv /tmp/cmdline-tools "$ANDROID_HOME/cmdline-tools/latest"
|
||||
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
|
||||
yes | sdkmanager --licenses >/dev/null || true
|
||||
sdkmanager --install \
|
||||
"platforms;android-36" \
|
||||
"build-tools;36.0.0" \
|
||||
"platform-tools"
|
||||
echo "ANDROID_HOME=$ANDROID_HOME" >> "$GITHUB_ENV"
|
||||
fi
|
||||
# NDK (cache miss 或首页 SDK 未含 NDK 时安装到 ~/android-sdk/ndk)
|
||||
if [ ! -d "$HOME/android-sdk/ndk/${{ env.NDK_VERSION }}" ]; then
|
||||
export ANDROID_HOME="${ANDROID_ROOT:-$HOME/android-sdk}"
|
||||
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$PATH"
|
||||
sdkmanager --install "ndk;${{ env.NDK_VERSION }}"
|
||||
fi
|
||||
|
||||
- name: Add Android Rust targets
|
||||
run: |
|
||||
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android i686-linux-android
|
||||
cargo install cargo-ndk --locked || true
|
||||
|
||||
- name: Install frontend deps
|
||||
run: npm ci --no-audit --no-fund
|
||||
|
||||
# ---------- 全局 Android 环境 ----------
|
||||
- name: Set Android env
|
||||
run: |
|
||||
echo "ANDROID_HOME=${ANDROID_ROOT:-$HOME/android-sdk}" >> "$GITHUB_ENV"
|
||||
echo "ANDROID_NDK_HOME=${ANDROID_ROOT:-$HOME/android-sdk}/ndk/${{ env.NDK_VERSION }}" >> "$GITHUB_ENV"
|
||||
echo "${ANDROID_ROOT:-$HOME/android-sdk}/cmdline-tools/latest/bin" >> "$GITHUB_PATH"
|
||||
echo "${ANDROID_ROOT:-$HOME/android-sdk}/platform-tools" >> "$GITHUB_PATH"
|
||||
|
||||
# ---------- 构建 ----------
|
||||
- name: Init Android project
|
||||
run: |
|
||||
cd src-tauri
|
||||
npx tauri android init --ci
|
||||
|
||||
- name: Fetch config center (signing + R2)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export CONFIG_CENTER_URL="${CONFIG_CENTER_URL:-$REGISTRY_URL_FALLBACK}"
|
||||
python3 - <<'PYEOF'
|
||||
import json, os, urllib.request
|
||||
url = os.environ["CONFIG_CENTER_URL"]
|
||||
token = os.environ.get("CONFIG_CENTER_TOKEN", "") or ""
|
||||
wanted_keys = {
|
||||
"CFG_PROJECT_SIGNING": ("ANDROID_KEYSTORE_B64", "ANDROID_KEY_ALIAS", "ANDROID_KEY_PASSWORD"),
|
||||
"CFG_PROJECT_R2": ("CF_API_TOKEN", "CF_R2_PUBLIC_URL"),
|
||||
}
|
||||
wanted = {}
|
||||
for pid_env, keys in wanted_keys.items():
|
||||
pid = os.environ[pid_env]
|
||||
req = urllib.request.Request(f"{url}/api/projects/{pid}/configs")
|
||||
if token:
|
||||
req.add_header("Authorization", "Bearer " + token)
|
||||
with urllib.request.urlopen(req, timeout=20) as r:
|
||||
data = json.load(r).get("data", [])
|
||||
for cfg in data:
|
||||
if cfg.get("key") in keys:
|
||||
wanted[cfg["key"]] = cfg.get("value", "")
|
||||
with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as f:
|
||||
for k, v in wanted.items():
|
||||
f.write(f"{k}={v}\n")
|
||||
missing = [k for k in ("ANDROID_KEYSTORE_B64", "ANDROID_KEY_ALIAS", "ANDROID_KEY_PASSWORD", "CF_API_TOKEN") if k not in wanted]
|
||||
if missing:
|
||||
raise SystemExit("配置中心缺少密钥: " + ", ".join(missing))
|
||||
PYEOF
|
||||
|
||||
- name: Inject release signing config
|
||||
run: bash scripts/ci/android-signing.sh
|
||||
|
||||
- name: Build release APK
|
||||
run: |
|
||||
export ANDROID_HOME="${ANDROID_ROOT:-$HOME/android-sdk}"
|
||||
export ANDROID_NDK_HOME="$ANDROID_HOME/ndk/${{ env.NDK_VERSION }}"
|
||||
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH"
|
||||
cd src-tauri
|
||||
npx tauri android build --apk
|
||||
# 断言签名配置仍在且 release 块实际引用了它 (防止 tauri 重新生成 gen/android 或正则未命中)
|
||||
if ! grep -q 'create("release")' gen/android/app/build.gradle.kts || ! grep -q 'signingConfig = signingConfigs.getByName("release")' gen/android/app/build.gradle.kts; then
|
||||
echo "ERROR: 构建后签名配置丢失/未生效, APK 未签名" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Locate APK
|
||||
id: apk
|
||||
run: |
|
||||
APK=""
|
||||
UNIVERSAL=$(find src-tauri/gen/android/app/build/outputs/apk/universal/release -name '*.apk' ! -name '*unsigned*' 2>/dev/null | head -1)
|
||||
if [ -n "$UNIVERSAL" ]; then
|
||||
APK="$UNIVERSAL"
|
||||
else
|
||||
APK=$(find src-tauri/gen/android/app/build/outputs/apk -name '*.apk' ! -name '*unsigned*' | head -1)
|
||||
fi
|
||||
if [ -z "$APK" ]; then
|
||||
echo "ERROR: 未找到已签名 APK" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "path=$APK" >> "$GITHUB_OUTPUT"
|
||||
ls -lh "$APK"
|
||||
|
||||
- name: Upload APK to Cloudflare R2
|
||||
env:
|
||||
APK_PATH: ${{ steps.apk.outputs.path }}
|
||||
run: |
|
||||
bash scripts/upload-r2.sh "$APK_PATH"
|
||||
@@ -0,0 +1,112 @@
|
||||
name: OTA Frontend Build & Sign
|
||||
|
||||
# 手动触发 (发版前用): 构建前端 → 打生产 dist 的 OTA 升级包 (minisign 签名) → 上传 R2
|
||||
# 产物:
|
||||
# - ota-package/wardrobe-ota.tar.gz (前端替换包)
|
||||
# - ota-package/latest.json (manifest, 客户端 check 用)
|
||||
# 存储: Cloudflare R2 store/ota/ (公开桶)
|
||||
# 依赖配置注册中心 (bh.vps.honor3.com:8008):
|
||||
# - service-wardrobe / ota : MINISIGN_PRIVATE_KEY (整段 .key 内容, 多行)
|
||||
# - CommonExternalService / cloudflare : CF_API_TOKEN, CF_R2_PUBLIC_URL
|
||||
# 注意: 不修改 src-tauri/tauri.conf.json 的 pubkey, 换密钥请手动改 conf 并 commit。
|
||||
# 部署 server (托管 OTA check 端点 + 首页 dist) 见 README "OTA 升级 与 首页" 章节。
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sequence:
|
||||
description: 'OTA sequence (单调递增整数, 留空用 timestamp)'
|
||||
required: false
|
||||
type: string
|
||||
notes:
|
||||
description: '发布说明 (写入 latest.json.notes)'
|
||||
required: false
|
||||
type: string
|
||||
default: ''
|
||||
|
||||
jobs:
|
||||
build-ota:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
CONFIG_CENTER_URL: '${{ secrets.CONFIG_CENTER_URL }}'
|
||||
CONFIG_CENTER_TOKEN: '${{ secrets.CONFIG_CENTER_TOKEN }}'
|
||||
REGISTRY_URL_FALLBACK: 'http://bh.vps.honor3.com:8008'
|
||||
CFG_PROJECT_SIGNING: 'proj-mtwjnvir-k52zvp'
|
||||
CFG_PROJECT_R2: 'proj-mts0s5jo-fb3rz7'
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://gitea.com/actions/checkout@v4
|
||||
|
||||
- name: Setup Node
|
||||
uses: https://gitea.com/actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install deps
|
||||
run: npm ci --no-audit --no-fund
|
||||
|
||||
- name: Build frontend
|
||||
# 复用 vite build 产物 (同时是 OTA bundle 内容)
|
||||
run: npm run build
|
||||
|
||||
- name: Install minisign
|
||||
run: sudo apt-get update && sudo apt-get install -y minisign
|
||||
|
||||
- name: Fetch config center (minisign key + R2)
|
||||
run: |
|
||||
set -euo pipefail
|
||||
export CONFIG_CENTER_URL="${CONFIG_CENTER_URL:-$REGISTRY_URL_FALLBACK}"
|
||||
python3 - <<'PYEOF'
|
||||
import json, os, urllib.request
|
||||
url = os.environ["CONFIG_CENTER_URL"]
|
||||
token = os.environ.get("CONFIG_CENTER_TOKEN", "") or ""
|
||||
wanted = {}
|
||||
req = urllib.request.Request(f"{url}/api/projects/{os.environ['CFG_PROJECT_SIGNING']}/configs")
|
||||
if token:
|
||||
req.add_header("Authorization", "Bearer " + token)
|
||||
with urllib.request.urlopen(req, timeout=20) as r:
|
||||
data = json.load(r).get("data", [])
|
||||
for cfg in data:
|
||||
if cfg.get("key") in ("MINISIGN_PRIVATE_KEY",):
|
||||
wanted[cfg["key"]] = cfg.get("value", "")
|
||||
req = urllib.request.Request(f"{url}/api/projects/{os.environ['CFG_PROJECT_R2']}/configs")
|
||||
if token:
|
||||
req.add_header("Authorization", "Bearer " + token)
|
||||
with urllib.request.urlopen(req, timeout=20) as r:
|
||||
data = json.load(r).get("data", [])
|
||||
for cfg in data:
|
||||
if cfg.get("key") in ("CF_API_TOKEN", "CF_R2_PUBLIC_URL"):
|
||||
wanted[cfg["key"]] = cfg.get("value", "")
|
||||
# GITHUB_ENV 多行值需用 heredoc 语法; 单行值直接 NAME=value
|
||||
with open(os.environ["GITHUB_ENV"], "a", encoding="utf-8") as f:
|
||||
for k, v in wanted.items():
|
||||
if "\n" in v or v.startswith("untrusted comment"):
|
||||
f.write(f"{k}<<EOF\n{v}\nEOF\n")
|
||||
else:
|
||||
f.write(f"{k}={v}\n")
|
||||
missing = [k for k in ("MINISIGN_PRIVATE_KEY", "CF_API_TOKEN") if k not in wanted]
|
||||
if missing:
|
||||
raise SystemExit("配置中心缺少: " + ", ".join(missing))
|
||||
PYEOF
|
||||
|
||||
- name: Build & sign OTA package
|
||||
# MINISIGN_PRIVATE_KEY 由上一步写入 GITHUB_ENV, 已是本步的真实环境变量,
|
||||
# build-ota.mjs 直接读 process.env.MINISIGN_PRIVATE_KEY, 无需再映射
|
||||
env:
|
||||
OTA_NOTES: ${{ inputs.notes }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ARGS="--out ota-package"
|
||||
if [ -n "${{ inputs.sequence }}" ]; then ARGS="${ARGS} --sequence ${{ inputs.sequence }}"; fi
|
||||
if [ -n "${OTA_NOTES}" ]; then ARGS="${ARGS} --notes ${OTA_NOTES}"; fi
|
||||
node scripts/build-ota.mjs ${ARGS}
|
||||
echo "===== latest.json ====="
|
||||
cat ota-package/latest.json
|
||||
echo "===== bundle sha256 ====="
|
||||
sha256sum ota-package/wardrobe-ota.tar.gz
|
||||
|
||||
- name: Upload OTA package to Cloudflare R2
|
||||
run: |
|
||||
bash scripts/upload-ota-r2.sh ota-package
|
||||
Reference in New Issue
Block a user