NewLife/Stardust

ci: iOS 冒烟支持冷启动模式,修复无服务端时构建流水线失败

CI 没有 StarServer,iOS 冒烟以 --smoke 启动后必须登录并心跳成功,连接被拒即写入 ok=false,使 build-mobile 一直失败。

iOS 代理新增 --start 冷启动验证:启动代理约 3 秒后写出 agent-start.json(started/server/error),只要求实例起来,不要求连上服务端;run-ios.sh 增加 MODE=smoke|start 切换,构建流水线改用 MODE=start,与 Android 的 -Start 一致。
大石头 authored at 2026-10-24 19:23:07
178063b
Tree
1 Parent(s) 3fc9fd4
Summary: 3 changed files with 71 additions and 19 deletions.
Modified +2 -1
Modified +36 -10
Modified +33 -8
Modified +2 -1
diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml
index f19863b..282e5b3 100644
--- a/.github/workflows/build-mobile.yml
+++ b/.github/workflows/build-mobile.yml
@@ -111,11 +111,12 @@ jobs:
     - name: Build iOS
       run: dotnet build StarAgent.iOS/StarAgent.iOS.csproj -c Release -f net10.0-ios -p:RuntimeIdentifier=iossimulator-arm64 --nologo
 
-    # NewLife.Core 12.0.2026.1024 已修复 Host 退出钩子注册异常,iOS 冒烟随构建一并阻塞
+    # CI 没有可用 StarServer,-Smoke 需要真实登录,改用 start 模式校验冷启动(与 Android 的 -Start 一致)
     - name: Run iOS smoke
       run: bash StarAgent.iOS/run-ios.sh
       env:
         SKIP_BUILD: "1"
+        MODE: start
 
     - uses: actions/upload-artifact@v4
       if: always()
Modified +36 -10
diff --git a/StarAgent.iOS/AgentRuntime.cs b/StarAgent.iOS/AgentRuntime.cs
index 3917350..332896a 100644
--- a/StarAgent.iOS/AgentRuntime.cs
+++ b/StarAgent.iOS/AgentRuntime.cs
@@ -19,6 +19,9 @@ public static class AgentRuntime
     /// <summary>是否由冒烟测试启动</summary>
     public static Boolean SmokeRequested { get; private set; }
 
+    /// <summary>是否由冷启动验证启动。只验证代理能起来,不要求连上服务端</summary>
+    public static Boolean StartRequested { get; private set; }
+
     /// <summary>设置界面日志接收器</summary>
     /// <param name="sink">日志回调</param>
     public static void SetLogSink(Action<String>? sink) => _logSink = sink;
@@ -40,7 +43,9 @@ public static class AgentRuntime
         {
             try
             {
-                SmokeRequested = NSProcessInfo.ProcessInfo.Arguments.Any(e => e.Equals("--smoke", StringComparison.OrdinalIgnoreCase));
+                var args = NSProcessInfo.ProcessInfo.Arguments;
+                SmokeRequested = args.Any(e => e.Equals("--smoke", StringComparison.OrdinalIgnoreCase));
+                StartRequested = args.Any(e => e.Equals("--start", StringComparison.OrdinalIgnoreCase));
 
                 if (_agent == null)
                 {
@@ -59,11 +64,18 @@ public static class AgentRuntime
                 _agent.Start();
 
                 if (SmokeRequested) _ = RunSmokeAsync();
+                else if (StartRequested) _ = RunStartAsync();
             }
             catch (Exception ex)
             {
                 XTrace.WriteException(ex);
                 Log("启动失败:" + ex.Message);
+
+                if (StartRequested)
+                {
+                    WriteResult("agent-start.json", "started", false, ex.Message);
+                    Log("=== Agent start fail ===");
+                }
             }
         }
     }
@@ -96,21 +108,35 @@ public static class AgentRuntime
         }
 
         var ok = await PingOnceAsync().ConfigureAwait(false);
-        var server = _agent?.Setting.Server ?? "";
-        var error = _agent?.LastError ?? "";
+        WriteResult("agent-smoke.json", "ok", ok, _agent?.LastError ?? "");
+
+        Log(ok ? "=== Agent ready ===" : "=== Agent fail ===");
+    }
+
+    /// <summary>冷启动验证。CI 没有 StarServer,只确认代理实例已启动,不要求登录成功</summary>
+    private static async Task RunStartAsync()
+    {
+        // 留出登录尝试时间,便于区分"起来了但连不上服务端"与"根本没起来"
+        await Task.Delay(3_000).ConfigureAwait(false);
 
+        var started = _agent != null;
+        WriteResult("agent-start.json", "started", started, _agent?.LastError ?? "");
+
+        Log(started ? "=== Agent start ===" : "=== Agent start fail ===");
+    }
+
+    private static void WriteResult(String fileName, String flagName, Boolean ok, String error)
+    {
         try
         {
             var dir = _platform?.GetDataPath();
-            if (!dir.IsNullOrEmpty())
-            {
-                var json = $"{{\"ok\":{(ok ? "true" : "false")},\"server\":\"{Escape(server)}\",\"error\":\"{Escape(error)}\",\"time\":\"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\"}}";
-                File.WriteAllText(Path.Combine(dir, "agent-smoke.json"), json);
-            }
+            if (dir.IsNullOrEmpty()) return;
+
+            var server = _agent?.Setting.Server ?? "";
+            var json = $"{{\"{flagName}\":{(ok ? "true" : "false")},\"server\":\"{Escape(server)}\",\"error\":\"{Escape(error)}\",\"time\":\"{DateTime.Now:yyyy-MM-dd HH:mm:ss}\"}}";
+            File.WriteAllText(Path.Combine(dir, fileName), json);
         }
         catch { }
-
-        Log(ok ? "=== Agent ready ===" : "=== Agent fail ===");
     }
 
     private static String Escape(String value) => (value ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"");
Modified +33 -8
diff --git a/StarAgent.iOS/run-ios.sh b/StarAgent.iOS/run-ios.sh
index 4c3314c..b776560 100644
--- a/StarAgent.iOS/run-ios.sh
+++ b/StarAgent.iOS/run-ios.sh
@@ -7,8 +7,29 @@ CONFIGURATION="${CONFIGURATION:-Release}"
 RUNTIME="${RUNTIME:-iossimulator-arm64}"
 BUNDLE_ID="com.newlife.staragent"
 TIMEOUT="${TIMEOUT:-180}"
+# smoke:等待登录并心跳成功,需要可用的 StarServer;start:只验证冷启动,CI 无服务端时使用
+MODE="${MODE:-smoke}"
 ARTIFACTS="$ROOT/Bin/Agent/mobile/ios/artifacts"
 
+case "$MODE" in
+  smoke)
+    APP_ARG="--smoke"
+    RESULT_NAME="agent-smoke.json"
+    OK_PATTERN='"ok":true'
+    PASS_LABEL="iOS smoke passed."
+    ;;
+  start)
+    APP_ARG="--start"
+    RESULT_NAME="agent-start.json"
+    OK_PATTERN='"started":true'
+    PASS_LABEL="iOS start smoke passed."
+    ;;
+  *)
+    echo "Unknown MODE: $MODE (expected smoke|start)"
+    exit 2
+    ;;
+esac
+
 mkdir -p "$ARTIFACTS"
 
 if [ "${SKIP_BUILD:-0}" != "1" ]; then
@@ -27,19 +48,19 @@ if [ -z "$UDID" ]; then
   exit 1
 fi
 
-echo "Using simulator $UDID"
+echo "Using simulator $UDID (mode=$MODE)"
 xcrun simctl boot "$UDID" 2>/dev/null || true
 xcrun simctl bootstatus "$UDID" -b
 xcrun simctl install "$UDID" "$APP"
 xcrun simctl terminate "$UDID" "$BUNDLE_ID" 2>/dev/null || true
-xcrun simctl launch "$UDID" "$BUNDLE_ID" --smoke
+xcrun simctl launch "$UDID" "$BUNDLE_ID" "$APP_ARG"
 
 RESULT=""
 DEADLINE=$((SECONDS + TIMEOUT))
 while [ "$SECONDS" -lt "$DEADLINE" ]; do
   DATA_DIR="$(xcrun simctl get_app_container "$UDID" "$BUNDLE_ID" data 2>/dev/null || true)"
   if [ -n "$DATA_DIR" ]; then
-    RESULT="$(find "$DATA_DIR" -name agent-smoke.json -print -quit 2>/dev/null || true)"
+    RESULT="$(find "$DATA_DIR" -name "$RESULT_NAME" -print -quit 2>/dev/null || true)"
     if [ -n "$RESULT" ]; then
       break
     fi
@@ -48,18 +69,22 @@ while [ "$SECONDS" -lt "$DEADLINE" ]; do
 done
 
 if [ -z "$RESULT" ]; then
-  echo "agent-smoke.json not found within ${TIMEOUT}s. Recent app log:"
+  echo "$RESULT_NAME not found within ${TIMEOUT}s. Recent app log:"
   xcrun simctl spawn "$UDID" log show --last 2m --style compact --predicate 'process == "StarAgent.iOS"' || true
   exit 1
 fi
 
-cp "$RESULT" "$ARTIFACTS/ios-smoke.json"
+cp "$RESULT" "$ARTIFACTS/ios-$MODE.json"
 cat "$RESULT"
 
 if command -v jq >/dev/null 2>&1; then
-  jq -e '.ok == true' "$RESULT" >/dev/null
+  if [ "$MODE" = "start" ]; then
+    jq -e '.started == true' "$RESULT" >/dev/null
+  else
+    jq -e '.ok == true' "$RESULT" >/dev/null
+  fi
 else
-  grep -q '"ok":true' "$RESULT"
+  grep -q "$OK_PATTERN" "$RESULT"
 fi
 
-echo "iOS smoke passed."
+echo "$PASS_LABEL"
\ No newline at end of file