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: nas env: GOPROXY: https://goproxy.cn,direct PUB_HOSTED_URL: https://pub.flutter-io.cn FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn 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/ # Why build-macos/build-ios don't block the working android+windows pipeline # when Apple secrets aren't configured yet (they aren't, as of this writing): # 1. release-deploy's `needs:` below is [build-android, build-windows] # ONLY — macOS/iOS are NOT dependencies, so release-deploy never waits # on them and never fails because of them. # 2. `continue-on-error: true` on both jobs keeps the overall workflow-run # status green even while compile-macos.sh hard-fails (Apple Developer # ID / notary secrets absent — see its fail-fast checks) — that failure # is real signal ("go configure the secrets"), but it shouldn't read as # "the release pipeline is broken" when android+windows shipped fine. # 3. compile-macos.sh's own default behavior is to hard-fail (not skip) # when its secrets are missing (macOS distribution must never ship # unsigned/unnotarized — see its header comment); compile-ios.sh's # default is to skip gracefully (exit 0) since an unconfigured iOS # account is a normal "not set up yet" state, not a defect. Either way # the job produces no dist/pangolin-macos-x64.zip, and # release-deploy's "Download all artifacts" step (no `name:` filter) # simply picks up whatever artifacts DO exist — an absent "macos" # artifact is not an error there. build-macos: runs-on: nas continue-on-error: true env: PUB_HOSTED_URL: https://pub.flutter-io.cn FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn 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: nas continue-on-error: true env: PUB_HOSTED_URL: https://pub.flutter-io.cn FLUTTER_STORAGE_BASE_URL: https://storage.flutter-io.cn 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 build-android(唯一稳定可用的平台 floor)。windows/macos/ios # best-effort:各自 runner+secret 就绪则上传 artifact,release-deploy flatten 收 # dist-raw/ 里"当时存在"的产物。windows 机离线 / Apple secret 未配 都不阻塞发版 # (对应平台下载保留 pangolin1 上一版,待可用时下个 client-v* 追上)。 release-deploy: needs: [build-android] runs-on: nas 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