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 = {} minisign_key = None 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") == "MINISIGN_PRIVATE_KEY": minisign_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 多行值问题); R2 单行值仍走 GITHUB_ENV if not minisign_key: raise SystemExit("配置中心缺少 MINISIGN_PRIVATE_KEY (service-wardrobe/ota)") os.makedirs("ota-package", exist_ok=True) with open("ota-package/hotswap.key", "w", encoding="utf-8") as f: f.write(minisign_key if minisign_key.endswith("\n") else minisign_key + "\n") os.chmod("ota-package/hotswap.key", 0o600) 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 ("CF_API_TOKEN", "CF_R2_PUBLIC_URL") if k not in wanted] if missing: raise SystemExit("配置中心缺少: " + ", ".join(missing)) PYEOF echo "hotswap.key lines=$(wc -l < ota-package/hotswap.key)" - name: Build & sign OTA package # 私钥已由上一步写入 ota-package/hotswap.key, --key 显式指定 env: OTA_NOTES: ${{ inputs.notes }} run: | set -euo pipefail ARGS="--out ota-package --key ota-package/hotswap.key" 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