Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4e8d928350 |
@@ -58,6 +58,8 @@ jobs:
|
|||||||
/mnt/scripts/ci/backup-db.sh \
|
/mnt/scripts/ci/backup-db.sh \
|
||||||
/mnt/scripts/ci/compile-android.sh \
|
/mnt/scripts/ci/compile-android.sh \
|
||||||
/mnt/scripts/ci/compile-windows.sh \
|
/mnt/scripts/ci/compile-windows.sh \
|
||||||
|
/mnt/scripts/ci/compile-macos.sh \
|
||||||
|
/mnt/scripts/ci/compile-ios.sh \
|
||||||
/mnt/scripts/ci/release-client.sh \
|
/mnt/scripts/ci/release-client.sh \
|
||||||
/mnt/scripts/ci/deploy-client.sh
|
/mnt/scripts/ci/deploy-client.sh
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
name: Deploy Client
|
name: Deploy Client
|
||||||
|
|
||||||
# Mirrors ~/code/jiu/.gitea/workflows/deploy-client.yml's tag→build→release→
|
# Mirrors ~/code/jiu/.gitea/workflows/deploy-client.yml's tag→build→release→
|
||||||
# deploy shape, trimmed to the two platforms drafted so far (Android +
|
# deploy shape. Android + Windows are required (release-deploy `needs` them);
|
||||||
# Windows — no client-web/macOS/iOS jobs; pangolin has no Flutter-web build
|
# macOS + iOS (Phase 3, see docs/superpowers/plans/2026-07-05-cicd.md) are
|
||||||
# in this pipeline and macOS/iOS are a separate not-yet-drafted phase, see
|
# intentionally DECOUPLED — see the "why build-macos/build-ios don't block"
|
||||||
# docs/superpowers/plans/2026-07-05-cicd.md Phase 3).
|
# note above the build-macos job below for the mechanism and rationale.
|
||||||
#
|
#
|
||||||
# TODO(controller) — RUNNER AVAILABILITY: per docs/ci-runner.md, pangolin
|
# TODO(controller) — RUNNER AVAILABILITY: per docs/ci-runner.md, pangolin
|
||||||
# currently has exactly ONE registered Gitea Actions runner
|
# currently has exactly ONE registered Gitea Actions runner
|
||||||
@@ -76,6 +76,77 @@ jobs:
|
|||||||
name: windows
|
name: windows
|
||||||
path: dist/
|
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: mac
|
||||||
|
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:
|
||||||
|
MACOS_DEVELOPER_ID_CERT_P12_BASE64: ${{ secrets.MACOS_DEVELOPER_ID_CERT_P12_BASE64 }}
|
||||||
|
MACOS_DEVELOPER_ID_CERT_PASSWORD: ${{ secrets.MACOS_DEVELOPER_ID_CERT_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:
|
||||||
|
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:
|
||||||
|
IOS_DIST_CERT_P12_BASE64: ${{ secrets.IOS_DIST_CERT_P12_BASE64 }}
|
||||||
|
IOS_DIST_CERT_PASSWORD: ${{ secrets.IOS_DIST_CERT_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:
|
release-deploy:
|
||||||
needs: [build-android, build-windows]
|
needs: [build-android, build-windows]
|
||||||
runs-on: mac
|
runs-on: mac
|
||||||
@@ -101,6 +172,11 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
|
||||||
FORGEJO_URL: ${{ secrets.FORGEJO_URL }}
|
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 }}
|
REF_NAME: ${{ gitea.ref_name }}
|
||||||
run: bash scripts/ci/release-client.sh "$REF_NAME"
|
run: bash scripts/ci/release-client.sh "$REF_NAME"
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import package_info_plus
|
|||||||
import screen_retriever_macos
|
import screen_retriever_macos
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import tray_manager
|
import tray_manager
|
||||||
|
import url_launcher_macos
|
||||||
import window_manager
|
import window_manager
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
@@ -20,5 +21,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
|
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
|
||||||
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||||
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
|
||||||
#include <tray_manager/tray_manager_plugin.h>
|
#include <tray_manager/tray_manager_plugin.h>
|
||||||
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
#include <window_manager/window_manager_plugin.h>
|
#include <window_manager/window_manager_plugin.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
@@ -18,6 +19,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
|
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
|
||||||
TrayManagerPluginRegisterWithRegistrar(
|
TrayManagerPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("TrayManagerPlugin"));
|
registry->GetRegistrarForPlugin("TrayManagerPlugin"));
|
||||||
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
WindowManagerPluginRegisterWithRegistrar(
|
WindowManagerPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
|
registry->GetRegistrarForPlugin("WindowManagerPlugin"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
flutter_secure_storage_windows
|
flutter_secure_storage_windows
|
||||||
screen_retriever_windows
|
screen_retriever_windows
|
||||||
tray_manager
|
tray_manager
|
||||||
|
url_launcher_windows
|
||||||
window_manager
|
window_manager
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Executable
+198
@@ -0,0 +1,198 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# compile-ios.sh <tag> — build a signed iOS IPA (app + PacketTunnel Network
|
||||||
|
# Extension) and upload it to TestFlight (App Store Connect).
|
||||||
|
#
|
||||||
|
# Mirrors ~/code/jiu/scripts/ci/compile-ios.sh's shape (temp-keychain dist
|
||||||
|
# cert import, provisioning-profile install by UUID, ExportOptions.plist,
|
||||||
|
# `flutter build ipa`, `xcrun altool --upload-app`), adapted for pangolin:
|
||||||
|
# - pangolin's iOS app ships a NEPacketTunnelProvider **app extension**
|
||||||
|
# (client/ios/PacketTunnel/, bundle com.pangolin.pangolinVpn.PacketTunnel)
|
||||||
|
# alongside the main app (com.pangolin.pangolinVpn) — jiu has no
|
||||||
|
# extension at all, just the one app target. So this script needs TWO
|
||||||
|
# distribution provisioning profiles (app + extension), not one, and the
|
||||||
|
# ExportOptions.plist provisioningProfiles dict needs both bundle-id ->
|
||||||
|
# profile-name mappings.
|
||||||
|
# - pangolin embeds sing-box as a native core (gomobile-built
|
||||||
|
# Libbox.xcframework, io.nekohasekai.libbox) — jiu's Flutter app has no
|
||||||
|
# embedded core. This script MUST run `scripts/build-libbox.sh apple ios`
|
||||||
|
# before `flutter build ipa`, or the IPA links no VPN kernel (mirrors the
|
||||||
|
# equivalent step in compile-android.sh / compile-macos.sh).
|
||||||
|
# - Team ID BYL4KQHMTN is hardcoded as a script constant rather than a
|
||||||
|
# secret (unlike jiu's IOS_TEAM_ID secret): it's already public inside
|
||||||
|
# this repo (CLAUDE.md, client/ios/Runner.xcodeproj/project.pbxproj
|
||||||
|
# DEVELOPMENT_TEAM, scripts/local_test.sh SIGN_ID) — not sensitive, no
|
||||||
|
# reason to add another secret for it.
|
||||||
|
# - APPSTORE_API_KEY_ID / APPSTORE_API_ISSUER_ID / APPSTORE_API_KEY_P8_BASE64
|
||||||
|
# are shared with compile-macos.sh's notarization step (same App Store
|
||||||
|
# Connect API key serves both notarization and TestFlight upload).
|
||||||
|
#
|
||||||
|
# Required CI secrets (missing ANY of them => SKIP gracefully, exit 0 — does
|
||||||
|
# NOT fail the pipeline; Apple iOS distribution needs an enrolled account +
|
||||||
|
# certs that may not exist yet, unlike macOS Developer ID which is a hard
|
||||||
|
# fail-fast in compile-macos.sh):
|
||||||
|
# IOS_DIST_CERT_P12_BASE64 Apple Distribution 证书(.p12)base64
|
||||||
|
# IOS_DIST_CERT_PASSWORD .p12 导出密码
|
||||||
|
# IOS_APP_PROVISIONING_PROFILE_BASE64 App Store 类型描述文件(主 app,
|
||||||
|
# com.pangolin.pangolinVpn)base64
|
||||||
|
# IOS_PACKETTUNNEL_PROVISIONING_PROFILE_BASE64 App Store 类型描述文件(扩展,
|
||||||
|
# com.pangolin.pangolinVpn.PacketTunnel)base64
|
||||||
|
# APPSTORE_API_KEY_ID / APPSTORE_API_ISSUER_ID / APPSTORE_API_KEY_P8_BASE64
|
||||||
|
# App Store Connect API Key(上传 TestFlight 用;与 compile-macos.sh 公证共用)。
|
||||||
|
#
|
||||||
|
# No dist/ artifact is produced (matches jiu) — the IPA goes straight to
|
||||||
|
# TestFlight via altool, nothing is uploaded to the Forgejo release.
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=scripts/ci/_env.sh
|
||||||
|
. "${SCRIPT_DIR}/_env.sh"
|
||||||
|
|
||||||
|
TAG="${1:?usage: compile-ios.sh <tag>}"
|
||||||
|
|
||||||
|
ver_file="/tmp/compile_ios_ver.$$"
|
||||||
|
ver_from_tag client "$TAG" > "$ver_file"
|
||||||
|
VER=""
|
||||||
|
read -r VER < "$ver_file" || true # 无结尾换行的 read 退出码,见 compile-android.sh 同注释
|
||||||
|
rm -f "$ver_file"
|
||||||
|
|
||||||
|
# CFBundleVersion(两个 target 均为 "$(FLUTTER_BUILD_NUMBER)",见
|
||||||
|
# client/ios/Runner.xcodeproj/project.pbxproj —— 与 macOS 的 PacketTunnel 硬编码
|
||||||
|
# CURRENT_PROJECT_VERSION 不同,这里走 flutter build --build-number 走标准路径)。
|
||||||
|
# 必须单调递增,否则 TestFlight 拒绝重复上传(同 jiu 注释),公式与
|
||||||
|
# compile-android.sh / compile-macos.sh 一致。
|
||||||
|
MAJOR="${VER%%.*}"
|
||||||
|
_REST="${VER#*.}"
|
||||||
|
MINOR="${_REST%%.*}"
|
||||||
|
PATCH="${_REST#*.}"
|
||||||
|
PATCH="${PATCH%%-*}"
|
||||||
|
BUILD=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
|
||||||
|
|
||||||
|
echo "==> compile-ios: tag=${TAG} version=${VER} build=${BUILD}"
|
||||||
|
|
||||||
|
# ── 凭证缺失则优雅跳过(不阻塞流水线;见头部说明)─────────────────────────
|
||||||
|
if [ -z "${IOS_DIST_CERT_P12_BASE64:-}" ] || \
|
||||||
|
[ -z "${IOS_DIST_CERT_PASSWORD:-}" ] || \
|
||||||
|
[ -z "${IOS_APP_PROVISIONING_PROFILE_BASE64:-}" ] || \
|
||||||
|
[ -z "${IOS_PACKETTUNNEL_PROVISIONING_PROFILE_BASE64:-}" ] || \
|
||||||
|
[ -z "${APPSTORE_API_KEY_ID:-}" ] || \
|
||||||
|
[ -z "${APPSTORE_API_ISSUER_ID:-}" ] || \
|
||||||
|
[ -z "${APPSTORE_API_KEY_P8_BASE64:-}" ]; then
|
||||||
|
echo "==> compile-ios: SKIP — iOS signing secrets not fully configured yet (not a failure)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||||
|
API_URL="${PANGOLIN_API_URL:-https://api.yanmeiai.com}"
|
||||||
|
TEAM_ID="BYL4KQHMTN"
|
||||||
|
APP_BUNDLE_ID="com.pangolin.pangolinVpn"
|
||||||
|
EXT_BUNDLE_ID="com.pangolin.pangolinVpn.PacketTunnel"
|
||||||
|
|
||||||
|
WORK="$(mktemp -d)"
|
||||||
|
KEYCHAIN="${WORK}/pangolin-ios-ci.keychain-db"
|
||||||
|
PROFILES_DIR="${HOME}/Library/MobileDevice/Provisioning Profiles"
|
||||||
|
mkdir -p "$PROFILES_DIR"
|
||||||
|
|
||||||
|
cleanup_ios() {
|
||||||
|
security delete-keychain "$KEYCHAIN" 2>/dev/null || true
|
||||||
|
security list-keychains -d user -s login.keychain-db 2>/dev/null || true
|
||||||
|
[ -n "${APP_PROFILE_UUID:-}" ] && rm -f "${PROFILES_DIR}/${APP_PROFILE_UUID}.mobileprovision" 2>/dev/null || true
|
||||||
|
[ -n "${EXT_PROFILE_UUID:-}" ] && rm -f "${PROFILES_DIR}/${EXT_PROFILE_UUID}.mobileprovision" 2>/dev/null || true
|
||||||
|
rm -rf "$WORK"
|
||||||
|
}
|
||||||
|
trap cleanup_ios EXIT
|
||||||
|
|
||||||
|
# ── [1/6] 重建 libbox.xcframework(apple ios;gitignore 产物,每次都要重建) ──
|
||||||
|
echo "==> compile-ios: building Libbox.xcframework (apple ios)"
|
||||||
|
bash "${REPO_ROOT}/scripts/build-libbox.sh" apple ios
|
||||||
|
LIBBOX_FW="${REPO_ROOT}/client/ios/Frameworks/Libbox.xcframework"
|
||||||
|
if [ ! -d "$LIBBOX_FW" ]; then
|
||||||
|
echo "ERROR: ${LIBBOX_FW} not found after build-libbox.sh — aborting." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── [2/6] 临时 keychain 导入 Apple Distribution 证书 ─────────────────────────
|
||||||
|
KEYCHAIN_PWD="ci-temp-$$"
|
||||||
|
security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN"
|
||||||
|
security set-keychain-settings -lut 21600 "$KEYCHAIN"
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN"
|
||||||
|
printf '%s' "$IOS_DIST_CERT_P12_BASE64" | base64 --decode > "${WORK}/dist.p12"
|
||||||
|
security import "${WORK}/dist.p12" -k "$KEYCHAIN" \
|
||||||
|
-P "$IOS_DIST_CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
|
||||||
|
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PWD" "$KEYCHAIN" >/dev/null
|
||||||
|
security list-keychains -d user -s "$KEYCHAIN" login.keychain-db
|
||||||
|
|
||||||
|
# ── [3/6] 安装 2 个 App Store 类型描述文件(app + PacketTunnel 扩展) ─────────
|
||||||
|
printf '%s' "$IOS_APP_PROVISIONING_PROFILE_BASE64" | base64 --decode > "${WORK}/app.mobileprovision"
|
||||||
|
security cms -D -i "${WORK}/app.mobileprovision" > "${WORK}/app_profile.plist"
|
||||||
|
/usr/libexec/PlistBuddy -c 'Print :Name' "${WORK}/app_profile.plist" > "${WORK}/app_name.txt"
|
||||||
|
/usr/libexec/PlistBuddy -c 'Print :UUID' "${WORK}/app_profile.plist" > "${WORK}/app_uuid.txt"
|
||||||
|
APP_PROFILE_NAME=""
|
||||||
|
read -r APP_PROFILE_NAME < "${WORK}/app_name.txt" || true
|
||||||
|
APP_PROFILE_UUID=""
|
||||||
|
read -r APP_PROFILE_UUID < "${WORK}/app_uuid.txt" || true
|
||||||
|
cp "${WORK}/app.mobileprovision" "${PROFILES_DIR}/${APP_PROFILE_UUID}.mobileprovision"
|
||||||
|
echo "==> compile-ios: app profile '${APP_PROFILE_NAME}' (${APP_PROFILE_UUID})"
|
||||||
|
|
||||||
|
printf '%s' "$IOS_PACKETTUNNEL_PROVISIONING_PROFILE_BASE64" | base64 --decode > "${WORK}/ext.mobileprovision"
|
||||||
|
security cms -D -i "${WORK}/ext.mobileprovision" > "${WORK}/ext_profile.plist"
|
||||||
|
/usr/libexec/PlistBuddy -c 'Print :Name' "${WORK}/ext_profile.plist" > "${WORK}/ext_name.txt"
|
||||||
|
/usr/libexec/PlistBuddy -c 'Print :UUID' "${WORK}/ext_profile.plist" > "${WORK}/ext_uuid.txt"
|
||||||
|
EXT_PROFILE_NAME=""
|
||||||
|
read -r EXT_PROFILE_NAME < "${WORK}/ext_name.txt" || true
|
||||||
|
EXT_PROFILE_UUID=""
|
||||||
|
read -r EXT_PROFILE_UUID < "${WORK}/ext_uuid.txt" || true
|
||||||
|
cp "${WORK}/ext.mobileprovision" "${PROFILES_DIR}/${EXT_PROFILE_UUID}.mobileprovision"
|
||||||
|
echo "==> compile-ios: PacketTunnel profile '${EXT_PROFILE_NAME}' (${EXT_PROFILE_UUID})"
|
||||||
|
|
||||||
|
# ── [4/6] App Store Connect API Key(供 altool 上传使用)──────────────────────
|
||||||
|
API_KEYS_DIR="${HOME}/.appstoreconnect/private_keys"
|
||||||
|
mkdir -p "$API_KEYS_DIR"
|
||||||
|
printf '%s' "$APPSTORE_API_KEY_P8_BASE64" | base64 --decode > "${API_KEYS_DIR}/AuthKey_${APPSTORE_API_KEY_ID}.p8"
|
||||||
|
|
||||||
|
# ── [5/6] 同步版本号 + 生成 ExportOptions.plist(manual 签名,两个 bundle 都映射)──
|
||||||
|
sed -i '' "s/^version:.*/version: ${VER}+${BUILD}/" "${REPO_ROOT}/client/pubspec.yaml"
|
||||||
|
|
||||||
|
cat > "${WORK}/ExportOptions.plist" <<EOF
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>method</key><string>app-store</string>
|
||||||
|
<key>teamID</key><string>${TEAM_ID}</string>
|
||||||
|
<key>signingStyle</key><string>manual</string>
|
||||||
|
<key>uploadBitcode</key><false/>
|
||||||
|
<key>uploadSymbols</key><true/>
|
||||||
|
<key>provisioningProfiles</key>
|
||||||
|
<dict>
|
||||||
|
<key>${APP_BUNDLE_ID}</key><string>${APP_PROFILE_NAME}</string>
|
||||||
|
<key>${EXT_BUNDLE_ID}</key><string>${EXT_PROFILE_NAME}</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ── [6/6] flutter build ipa + 上传 TestFlight ───────────────────────────────
|
||||||
|
cd "${REPO_ROOT}/client"
|
||||||
|
flutter build ipa --release \
|
||||||
|
--build-name="${VER}" \
|
||||||
|
--build-number="${BUILD}" \
|
||||||
|
--export-options-plist="${WORK}/ExportOptions.plist" \
|
||||||
|
"--dart-define=PANGOLIN_API_URL=${API_URL}"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
IPA=""
|
||||||
|
for cand in "${REPO_ROOT}"/client/build/ios/ipa/*.ipa; do
|
||||||
|
if [ -f "$cand" ]; then IPA="$cand"; break; fi
|
||||||
|
done
|
||||||
|
if [ -z "$IPA" ]; then
|
||||||
|
echo "ERROR: IPA not found under client/build/ios/ipa/" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "==> compile-ios: built ${IPA}"
|
||||||
|
|
||||||
|
echo "==> compile-ios: uploading to TestFlight"
|
||||||
|
xcrun altool --upload-app -f "$IPA" -t ios \
|
||||||
|
--apiKey "$APPSTORE_API_KEY_ID" \
|
||||||
|
--apiIssuer "$APPSTORE_API_ISSUER_ID"
|
||||||
|
|
||||||
|
echo "==> compile-ios: done — uploaded build ${BUILD} to TestFlight"
|
||||||
Executable
+267
@@ -0,0 +1,267 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# compile-macos.sh <tag> — build the Flutter macOS app (with its embedded
|
||||||
|
# PacketTunnel System Extension), Developer ID sign + notarize + staple,
|
||||||
|
# package into dist/.
|
||||||
|
#
|
||||||
|
# Mirrors ~/code/jiu/scripts/ci/compile-macos.sh's shape (temp-keychain
|
||||||
|
# Developer ID import, inside-out codesign, notarytool submit --wait, staple,
|
||||||
|
# ditto zip), but pangolin's macOS app is heavier than jiu's plain window:
|
||||||
|
# - it embeds a System Extension (com.pangolin.pangolin.PacketTunnel,
|
||||||
|
# `.systemextension` bundle under Contents/Library/SystemExtensions/)
|
||||||
|
# that needs ITS OWN Developer ID provisioning profile + entitlements,
|
||||||
|
# signed separately, inside-out, before the outer app is signed — see
|
||||||
|
# CLAUDE.md "client/ macOS 原生隧道" and
|
||||||
|
# docs/macos-sysext-realize-troubleshooting.html for why every one of
|
||||||
|
# these steps is load-bearing (wrong order / missing entitlement /
|
||||||
|
# unsigned nested item => sysextd silently refuses to load it).
|
||||||
|
# - the Xcode project (client/macos/Runner.xcodeproj) is ALREADY configured
|
||||||
|
# CODE_SIGN_STYLE=Manual with CODE_SIGN_IDENTITY="Developer ID
|
||||||
|
# Application" + PROVISIONING_PROFILE_SPECIFIER set per target (unlike
|
||||||
|
# jiu, whose Release config has CODE_SIGN_IDENTITY="-", i.e. unsigned at
|
||||||
|
# build time, signed entirely by hand afterward). So `flutter build macos
|
||||||
|
# --release` here should already produce a signed .app *if* the identity
|
||||||
|
# is in a searched keychain and the profiles are in place. This script
|
||||||
|
# still re-runs the explicit inside-out codesign pass below (mirroring
|
||||||
|
# scripts/local_test.sh's now-legacy `cmd_sign`) as a defensive,
|
||||||
|
# idempotent safety net — resigning with the same identity is harmless,
|
||||||
|
# and it removes any dependency on Xcode's automatic nested-item signing
|
||||||
|
# behaving identically on a CI runner vs. the maintainer's dev machine.
|
||||||
|
# - CFBundleVersion for the PacketTunnel extension is NOT derived from
|
||||||
|
# FLUTTER_BUILD_NUMBER (unlike the main Runner target, and unlike iOS
|
||||||
|
# where every target uses "$(FLUTTER_BUILD_NUMBER)") — it's a literal
|
||||||
|
# CURRENT_PROJECT_VERSION build setting hardcoded in project.pbxproj
|
||||||
|
# (currently 53, see CLAUDE.md: "每次构建必递增 CFBundleVersion
|
||||||
|
# (CURRENT_PROJECT_VERSION)——否则 sysextd 视为同版本不更新"). This script
|
||||||
|
# computes a monotonic build number from the tag version (same
|
||||||
|
# major*10000+minor*100+patch formula as compile-android.sh /
|
||||||
|
# compile-ios.sh) and sed's every CURRENT_PROJECT_VERSION occurrence in
|
||||||
|
# the pbxproj (RunnerTests + PacketTunnel Debug/Release/Profile, 6 total)
|
||||||
|
# before building. This edit is NOT committed back to git — each CI run
|
||||||
|
# starts from the repo's checked-in value and recomputes deterministically
|
||||||
|
# from the tag, so it stays monotonic across releases as long as the
|
||||||
|
# version itself increases.
|
||||||
|
#
|
||||||
|
# Required CI secrets (fail-fast — ANY missing => abort, never ship unsigned):
|
||||||
|
# MACOS_DEVELOPER_ID_CERT_P12_BASE64 Developer ID Application 证书(.p12)base64
|
||||||
|
# MACOS_DEVELOPER_ID_CERT_PASSWORD .p12 导出密码
|
||||||
|
# MACOS_APP_PROVISION_PROFILE_BASE64 "Pangolin App DevID" 描述文件 base64
|
||||||
|
# MACOS_SYSEXT_PROVISION_PROFILE_BASE64 "Pangolin PacketTunnel DevID" 描述文件 base64
|
||||||
|
# APPSTORE_API_KEY_ID / APPSTORE_API_ISSUER_ID / APPSTORE_API_KEY_P8_BASE64
|
||||||
|
# App Store Connect API Key(公证用,notarytool --key/--key-id/--issuer;
|
||||||
|
# 不同于 scripts/local_test.sh 本机用的 --keychain-profile pangolin-notary,
|
||||||
|
# CI 无法预置 keychain profile,必须走显式 API key 三件套)。
|
||||||
|
#
|
||||||
|
# Output: dist/pangolin-macos-x64.zip (stable name — deploy-client.sh /
|
||||||
|
# release-client.sh key off this exact filename).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
# shellcheck source=scripts/ci/_env.sh
|
||||||
|
. "${SCRIPT_DIR}/_env.sh"
|
||||||
|
|
||||||
|
TAG="${1:?usage: compile-macos.sh <tag>}"
|
||||||
|
|
||||||
|
ver_file="/tmp/compile_macos_ver.$$"
|
||||||
|
ver_from_tag client "$TAG" > "$ver_file"
|
||||||
|
VER=""
|
||||||
|
read -r VER < "$ver_file" || true # 无结尾换行的 read 退出码,见 compile-android.sh 同注释
|
||||||
|
rm -f "$ver_file"
|
||||||
|
|
||||||
|
# 单调递增 build 号:major*10000 + minor*100 + patch(同 compile-android.sh /
|
||||||
|
# compile-ios.sh 公式)。纯参数展开,不用 cut/$();剥掉 patch 段的 `-suffix`
|
||||||
|
# (如预发布 tag "48-rc1")以保证后面的算术展开是纯数字。
|
||||||
|
MAJOR="${VER%%.*}"
|
||||||
|
_REST="${VER#*.}"
|
||||||
|
MINOR="${_REST%%.*}"
|
||||||
|
PATCH="${_REST#*.}"
|
||||||
|
PATCH="${PATCH%%-*}"
|
||||||
|
BUILD=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
|
||||||
|
|
||||||
|
echo "==> compile-macos: tag=${TAG} version=${VER} build=${BUILD}"
|
||||||
|
|
||||||
|
# ── [0/7] fail-fast:签名/公证所需 secret 一个不少 ──────────────────────────
|
||||||
|
# 比 jiu 的 mac 脚本(只查 2 个)更严格:任一缺失都直接报错中止,不产出未签名/
|
||||||
|
# 未公证包。用 `${VAR:?msg}` 而非 if-empty,出错信息更精确定位到具体哪个 secret。
|
||||||
|
: "${MACOS_DEVELOPER_ID_CERT_P12_BASE64:?缺少 MACOS_DEVELOPER_ID_CERT_P12_BASE64:未配置 Developer ID 证书,构建中止(不产出未签名包)}"
|
||||||
|
: "${MACOS_DEVELOPER_ID_CERT_PASSWORD:?缺少 MACOS_DEVELOPER_ID_CERT_PASSWORD}"
|
||||||
|
: "${MACOS_APP_PROVISION_PROFILE_BASE64:?缺少 MACOS_APP_PROVISION_PROFILE_BASE64('Pangolin App DevID' 描述文件)}"
|
||||||
|
: "${MACOS_SYSEXT_PROVISION_PROFILE_BASE64:?缺少 MACOS_SYSEXT_PROVISION_PROFILE_BASE64('Pangolin PacketTunnel DevID' 描述文件)}"
|
||||||
|
: "${APPSTORE_API_KEY_ID:?缺少 APPSTORE_API_KEY_ID(公证用 App Store Connect API Key)}"
|
||||||
|
: "${APPSTORE_API_ISSUER_ID:?缺少 APPSTORE_API_ISSUER_ID}"
|
||||||
|
: "${APPSTORE_API_KEY_P8_BASE64:?缺少 APPSTORE_API_KEY_P8_BASE64}"
|
||||||
|
|
||||||
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||||
|
API_URL="${PANGOLIN_API_URL:-https://api.yanmeiai.com}"
|
||||||
|
SIGN_ID_PREFIX="Developer ID Application"
|
||||||
|
TEAM_ID="BYL4KQHMTN"
|
||||||
|
APP_BUNDLE_ID="com.pangolin.pangolin"
|
||||||
|
SYSEXT_BUNDLE_ID="com.pangolin.pangolin.PacketTunnel"
|
||||||
|
APP_GROUP="${TEAM_ID}.com.pangolin.pangolin"
|
||||||
|
|
||||||
|
WORK="$(mktemp -d)"
|
||||||
|
KEYCHAIN="${WORK}/pangolin-mac-ci.keychain-db"
|
||||||
|
# 与 scripts/local_test.sh 用的描述文件目录保持一致(Xcode UserData 目录,
|
||||||
|
# 而非经典的 ~/Library/MobileDevice/Provisioning Profiles/)——CI 与本机联调
|
||||||
|
# 复用同一处,两边的 "Pangolin App DevID"/"Pangolin PacketTunnel DevID" 描述
|
||||||
|
# 文件本就应该是同一份,CI 覆盖写入是幂等的。故清理阶段【不删除】这两个描述
|
||||||
|
# 文件(只清理临时 keychain/证书),避免影响维护者在同一台 mac runner 上继续
|
||||||
|
# 用 local_test.sh 手动联调。
|
||||||
|
PROF_DIR="${HOME}/Library/Developer/Xcode/UserData/Provisioning Profiles"
|
||||||
|
mkdir -p "$PROF_DIR"
|
||||||
|
|
||||||
|
cleanup_mac() {
|
||||||
|
security delete-keychain "$KEYCHAIN" 2>/dev/null || true
|
||||||
|
# 把 keychain 搜索列表还原成默认单项,不把临时 keychain 的痕迹留在这台常驻
|
||||||
|
# runner 上(mac-pangolin-2 同时也被人手动用来跑 local_test.sh)。
|
||||||
|
security list-keychains -d user -s login.keychain-db 2>/dev/null || true
|
||||||
|
rm -rf "$WORK"
|
||||||
|
}
|
||||||
|
trap cleanup_mac EXIT
|
||||||
|
|
||||||
|
# ── [1/7] 重建 libbox.xcframework(gitignore 产物,每次换 worktree/CI 都要重建)─
|
||||||
|
echo "==> compile-macos: building Libbox.xcframework (apple macos)"
|
||||||
|
bash "${REPO_ROOT}/scripts/build-libbox.sh" apple macos
|
||||||
|
LIBBOX_FW="${REPO_ROOT}/client/macos/Frameworks/Libbox.xcframework"
|
||||||
|
if [ ! -d "$LIBBOX_FW" ]; then
|
||||||
|
echo "ERROR: ${LIBBOX_FW} not found after build-libbox.sh — aborting." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── [2/7] 递增 CURRENT_PROJECT_VERSION(sysext CFBundleVersion,见头部说明)──
|
||||||
|
PBXPROJ="${REPO_ROOT}/client/macos/Runner.xcodeproj/project.pbxproj"
|
||||||
|
echo "==> compile-macos: bumping CURRENT_PROJECT_VERSION -> ${BUILD} in project.pbxproj"
|
||||||
|
sed -i '' -E "s/CURRENT_PROJECT_VERSION = [0-9]+;/CURRENT_PROJECT_VERSION = ${BUILD};/g" "$PBXPROJ"
|
||||||
|
# 同步 pubspec.yaml(主 app 自身的 FLUTTER_BUILD_NUMBER,和 android/windows 脚本一致)。
|
||||||
|
sed -i '' "s/^version:.*/version: ${VER}+${BUILD}/" "${REPO_ROOT}/client/pubspec.yaml"
|
||||||
|
|
||||||
|
# ── [3/7] 解码 2 个 Developer ID 描述文件到 local_test.sh 同款目录 ──────────
|
||||||
|
printf '%s' "$MACOS_APP_PROVISION_PROFILE_BASE64" | base64 --decode > "${WORK}/app.provisionprofile"
|
||||||
|
printf '%s' "$MACOS_SYSEXT_PROVISION_PROFILE_BASE64" | base64 --decode > "${WORK}/sysext.provisionprofile"
|
||||||
|
|
||||||
|
# 提取 UUID 用来落盘为规范文件名(Xcode/xcodebuild 靠内容匹配,文件名本身不
|
||||||
|
# 强制要求,但用 UUID 命名是 Xcode 自身导入时的惯例,避免与其他描述文件重名冲突)。
|
||||||
|
security cms -D -i "${WORK}/app.provisionprofile" > "${WORK}/app_profile.plist"
|
||||||
|
/usr/libexec/PlistBuddy -c 'Print :UUID' "${WORK}/app_profile.plist" > "${WORK}/app_uuid.txt"
|
||||||
|
APP_PROFILE_UUID=""
|
||||||
|
read -r APP_PROFILE_UUID < "${WORK}/app_uuid.txt" || true
|
||||||
|
APP_PROFILE="${PROF_DIR}/${APP_PROFILE_UUID}.provisionprofile"
|
||||||
|
cp "${WORK}/app.provisionprofile" "$APP_PROFILE"
|
||||||
|
|
||||||
|
security cms -D -i "${WORK}/sysext.provisionprofile" > "${WORK}/sysext_profile.plist"
|
||||||
|
/usr/libexec/PlistBuddy -c 'Print :UUID' "${WORK}/sysext_profile.plist" > "${WORK}/sysext_uuid.txt"
|
||||||
|
SYSEXT_PROFILE_UUID=""
|
||||||
|
read -r SYSEXT_PROFILE_UUID < "${WORK}/sysext_uuid.txt" || true
|
||||||
|
SYSEXT_PROFILE="${PROF_DIR}/${SYSEXT_PROFILE_UUID}.provisionprofile"
|
||||||
|
cp "${WORK}/sysext.provisionprofile" "$SYSEXT_PROFILE"
|
||||||
|
|
||||||
|
echo "==> compile-macos: profiles installed to '${PROF_DIR}' (app=${APP_PROFILE_UUID}, sysext=${SYSEXT_PROFILE_UUID})"
|
||||||
|
|
||||||
|
# ── [4/7] 临时 keychain 导入 Developer ID Application 证书 ──────────────────
|
||||||
|
KEYCHAIN_PWD="ci-temp-$$"
|
||||||
|
security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN"
|
||||||
|
security set-keychain-settings -lut 21600 "$KEYCHAIN"
|
||||||
|
security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN"
|
||||||
|
printf '%s' "$MACOS_DEVELOPER_ID_CERT_P12_BASE64" | base64 --decode > "${WORK}/devid.p12"
|
||||||
|
security import "${WORK}/devid.p12" -k "$KEYCHAIN" \
|
||||||
|
-P "$MACOS_DEVELOPER_ID_CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
|
||||||
|
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PWD" "$KEYCHAIN" >/dev/null
|
||||||
|
security list-keychains -d user -s "$KEYCHAIN" login.keychain-db
|
||||||
|
|
||||||
|
security find-identity -v -p codesigning "$KEYCHAIN" > "${WORK}/identities.txt"
|
||||||
|
grep "$SIGN_ID_PREFIX" "${WORK}/identities.txt" | head -1 | sed -E 's/.*"(.*)"/\1/' > "${WORK}/identity.txt"
|
||||||
|
IDENTITY=""
|
||||||
|
read -r IDENTITY < "${WORK}/identity.txt" || true
|
||||||
|
if [ -z "$IDENTITY" ]; then
|
||||||
|
echo "ERROR: '${SIGN_ID_PREFIX}' identity not found in imported keychain" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "==> compile-macos: signing identity '${IDENTITY}'"
|
||||||
|
|
||||||
|
# ── [5/7] flutter build macos --release ─────────────────────────────────────
|
||||||
|
# 项目已配 CODE_SIGN_STYLE=Manual + PROVISIONING_PROFILE_SPECIFIER(见头部说
|
||||||
|
# 明),本步应已产出 Developer ID 签名 + hardened runtime 的 .app;下一步的
|
||||||
|
# inside-out 重签是幂等的安全网,不依赖这一步是否已经"恰好签对"。
|
||||||
|
cd "${REPO_ROOT}/client"
|
||||||
|
flutter build macos --release --dart-define="PANGOLIN_API_URL=${API_URL}"
|
||||||
|
cd "$REPO_ROOT"
|
||||||
|
|
||||||
|
APP="${REPO_ROOT}/client/build/macos/Build/Products/Release/pangolin_vpn.app"
|
||||||
|
SE="${APP}/Contents/Library/SystemExtensions/${SYSEXT_BUNDLE_ID}.systemextension"
|
||||||
|
[ -d "$APP" ] || { echo "ERROR: build product not found: ${APP}" >&2; exit 1; }
|
||||||
|
[ -d "$SE" ] || { echo "ERROR: sysext bundle not found: ${SE}" >&2; exit 1; }
|
||||||
|
|
||||||
|
# ── [6/7] inside-out 重签(sysext → app frameworks → app),同 local_test.sh cmd_sign ──
|
||||||
|
cat > "${WORK}/app.entitlements" <<PLIST
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0"><dict>
|
||||||
|
<key>com.apple.application-identifier</key><string>${TEAM_ID}.${APP_BUNDLE_ID}</string>
|
||||||
|
<key>com.apple.developer.team-identifier</key><string>${TEAM_ID}</string>
|
||||||
|
<key>com.apple.developer.system-extension.install</key><true/>
|
||||||
|
<key>com.apple.developer.networking.networkextension</key>
|
||||||
|
<array><string>packet-tunnel-provider-systemextension</string></array>
|
||||||
|
<key>com.apple.security.app-sandbox</key><false/>
|
||||||
|
<key>com.apple.security.network.client</key><true/>
|
||||||
|
<key>com.apple.security.network.server</key><true/>
|
||||||
|
<key>keychain-access-groups</key>
|
||||||
|
<array><string>${APP_GROUP}</string></array>
|
||||||
|
</dict></plist>
|
||||||
|
PLIST
|
||||||
|
cat > "${WORK}/sysext.entitlements" <<PLIST
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0"><dict>
|
||||||
|
<key>com.apple.application-identifier</key><string>${TEAM_ID}.${SYSEXT_BUNDLE_ID}</string>
|
||||||
|
<key>com.apple.developer.team-identifier</key><string>${TEAM_ID}</string>
|
||||||
|
<key>com.apple.developer.networking.networkextension</key>
|
||||||
|
<array><string>packet-tunnel-provider-systemextension</string></array>
|
||||||
|
<key>com.apple.security.app-sandbox</key><true/>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array><string>group.com.pangolin.pangolin</string></array>
|
||||||
|
</dict></plist>
|
||||||
|
PLIST
|
||||||
|
|
||||||
|
echo "==> compile-macos: inside-out codesign (sysext -> frameworks -> app)"
|
||||||
|
xattr -cr "$APP"
|
||||||
|
cp "$SYSEXT_PROFILE" "${SE}/Contents/embedded.provisionprofile"
|
||||||
|
cp "$APP_PROFILE" "${APP}/Contents/embedded.provisionprofile"
|
||||||
|
|
||||||
|
# sysext first — libbox is linked (not embedded) into the sysext binary
|
||||||
|
# itself (see CLAUDE.md), so signing the sysext bundle re-covers its statically
|
||||||
|
# linked libbox; no separate Libbox.framework to sign inside it.
|
||||||
|
codesign --force --options runtime --timestamp \
|
||||||
|
--entitlements "${WORK}/sysext.entitlements" --sign "$IDENTITY" "$SE"
|
||||||
|
|
||||||
|
if [ -d "${APP}/Contents/Frameworks" ]; then
|
||||||
|
for item in "${APP}/Contents/Frameworks/"*; do
|
||||||
|
[ -e "$item" ] && codesign --force --options runtime --timestamp --sign "$IDENTITY" "$item"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
codesign --force --options runtime --timestamp \
|
||||||
|
--entitlements "${WORK}/app.entitlements" --sign "$IDENTITY" "$APP"
|
||||||
|
|
||||||
|
codesign --verify --deep --strict --verbose=2 "$APP"
|
||||||
|
echo "==> compile-macos: signed + verified (${IDENTITY})"
|
||||||
|
|
||||||
|
# ── [7/7] 公证(notarytool,API key 三件套) + staple + 打包 ──────────────────
|
||||||
|
echo "==> compile-macos: notarizing (notarytool submit --wait, ~1-5min)"
|
||||||
|
NOTARIZE_ZIP="${WORK}/notarize.zip"
|
||||||
|
ditto -c -k --keepParent "$APP" "$NOTARIZE_ZIP"
|
||||||
|
printf '%s' "$APPSTORE_API_KEY_P8_BASE64" | base64 --decode > "${WORK}/AuthKey.p8"
|
||||||
|
xcrun notarytool submit "$NOTARIZE_ZIP" \
|
||||||
|
--key "${WORK}/AuthKey.p8" \
|
||||||
|
--key-id "$APPSTORE_API_KEY_ID" \
|
||||||
|
--issuer "$APPSTORE_API_ISSUER_ID" \
|
||||||
|
--wait
|
||||||
|
|
||||||
|
xcrun stapler staple "$APP"
|
||||||
|
xcrun stapler validate "$APP"
|
||||||
|
spctl -a -vvv -t install "$APP" || true # 期望 source=Notarized Developer ID
|
||||||
|
echo "==> compile-macos: notarized + stapled"
|
||||||
|
|
||||||
|
mkdir -p "${REPO_ROOT}/dist"
|
||||||
|
ditto -c -k --keepParent "$APP" "${REPO_ROOT}/dist/pangolin-macos-x64.zip"
|
||||||
|
|
||||||
|
echo "==> compile-macos: done — dist/ contents:"
|
||||||
|
ls -lh "${REPO_ROOT}/dist/"
|
||||||
@@ -47,7 +47,7 @@ if ! [[ "$TAG" =~ ^client-v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?$ ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f dist/pangolin-android.apk ] && [ ! -f dist/pangolin-windows-x64-setup.exe ]; then
|
if [ ! -f dist/pangolin-android.apk ] && [ ! -f dist/pangolin-windows-x64-setup.exe ] && [ ! -f dist/pangolin-macos-x64.zip ]; then
|
||||||
echo "deploy-client: no artifacts found in dist/ — nothing to deploy" >&2
|
echo "deploy-client: no artifacts found in dist/ — nothing to deploy" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
@@ -70,7 +70,13 @@ if [ -f dist/pangolin-windows-x64-setup.exe ]; then
|
|||||||
$SSH "root@${DEPLOY_HOST}" "rm -f ${DOWNLOADS_DIR}/pangolin-windows-x64-setup.exe && mv /tmp/pangolin-windows-x64-setup.exe.new ${DOWNLOADS_DIR}/pangolin-windows-x64-setup.exe && chmod 644 ${DOWNLOADS_DIR}/pangolin-windows-x64-setup.exe"
|
$SSH "root@${DEPLOY_HOST}" "rm -f ${DOWNLOADS_DIR}/pangolin-windows-x64-setup.exe && mv /tmp/pangolin-windows-x64-setup.exe.new ${DOWNLOADS_DIR}/pangolin-windows-x64-setup.exe && chmod 644 ${DOWNLOADS_DIR}/pangolin-windows-x64-setup.exe"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# TODO(controller): macOS artifact not yet produced by this draft — add the
|
if [ -f dist/pangolin-macos-x64.zip ]; then
|
||||||
# same guarded upload+swap once compile-macos.sh lands (Phase 3).
|
echo "==> deploy-client: uploading pangolin-macos-x64.zip"
|
||||||
|
$SCP dist/pangolin-macos-x64.zip "root@${DEPLOY_HOST}:/tmp/pangolin-macos-x64.zip.new"
|
||||||
|
$SSH "root@${DEPLOY_HOST}" "rm -f ${DOWNLOADS_DIR}/pangolin-macos-x64.zip && mv /tmp/pangolin-macos-x64.zip.new ${DOWNLOADS_DIR}/pangolin-macos-x64.zip && chmod 644 ${DOWNLOADS_DIR}/pangolin-macos-x64.zip"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# iOS has no dist/ artifact to deploy here — compile-ios.sh uploads straight
|
||||||
|
# to TestFlight via altool (matches jiu).
|
||||||
|
|
||||||
echo "==> deploy-client: done"
|
echo "==> deploy-client: done"
|
||||||
|
|||||||
@@ -1,14 +1,27 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# release-client.sh <tag> — ensure the Forgejo release for a client-v* tag
|
# release-client.sh <tag> — ensure the Forgejo release for a client-v* tag
|
||||||
# exists and upload whichever client artifacts are present in dist/.
|
# exists, upload whichever client artifacts are present in dist/, and roll
|
||||||
|
# the auto-update manifest (deploy/single-node/version.yaml) forward.
|
||||||
#
|
#
|
||||||
# Mirrors ~/code/jiu/scripts/ci/release-client.sh's asset-guarding pattern,
|
# Mirrors ~/code/jiu/scripts/ci/release-client.sh's asset-guarding pattern,
|
||||||
# but uses pangolin's own lib-forgejo.sh API (forgejo_release_ensure /
|
# but uses pangolin's own lib-forgejo.sh API (forgejo_release_ensure /
|
||||||
# forgejo_upload_asset — different names/signature than jiu's create_release/
|
# forgejo_upload_asset — different names/signature than jiu's create_release/
|
||||||
# upload_asset) and drops jiu's version.yaml/CHANGELOG bookkeeping entirely:
|
# upload_asset).
|
||||||
# pangolin has no equivalent backend/config/version.yaml manifest — client
|
#
|
||||||
# self-update checking is out of scope here (server just serves the release
|
# Manifest handling differs from jiu on purpose: jiu's backend reads
|
||||||
# assets; see deploy-client.sh).
|
# backend/config/version.yaml straight out of its own working directory, so
|
||||||
|
# rewriting that file in the repo checkout is enough. Pangolin's server reads
|
||||||
|
# VERSION_MANIFEST from /etc/pangolin (see server/internal/httpapi/version.go
|
||||||
|
# + deploy/single-node/deploy.sh), a path that lives on pangolin1, not in any
|
||||||
|
# git checkout — so getting the new version/build_number onto the live host
|
||||||
|
# needs an SSH push, the same DEPLOY_SSH_KEY / lib-ssh.sh path
|
||||||
|
# deploy-client.sh already uses for the downloads/ dir (see
|
||||||
|
# .gitea/workflows/deploy-client.yml, which now passes DEPLOY_SSH_KEY into
|
||||||
|
# this step too). Also unlike jiu, download_urls are fixed "latest" stable
|
||||||
|
# URLs (deploy-client.sh keeps only one file per platform) and changelog[]
|
||||||
|
# isn't populated yet (no CHANGELOG-client.md in this repo) — so only
|
||||||
|
# version/build_number are rewritten here, everything else in the manifest
|
||||||
|
# template is passed through unchanged.
|
||||||
#
|
#
|
||||||
# Multiple platform build jobs (android/windows/...) all upload into the SAME
|
# Multiple platform build jobs (android/windows/...) all upload into the SAME
|
||||||
# release, guarded by `[ -f ... ]` since a given CI run may only have built a
|
# release, guarded by `[ -f ... ]` since a given CI run may only have built a
|
||||||
@@ -16,10 +29,13 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||||
# shellcheck source=scripts/ci/_env.sh
|
# shellcheck source=scripts/ci/_env.sh
|
||||||
. "${SCRIPT_DIR}/_env.sh"
|
. "${SCRIPT_DIR}/_env.sh"
|
||||||
# shellcheck source=scripts/ci/lib-forgejo.sh
|
# shellcheck source=scripts/ci/lib-forgejo.sh
|
||||||
. "${SCRIPT_DIR}/lib-forgejo.sh"
|
. "${SCRIPT_DIR}/lib-forgejo.sh"
|
||||||
|
# shellcheck source=scripts/ci/lib-ssh.sh
|
||||||
|
. "${SCRIPT_DIR}/lib-ssh.sh"
|
||||||
|
|
||||||
TAG="${1:?usage: release-client.sh <tag>}"
|
TAG="${1:?usage: release-client.sh <tag>}"
|
||||||
|
|
||||||
@@ -41,12 +57,59 @@ if [ -f dist/pangolin-windows-x64-setup.exe ]; then
|
|||||||
forgejo_upload_asset "$TAG" dist/pangolin-windows-x64-setup.exe
|
forgejo_upload_asset "$TAG" dist/pangolin-windows-x64-setup.exe
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# TODO(controller): macOS artifact not yet built by any compile-*.sh in this
|
if [ -f dist/pangolin-macos-x64.zip ]; then
|
||||||
# draft (Phase 3 / Task 9 of docs/superpowers/plans/2026-07-05-cicd.md is out
|
forgejo_upload_asset "$TAG" dist/pangolin-macos-x64.zip
|
||||||
# of scope for this task). Once compile-macos.sh lands, add here:
|
fi
|
||||||
# if [ -f dist/pangolin-macos-x64.zip ]; then
|
|
||||||
# forgejo_upload_asset "$TAG" dist/pangolin-macos-x64.zip
|
# iOS has no dist/ artifact — compile-ios.sh uploads straight to TestFlight
|
||||||
# fi
|
# via altool (matches jiu), nothing to attach to the Forgejo release here.
|
||||||
|
|
||||||
|
# ── Auto-update manifest: bump version/build_number, stage + push to pangolin1 ──
|
||||||
|
# BUILD is derived the exact same way compile-android.sh derives the APK's
|
||||||
|
# Android versionCode (major*10000 + minor*100 + patch) so the manifest's
|
||||||
|
# build_number stays consistent with the shipped APK for a given tag, rather
|
||||||
|
# than being an independently-incremented counter like jiu's.
|
||||||
|
MAJOR="${VER%%.*}"
|
||||||
|
_REST="${VER#*.}"
|
||||||
|
MINOR="${_REST%%.*}"
|
||||||
|
PATCH="${_REST#*.}"
|
||||||
|
PATCH="${PATCH%%-*}"
|
||||||
|
BUILD=$(( MAJOR * 10000 + MINOR * 100 + PATCH ))
|
||||||
|
|
||||||
|
MANIFEST_TMPL="${REPO_ROOT}/deploy/single-node/version.yaml"
|
||||||
|
MANIFEST_OUT="/tmp/pangolin_version_manifest.$$.yaml"
|
||||||
|
|
||||||
|
if [ -f "$MANIFEST_TMPL" ]; then
|
||||||
|
: > "$MANIFEST_OUT"
|
||||||
|
while IFS= read -r line || [ -n "$line" ]; do
|
||||||
|
case "$line" in
|
||||||
|
version:*) printf 'version: "%s"\n' "$VER" >> "$MANIFEST_OUT" ;;
|
||||||
|
build_number:*) printf 'build_number: %d\n' "$BUILD" >> "$MANIFEST_OUT" ;;
|
||||||
|
*) printf '%s\n' "$line" >> "$MANIFEST_OUT" ;;
|
||||||
|
esac
|
||||||
|
done < "$MANIFEST_TMPL"
|
||||||
|
|
||||||
|
mkdir -p dist
|
||||||
|
cp "$MANIFEST_OUT" dist/version.yaml
|
||||||
|
# Stage as a release asset too, so a manual rollback can re-fetch the exact
|
||||||
|
# manifest that shipped alongside this tag (mirrors jiu's rationale).
|
||||||
|
forgejo_upload_asset "$TAG" dist/version.yaml
|
||||||
|
|
||||||
|
if [ -n "${DEPLOY_SSH_KEY:-}" ]; then
|
||||||
|
echo "==> release-client: pushing version.yaml (version=${VER} build_number=${BUILD}) to pangolin1"
|
||||||
|
setup_ssh
|
||||||
|
SCP="scp -i ${SSH_KEY_FILE} -P ${DEPLOY_PORT} -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=${SSH_KNOWN_HOSTS_FILE}"
|
||||||
|
$SSH "root@${DEPLOY_HOST}" "mkdir -p /etc/pangolin"
|
||||||
|
$SCP "$MANIFEST_OUT" "root@${DEPLOY_HOST}:/tmp/pangolin_version.yaml.new"
|
||||||
|
$SSH "root@${DEPLOY_HOST}" "mv /tmp/pangolin_version.yaml.new /etc/pangolin/version.yaml && chown pangolin:pangolin /etc/pangolin/version.yaml && chmod 644 /etc/pangolin/version.yaml"
|
||||||
|
echo "==> release-client: version.yaml deployed to pangolin1 (/etc/pangolin/version.yaml)"
|
||||||
|
else
|
||||||
|
echo "==> release-client: DEPLOY_SSH_KEY not set — skipping live manifest push (dist/version.yaml still staged + uploaded as a release asset)"
|
||||||
|
fi
|
||||||
|
rm -f "$MANIFEST_OUT"
|
||||||
|
else
|
||||||
|
echo "==> release-client: WARN manifest template ${MANIFEST_TMPL} not found — skipping version.yaml update" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
echo "==> release-client: done — Release ${TAG} updated. dist/ contents:"
|
echo "==> release-client: done — Release ${TAG} updated. dist/ contents:"
|
||||||
ls -lh dist/ 2>/dev/null || true
|
ls -lh dist/ 2>/dev/null || true
|
||||||
|
|||||||
Reference in New Issue
Block a user