NewLife/NewLife.Remoting

ci: 加固移动端冒烟并固定 iOS workload

- iOS 固定 .NET SDK 10.0.100 并用 SDK 自带 iOS workload(--skip-manifest-update),
  避免 workload 更新到 27.1 后要求 runner 尚未提供的 Xcode 27.1
- Android 超时或失败时保存完整 logcat 并随 APK 一起上传,便于定位启动崩溃
- smoke.json 写入失败不再吞掉结论,Android/iOS 入口补 SMOKE_START/SMOKE_FAIL 日志
大石头 authored at 2026-10-24 13:17:12
080aa94
Tree
1 Parent(s) deb8798
Summary: 5 changed files with 52 additions and 15 deletions.
Modified +7 -4
Modified +10 -2
Modified +13 -5
Modified +5 -0
Modified +17 -4
Modified +7 -4
diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml
index bae9f14..8780850 100644
--- a/.github/workflows/mobile.yml
+++ b/.github/workflows/mobile.yml
@@ -50,7 +50,9 @@ jobs:
       uses: actions/upload-artifact@v4
       with:
         name: mobile-android
-        path: Bin/MobileSmoke/**/*.apk
+        path: |
+          Bin/MobileSmoke/**/*.apk
+          Bin/MobileSmoke/artifacts/**
         if-no-files-found: ignore
 
   # iOS 模拟器:构建 .app 并安装运行,读取应用容器内的 smoke.json。
@@ -65,12 +67,13 @@ jobs:
     - name: Setup dotNET
       uses: actions/setup-dotnet@v4
       with:
-        dotnet-version: 10.0.x
+        # macos-26 默认 Xcode 26.6;固定 10.0.100 并使用 SDK 自带 iOS workload(26.5),
+        # 避免更新到 27.1 后要求尚未提供的 Xcode 27.1
+        dotnet-version: 10.0.100
     - name: Show Xcode
-      # 不再硬切到指定 Xcode:iOS workload 与镜像默认 Xcode 是配套的,强行切换容易版本不匹配导致构建失败
       run: xcodebuild -version
     - name: Install iOS workload
-      run: dotnet workload install ios
+      run: dotnet workload install ios --skip-manifest-update
     - name: Run iOS smoke
       run: bash ./Samples/MobileSmoke/run-ios.sh
     - name: Upload iOS artifacts
Modified +10 -2
diff --git a/Samples/MobileSmoke/AppDelegate.cs b/Samples/MobileSmoke/AppDelegate.cs
index 99e7c3c..606eaca 100644
--- a/Samples/MobileSmoke/AppDelegate.cs
+++ b/Samples/MobileSmoke/AppDelegate.cs
@@ -22,8 +22,16 @@ public class AppDelegate : UIApplicationDelegate
     /// <returns>是否继续启动</returns>
     public override Boolean FinishedLaunching(UIApplication application, NSDictionary? launchOptions)
     {
-        var (pass, fail, _) = Smoke.Run();
-        Console.WriteLine(fail == 0 ? $"SMOKE_OK pass={pass}" : $"SMOKE_FAIL pass={pass} fail={fail}");
+        Console.WriteLine("SMOKE_START");
+        try
+        {
+            var (pass, fail, _) = Smoke.Run();
+            Console.WriteLine(fail == 0 ? $"SMOKE_OK pass={pass}" : $"SMOKE_FAIL pass={pass} fail={fail}");
+        }
+        catch (Exception ex)
+        {
+            Console.WriteLine($"SMOKE_FAIL {ex}");
+        }
         return true;
     }
 }
Modified +13 -5
diff --git a/Samples/MobileSmoke/MainActivity.cs b/Samples/MobileSmoke/MainActivity.cs
index 4469d8d..427ee2b 100644
--- a/Samples/MobileSmoke/MainActivity.cs
+++ b/Samples/MobileSmoke/MainActivity.cs
@@ -15,11 +15,19 @@ public class MainActivity : Activity
     {
         base.OnCreate(savedInstanceState);
 
-        var (pass, fail, _) = Smoke.Run();
-        if (fail == 0)
-            Log.Info("MobileSmoke", $"SMOKE_OK pass={pass}");
-        else
-            Log.Error("MobileSmoke", $"SMOKE_FAIL pass={pass} fail={fail}");
+        Log.Info("MobileSmoke", "SMOKE_START");
+        try
+        {
+            var (pass, fail, _) = Smoke.Run();
+            if (fail == 0)
+                Log.Info("MobileSmoke", $"SMOKE_OK pass={pass}");
+            else
+                Log.Error("MobileSmoke", $"SMOKE_FAIL pass={pass} fail={fail}");
+        }
+        catch (Exception ex)
+        {
+            Log.Error("MobileSmoke", $"SMOKE_FAIL {ex}");
+        }
     }
 }
 #endif
Modified +5 -0
diff --git a/Samples/MobileSmoke/run-android.ps1 b/Samples/MobileSmoke/run-android.ps1
index 38ec724..5ccda4f 100644
--- a/Samples/MobileSmoke/run-android.ps1
+++ b/Samples/MobileSmoke/run-android.ps1
@@ -8,6 +8,8 @@ $ErrorActionPreference = "Stop"
 $root = Resolve-Path "$PSScriptRoot/../.."
 $project = Join-Path $PSScriptRoot "MobileSmoke.csproj"
 $package = "com.newlife.smoke.remoting"
+$artifacts = Join-Path $root "Bin/MobileSmoke/artifacts"
+New-Item -ItemType Directory -Force -Path $artifacts | Out-Null
 
 # 项目是 android+ios 双目标,必须把目标框架收窄到单一平台;
 # 否则在 Linux/macOS 上只跑一端时,还原期仍会解析另一平台的 workload,缺失时直接 NETSDK1178
@@ -33,11 +35,14 @@ while ((Get-Date) -lt $deadline) {
     if ($hit) {
         Write-Host $hit.Line
         if ($hit.Line -match "SMOKE_OK") { exit 0 }
+        adb logcat -d | Out-File (Join-Path $artifacts "android-logcat.txt") -Encoding utf8
         adb logcat -d -s MobileSmoke:* | Out-Host
         exit 1
     }
     Start-Sleep -Seconds 2
 }
 
+# 超时多为应用启动失败或崩溃;完整 logcat 比过滤标签更能定位根因
+adb logcat -d | Out-File (Join-Path $artifacts "android-logcat.txt") -Encoding utf8
 adb logcat -d -s MobileSmoke:* | Out-Host
 throw "Android smoke timed out"
Modified +17 -4
diff --git a/Samples/MobileSmoke/Smoke.cs b/Samples/MobileSmoke/Smoke.cs
index f9f92b2..83b3b1c 100644
--- a/Samples/MobileSmoke/Smoke.cs
+++ b/Samples/MobileSmoke/Smoke.cs
@@ -35,9 +35,6 @@ public static class Smoke
             if (CountTypes(asm) == 0) throw new Exception("no exported types");
         });
         Check("ApiClient ctor", () => { using var client = new NewLife.Remoting.ApiClient("http://127.0.0.1:5000"); });
-        var dir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
-        Directory.CreateDirectory(dir);
-        var file = System.IO.Path.Combine(dir, "smoke.json");
         var json = JsonSerializer.Serialize(new
         {
             pass,
@@ -46,7 +43,23 @@ public static class Smoke
             os = System.Runtime.InteropServices.RuntimeInformation.OSDescription,
             log,
         });
-        File.WriteAllText(file, json);
+
+        // 结果文件写入失败不应吞掉冒烟结论;记录失败原因后照常返回,Android 仍可通过 logcat 判定
+        var file = String.Empty;
+        try
+        {
+            var dir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
+            if (String.IsNullOrEmpty(dir)) dir = System.IO.Path.GetTempPath();
+            Directory.CreateDirectory(dir);
+            file = System.IO.Path.Combine(dir, "smoke.json");
+            File.WriteAllText(file, json);
+        }
+        catch (Exception ex)
+        {
+            log.Add($"FAIL write smoke.json: {ex.GetType().Name}: {ex.Message}");
+            fail++;
+        }
+
         return (pass, fail, file);
     }