name: Deploy Client # Mirrors ~/code/jiu/.gitea/workflows/deploy-client.yml's tag→build→release→ # deploy shape. Android + Windows are required (release-deploy `needs` them); # macOS + iOS (Phase 3, see docs/superpowers/plans/2026-07-05-cicd.md) are # intentionally DECOUPLED — see the "why build-macos/build-ios don't block" # note above the build-macos job below for the mechanism and rationale. # # TODO(controller) — RUNNER AVAILABILITY: per docs/ci-runner.md, pangolin # currently has exactly ONE registered Gitea Actions runner # ("mac-pangolin-2", label `nas:host`). Neither `runs-on: mac` nor # `runs-on: windows` below has any runner registered to pick it up yet — this # workflow will queue forever until that's fixed. Options: (a) register # mac-pangolin-2 with an additional `mac` label (it's already a mac host — # cheapest fix for build-android/release-deploy) and separately stand up + # register an actual Windows host runner labeled `windows` for build-windows # (no such machine exists per docs/ci-runner.md), or (b) repoint both at # `nas` and accept that Android/Windows builds then compete with the # docker-in-domain nas jobs on the same single mac host. This mirrors the # `runs-on: mac` / `runs-on: windows` split already planned in # docs/superpowers/plans/2026-07-05-cicd.md Task 7/10 — written that way here # for fidelity to that plan, NOT because the runners are confirmed to exist. on: push: tags: - 'client-v[0-9]*.[0-9]*.[0-9]*' workflow_dispatch: concurrency: group: deploy-client # true:新 client-v* 取消仍在跑的旧发版(如 windows 机离线导致 build-windows 无限排队 # 卡住的旧 run),让最新版顺利发布,避免并发组被僵尸 run 占死。 cancel-in-progress: true jobs: build-android: runs-on: mac env: GOPROXY: https://goproxy.cn,direct PUB_HOSTED_URL: https://pub.flutter-io.cn FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn # sing-box 源码经 NAS gitea 公网镜像取(mac runner 连不上 github)。 # 用稳定公网域名 git.51yanmei.com(ali frps ← 家里 NAS frpc,nginx 443 反代 # gitea)—— 外网/内网都通,不依赖本地 relay/tailscale。见 scripts/build-libbox.sh。 SINGBOX_GIT: https://git.51yanmei.com/wangjia/sing-box.git steps: - name: Checkout uses: actions/checkout@v4 - name: Compile (Android APK) env: RELEASE_KEYSTORE: ${{ secrets.RELEASE_KEYSTORE }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} REF_NAME: ${{ gitea.ref_name }} run: bash scripts/ci/compile-android.sh "$REF_NAME" - name: Upload android artifact uses: actions/upload-artifact@v3 with: name: android path: dist/ build-windows: runs-on: windows env: GOPROXY: https://goproxy.cn,direct PUB_HOSTED_URL: https://pub.flutter-io.cn FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn # 境内镜像:GitHub release 资产在国内被 GFW 限速 → windows runner(LAN 内)下 # sing-box.exe / wintun.zip 超时。改从 NAS Gitea generic 包镜像拉, # fetch-desktop-bin.sh 命中镜像后照样验 SHA256,失败则回退官方源。 # ⚠️ 基址含 sing-box 版本目录(v1.13.12)——升级 app/kernel/VERSION 的 # SINGBOX_VERSION 时,须把新版 zip 重新 PUT 到对应版本目录并同步改这里。 DESKTOP_BIN_MIRROR: http://192.168.3.200:3000/api/packages/wangjia/generic/desktop-bin/v1.13.12 # 包默认可匿名读,token 非必需;带上以防将来把包设为私有(未设/为空则匿名 GET)。 DESKTOP_BIN_MIRROR_TOKEN: ${{ secrets.FORGEJO_TOKEN }} steps: - name: Checkout uses: actions/checkout@v4 - name: Compile (Windows installer) shell: bash env: REF_NAME: ${{ gitea.ref_name }} run: bash scripts/ci/compile-windows.sh "$REF_NAME" - name: Upload windows artifact uses: actions/upload-artifact@v3 with: name: windows path: dist/ # build-macos/build-ios 用 `continue-on-error: true` → 即便 Apple 侧构建抖了 # (frps 拉 sing-box 源码 / 公证超时等),也不阻塞 android+windows 发布: # 1. release-deploy 现在 `needs:` 全部四个平台(见下),所以它会**等**全部 # build 完成再发布 —— 避免旧 `needs: [build-android]` 只等 android、 # macos/ios 还没传产物就发布导致漏平台的竞态。 # 2. continue-on-error 让 macos/ios 失败仍算「completed」满足 needs,不使整个 # workflow 变红、不阻断已成功平台的发布;release-deploy 的「Download all # artifacts」(无 name 过滤)只捡实际存在的产物,缺某平台不报错。 # 3. Apple 签名 secret 现已在 gitea 用户级配齐(DEVELOPER_ID_P12/IOS_DIST_P12/ # APPSTORE_API_* 等),macos/ios 正常应成功;continue-on-error 只是抖动兜底。 build-macos: runs-on: mac continue-on-error: true env: GOPROXY: https://goproxy.cn,direct PUB_HOSTED_URL: https://pub.flutter-io.cn FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn # macOS 同样内嵌 libbox → 同走 NAS gitea 公网镜像取 sing-box 源码(见 build-android)。 SINGBOX_GIT: https://git.51yanmei.com/wangjia/sing-box.git steps: - name: Checkout uses: actions/checkout@v4 - name: Compile (macOS System Extension app) env: # 左=脚本期望的 env 名(勿改),右=实际密钥名。Apple 证书/公证密钥是 # 账号级、跨项目唯一 → 放【全局(用户级)密钥】用通用短名,pangolin/jiu 各自 # 在此映射到自己脚本的 env。Developer ID Application 证书=站外分发,一张签所有 app。 MACOS_DEVELOPER_ID_CERT_P12_BASE64: ${{ secrets.DEVELOPER_ID_P12 }} MACOS_DEVELOPER_ID_CERT_PASSWORD: ${{ secrets.P12_PASSWORD }} MACOS_APP_PROVISION_PROFILE_BASE64: ${{ secrets.MACOS_APP_PROVISION_PROFILE_BASE64 }} MACOS_SYSEXT_PROVISION_PROFILE_BASE64: ${{ secrets.MACOS_SYSEXT_PROVISION_PROFILE_BASE64 }} APPSTORE_API_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }} APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }} APPSTORE_API_KEY_P8_BASE64: ${{ secrets.APPSTORE_API_KEY_P8_BASE64 }} REF_NAME: ${{ gitea.ref_name }} run: bash scripts/ci/compile-macos.sh "$REF_NAME" - name: Upload macos artifact uses: actions/upload-artifact@v3 with: name: macos path: dist/ build-ios: runs-on: mac continue-on-error: true env: GOPROXY: https://goproxy.cn,direct PUB_HOSTED_URL: https://pub.flutter-io.cn FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn # iOS 同样内嵌 libbox → 同走 NAS gitea 公网镜像取 sing-box 源码(见 build-android)。 SINGBOX_GIT: https://git.51yanmei.com/wangjia/sing-box.git steps: - name: Checkout uses: actions/checkout@v4 - name: Compile & upload to TestFlight (iOS) env: # 证书=账号级 →【全局】通用短名(Apple Distribution 证书,签 iOS App Store/TestFlight); # 描述文件=与 bundle id 绑定 →【项目级】pangolin 仓库密钥。 IOS_DIST_CERT_P12_BASE64: ${{ secrets.IOS_DIST_P12 }} IOS_DIST_CERT_PASSWORD: ${{ secrets.IOS_DIST_PASSWORD }} IOS_APP_PROVISIONING_PROFILE_BASE64: ${{ secrets.IOS_APP_PROVISIONING_PROFILE_BASE64 }} IOS_PACKETTUNNEL_PROVISIONING_PROFILE_BASE64: ${{ secrets.IOS_PACKETTUNNEL_PROVISIONING_PROFILE_BASE64 }} APPSTORE_API_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }} APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }} APPSTORE_API_KEY_P8_BASE64: ${{ secrets.APPSTORE_API_KEY_P8_BASE64 }} REF_NAME: ${{ gitea.ref_name }} run: bash scripts/ci/compile-ios.sh "$REF_NAME" # No artifact upload — compile-ios.sh uploads straight to TestFlight via # altool (matches jiu); nothing is produced under dist/ for this job. # release-deploy 发布策略:needs 全部四平台(等它们**完成**,不抢跑),但用 # `if` 让 mac/ios/windows 的失败不 skip 本 job —— 只要 android(稳定 floor)成功即发。 # 背景:build-macos/ios 必须跑在 mac runner,其 Checkout(git fetch git.51yanmei.com) # 走 frps 隧道偶发超时挂掉(run 239 的 ios 就是 fetch fdc13ea 超时,非 iOS 构建问题; # run 238 同代码 fetch 碰巧成功)。这类瞬断不该阻断已成功平台的发布。 # · needs 四平台 → download-artifact 收到当时**全部已上传**的产物(修旧 # `needs:[build-android]` 只等 android、macos/ios 没传就发布漏平台的竞态)。 # · continue-on-error 在 gitea 里**不**让下游 needs 在失败时继续(会 skip),故必须 # 配 `if: always() && needs.build-android.result=='success'` 才能「等全部、失败不阻」。 release-deploy: needs: [build-android, build-windows, build-macos, build-ios] if: ${{ always() && needs.build-android.result == 'success' }} # ubuntu-latest = 家里 nas act_runner,与 gitea 同机/同网:Release → Forgejo 的 # API 调用走本地不过 frps 隧道(mac runner 走 git.51yanmei.com→frps→抖,curl 超时 # exit 28,3m3s=release_ensure 60s×3 重试耗尽)。deploy-server/deploy-site 同为 # ubuntu-latest,其 Forgejo release 步稳定通过(server 仅 13s)。此 job 全为网络/ # SSH 步骤(下载产物/传 Forgejo/SSH pangolin1/通知),无 mac 专属需求。 runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 # 一次性下所有 artifact(不带 name),避免同 job 内两次复用 download-artifact # action → act 对其只读缓存 git 仓库做二次操作时 EACCES(pack idx 444)。 - name: Download all artifacts uses: actions/download-artifact@v3 with: path: dist-raw/ - name: Flatten artifacts into dist/ shell: bash run: | mkdir -p dist find dist-raw -type f -exec cp {} dist/ \; echo "dist/ 内容:"; ls -la dist/ - name: Release → Forgejo env: FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }} FORGEJO_URL: ${{ secrets.FORGEJO_URL }} # Needed here (not just in the "Deploy" step below) because # release-client.sh now also SSH-pushes the auto-update manifest # (version.yaml) straight to pangolin1's /etc/pangolin/ — see the # header comment in scripts/ci/release-client.sh. DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} REF_NAME: ${{ gitea.ref_name }} run: bash scripts/ci/release-client.sh "$REF_NAME" - name: Deploy → pangolin1 (downloads/) env: DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }} REF_NAME: ${{ gitea.ref_name }} run: bash scripts/ci/deploy-client.sh "$REF_NAME" - name: Notify if: always() env: TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} REF_NAME: ${{ gitea.ref_name }} JOB_STATUS: ${{ job.status }} run: | . scripts/ci/notify.sh if [ "$JOB_STATUS" = "success" ]; then notify_ok "client $REF_NAME released + deployed" else notify_fail "client $REF_NAME pipeline failed" fi