From b5f94890e99ab479e8e2ed8976eb837ea7049493 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 12:52:22 +0300 Subject: [PATCH 01/51] add E2E test infrastructure for Android and iOS - Unity test-app with QA scene, scripts and logging (AFQALogger, QATestScript) - Android: custom activity for deep link onNewIntent, mainTemplate.gradle, unitywrapper.aar, AndroidManifest with singleTask launch mode - iOS: UnityAppControllerDeepLink for deep link handling, AppsFlyerOpenURL.mm - .af-e2e/test-plan.json: 6-phase E2E plan covering cold launch, deep links, custom events, identity APIs, and SDK stop/start (38 checks) - scripts/: af-scenario-runner.sh, bump-version.sh, ios-pod-install.sh - .github/workflows/: rc-e2e-android, rc-e2e-ios, rc-release CI workflows - .claude/: project agents and skills for Unity plugin and E2E testing - CLAUDE.md: project architecture and workflow documentation - .gitignore: exclude reports, build artifacts and per-developer settings - test-app/ProjectSettings/: full Unity project settings for CI builds Co-Authored-By: Claude Sonnet 4.6 --- .af-e2e/test-plan.json | 390 +++++++ .claude/agents/unity-e2e-tester.md | 157 +++ .claude/agents/unity-plugin-developer.md | 164 +++ .claude/e2e-reports/README.md | 8 + .../appsflyer-event-validation/SKILL.md | 40 + .claude/skills/e2e-smoke-test/SKILL.md | 39 + .claude/skills/launch-log-analysis/SKILL.md | 39 + .../skills/platform-channel-debug/SKILL.md | 53 + .claude/skills/plugin-api-change/SKILL.md | 61 ++ .claude/skills/plugin-release/SKILL.md | 52 + .claude/skills/sdk-version-bump/SKILL.md | 65 ++ .github/workflows/rc-e2e-android.yml | 110 ++ .github/workflows/rc-e2e-ios.yml | 133 +++ .github/workflows/rc-release.yml | 262 +++++ .gitignore | 17 + CLAUDE.md | 72 ++ scripts/af-scenario-runner.sh | 973 ++++++++++++++++++ scripts/bump-version.sh | 114 ++ scripts/ios-pod-install.sh | 50 + test-app/Assets/Editor.meta | 8 + test-app/Assets/Editor/BuildScript.cs | 89 ++ test-app/Assets/Editor/BuildScript.cs.meta | 2 + test-app/Assets/Editor/iOSBuildPostProcess.cs | 73 ++ .../Assets/Editor/iOSBuildPostProcess.cs.meta | 2 + test-app/Assets/Plugins.meta | 8 + test-app/Assets/Plugins/Android.meta | 8 + .../Plugins/Android/AndroidManifest.xml | 29 + .../Plugins/Android/AndroidManifest.xml.meta | 7 + .../Android/AppsFlyerUnityActivity.java | 23 + .../Android/AppsFlyerUnityActivity.java.meta | 2 + .../Plugins/Android/mainTemplate.gradle | 50 + .../Plugins/Android/mainTemplate.gradle.meta | 7 + .../Assets/Plugins/Android/unitywrapper.aar | Bin 0 -> 24179 bytes .../Plugins/Android/unitywrapper.aar.meta | 2 + test-app/Assets/Scenes.meta | 8 + test-app/Assets/Scenes/QATestScene.unity | 121 +++ test-app/Assets/Scenes/QATestScene.unity.meta | 7 + test-app/Assets/Scripts.meta | 8 + test-app/Assets/Scripts/AFQALogger.cs | 28 + test-app/Assets/Scripts/AFQALogger.cs.meta | 2 + test-app/Assets/Scripts/QATestScript.cs | 263 +++++ test-app/Assets/Scripts/QATestScript.cs.meta | 2 + test-app/Assets/StreamingAssets.meta | 8 + test-app/Assets/StreamingAssets/.env.example | 3 + test-app/Assets/iOS.meta | 8 + test-app/Assets/iOS/AppsFlyerOpenURL.mm | 1 + test-app/Assets/iOS/AppsFlyerOpenURL.mm.meta | 2 + .../Assets/iOS/UnityAppControllerDeepLink.mm | 68 ++ .../iOS/UnityAppControllerDeepLink.mm.meta | 2 + test-app/Packages/manifest.json | 18 + test-app/ProjectSettings/AudioManager.asset | 23 + .../ProjectSettings/ClusterInputManager.asset | 6 + .../ProjectSettings/DynamicsManager.asset | 44 + .../ProjectSettings/EditorBuildSettings.asset | 11 + test-app/ProjectSettings/EditorSettings.asset | 50 + .../ProjectSettings/GraphicsSettings.asset | 68 ++ test-app/ProjectSettings/InputManager.asset | 296 ++++++ test-app/ProjectSettings/MemorySettings.asset | 35 + .../ProjectSettings/MultiplayerManager.asset | 9 + test-app/ProjectSettings/NavMeshAreas.asset | 93 ++ .../ProjectSettings/Physics2DSettings.asset | 57 + test-app/ProjectSettings/PresetManager.asset | 7 + .../ProjectSettings/ProjectSettings.asset | 277 +++++ test-app/ProjectSettings/ProjectVersion.txt | 2 + .../ProjectSettings/QualitySettings.asset | 347 +++++++ test-app/ProjectSettings/TagManager.asset | 45 + test-app/ProjectSettings/TimeManager.asset | 14 + .../UnityConnectSettings.asset | 40 + test-app/ProjectSettings/VFXManager.asset | 20 + .../VersionControlSettings.asset | 7 + 70 files changed, 5109 insertions(+) create mode 100644 .af-e2e/test-plan.json create mode 100644 .claude/agents/unity-e2e-tester.md create mode 100644 .claude/agents/unity-plugin-developer.md create mode 100644 .claude/e2e-reports/README.md create mode 100644 .claude/skills/appsflyer-event-validation/SKILL.md create mode 100644 .claude/skills/e2e-smoke-test/SKILL.md create mode 100644 .claude/skills/launch-log-analysis/SKILL.md create mode 100644 .claude/skills/platform-channel-debug/SKILL.md create mode 100644 .claude/skills/plugin-api-change/SKILL.md create mode 100644 .claude/skills/plugin-release/SKILL.md create mode 100644 .claude/skills/sdk-version-bump/SKILL.md create mode 100644 .github/workflows/rc-e2e-android.yml create mode 100644 .github/workflows/rc-e2e-ios.yml create mode 100644 .github/workflows/rc-release.yml create mode 100644 CLAUDE.md create mode 100755 scripts/af-scenario-runner.sh create mode 100755 scripts/bump-version.sh create mode 100755 scripts/ios-pod-install.sh create mode 100644 test-app/Assets/Editor.meta create mode 100644 test-app/Assets/Editor/BuildScript.cs create mode 100644 test-app/Assets/Editor/BuildScript.cs.meta create mode 100644 test-app/Assets/Editor/iOSBuildPostProcess.cs create mode 100644 test-app/Assets/Editor/iOSBuildPostProcess.cs.meta create mode 100644 test-app/Assets/Plugins.meta create mode 100644 test-app/Assets/Plugins/Android.meta create mode 100644 test-app/Assets/Plugins/Android/AndroidManifest.xml create mode 100644 test-app/Assets/Plugins/Android/AndroidManifest.xml.meta create mode 100644 test-app/Assets/Plugins/Android/AppsFlyerUnityActivity.java create mode 100644 test-app/Assets/Plugins/Android/AppsFlyerUnityActivity.java.meta create mode 100644 test-app/Assets/Plugins/Android/mainTemplate.gradle create mode 100644 test-app/Assets/Plugins/Android/mainTemplate.gradle.meta create mode 100644 test-app/Assets/Plugins/Android/unitywrapper.aar create mode 100644 test-app/Assets/Plugins/Android/unitywrapper.aar.meta create mode 100644 test-app/Assets/Scenes.meta create mode 100644 test-app/Assets/Scenes/QATestScene.unity create mode 100644 test-app/Assets/Scenes/QATestScene.unity.meta create mode 100644 test-app/Assets/Scripts.meta create mode 100644 test-app/Assets/Scripts/AFQALogger.cs create mode 100644 test-app/Assets/Scripts/AFQALogger.cs.meta create mode 100644 test-app/Assets/Scripts/QATestScript.cs create mode 100644 test-app/Assets/Scripts/QATestScript.cs.meta create mode 100644 test-app/Assets/StreamingAssets.meta create mode 100644 test-app/Assets/StreamingAssets/.env.example create mode 100644 test-app/Assets/iOS.meta create mode 100644 test-app/Assets/iOS/AppsFlyerOpenURL.mm create mode 100644 test-app/Assets/iOS/AppsFlyerOpenURL.mm.meta create mode 100644 test-app/Assets/iOS/UnityAppControllerDeepLink.mm create mode 100644 test-app/Assets/iOS/UnityAppControllerDeepLink.mm.meta create mode 100644 test-app/Packages/manifest.json create mode 100644 test-app/ProjectSettings/AudioManager.asset create mode 100644 test-app/ProjectSettings/ClusterInputManager.asset create mode 100644 test-app/ProjectSettings/DynamicsManager.asset create mode 100644 test-app/ProjectSettings/EditorBuildSettings.asset create mode 100644 test-app/ProjectSettings/EditorSettings.asset create mode 100644 test-app/ProjectSettings/GraphicsSettings.asset create mode 100644 test-app/ProjectSettings/InputManager.asset create mode 100644 test-app/ProjectSettings/MemorySettings.asset create mode 100644 test-app/ProjectSettings/MultiplayerManager.asset create mode 100644 test-app/ProjectSettings/NavMeshAreas.asset create mode 100644 test-app/ProjectSettings/Physics2DSettings.asset create mode 100644 test-app/ProjectSettings/PresetManager.asset create mode 100644 test-app/ProjectSettings/ProjectSettings.asset create mode 100644 test-app/ProjectSettings/ProjectVersion.txt create mode 100644 test-app/ProjectSettings/QualitySettings.asset create mode 100644 test-app/ProjectSettings/TagManager.asset create mode 100644 test-app/ProjectSettings/TimeManager.asset create mode 100644 test-app/ProjectSettings/UnityConnectSettings.asset create mode 100644 test-app/ProjectSettings/VFXManager.asset create mode 100644 test-app/ProjectSettings/VersionControlSettings.asset diff --git a/.af-e2e/test-plan.json b/.af-e2e/test-plan.json new file mode 100644 index 00000000..6f9309ba --- /dev/null +++ b/.af-e2e/test-plan.json @@ -0,0 +1,390 @@ +{ + "_meta": { + "plan_id": "unity-e2e", + "plugin": "unity", + "version": "1.0.0", + "description": "End-to-end test plan for the AppsFlyer Unity plugin. Runs against plugin source (path: ../Assets/AppsFlyer) in test-app/. Covers six scenarios: cold launch, background deep link, foreground deep link, custom event with parameters, identity APIs round-trip, and consent / SDK stop toggle. Mapped to E2E-001..E2E-006 in appsflyer-mobile-plugin-tooling/contracts/e2e-test-contract.md.", + "platforms": ["android", "ios"], + "schema_version": "1.0.0", + "tooling_contract_ref": "E2E-001, E2E-002, E2E-003, E2E-004, E2E-005, E2E-006" + }, + + "config": { + "android": { + "package_name": "com.appsflyer.engagement", + "activity": "com.unity3d.player.UnityPlayerActivity", + "apk_path": "test-app/Build/Android/com.appsflyer.engagement.apk", + "build_cmd": "echo 'Build handled by game-ci/unity-builder in CI'" + }, + "ios": { + "bundle_id": "com.appsflyer.engagement", + "app_path": "test-app/Build/iOS-Simulator/UnityQATest.app", + "build_cmd": "echo 'Build handled by game-ci/unity-builder + xcodebuild in CI'" + } + }, + + "phases": [ + { + "id": "phase_1", + "name": "Cold launch coverage", + "scenario_ref": "E2E-001", + "description": "Fresh install. Validate SDK startup, pre/post-start APIs, three auto-launched events with HTTP 200, and all standard callbacks.", + "requires_fresh_install": true, + "wait_after_launch_sec": 240, + "checks": [ + { + "id": "sdk_started", + "description": "startSDK returns SUCCESS", + "type": "log_contains", + "pattern": "[AF_QA][startSDK] result: SUCCESS", + "fail_action": "abort" + }, + { + "id": "is_first_launch_true", + "description": "onInstallConversionData fires with is_first_launch=true", + "type": "log_contains", + "pattern": "[AF_QA][CALLBACK][onInstallConversionData]", + "payload_check": {"field": "is_first_launch", "expected": "true"}, + "fail_action": "abort" + }, + { + "id": "pre_start_apis_complete", + "description": "Pre-start auto APIs ran", + "type": "log_contains", + "pattern": "[AF_QA][AUTO_APIS] --- Pre-start auto APIs complete ---", + "fail_action": "fail" + }, + { + "id": "post_start_apis_complete", + "description": "Post-start auto APIs ran", + "type": "log_contains", + "pattern": "[AF_QA][AUTO_APIS] --- Post-start auto APIs complete ---", + "fail_action": "fail" + }, + { + "id": "get_sdk_version", + "description": "getSDKVersion returns a value", + "type": "log_contains", + "pattern": "[AF_QA][getSDKVersion] result:", + "fail_action": "fail" + }, + { + "id": "get_appsflyer_uid", + "description": "getAppsFlyerUID returns a value", + "type": "log_contains", + "pattern": "[AF_QA][getAppsFlyerUID] result:", + "fail_action": "fail" + }, + { + "id": "event_af_demo_launch", + "description": "af_demo_launch event fires", + "type": "log_contains", + "pattern": "[AF_QA][logEvent(af_demo_launch)] result:", + "fail_action": "fail" + }, + { + "id": "event_af_purchase", + "description": "af_purchase event fires", + "type": "log_contains", + "pattern": "[AF_QA][logEvent: af_purchase sent] result:", + "fail_action": "fail" + }, + { + "id": "event_af_content_view", + "description": "af_content_view event fires", + "type": "log_contains", + "pattern": "[AF_QA][logEvent: af_content_view sent] result:", + "fail_action": "fail" + }, + { + "id": "http_200_count", + "description": "At least 3 HTTP 200 responses from AppsFlyer servers", + "type": "count_matches", + "pattern": "response code:200 OK|response_status=200|responseCode=200", + "minimum": 3, + "fail_action": "fail" + }, + { + "id": "on_deep_linking_callback", + "description": "onDeepLinking callback fires (NOT_FOUND expected on clean launch)", + "type": "log_contains", + "pattern": "[AF_QA][CALLBACK][onDeepLinking]", + "fail_action": "fail" + }, + { + "id": "no_fatal_errors", + "description": "No fatal exceptions or SDK start errors in logs", + "type": "absent", + "patterns": ["Fatal Exception", "FATAL", "[AF_QA][startSDK] error:", "response code:4", "response code:5"], + "fail_action": "fail" + } + ] + }, + + { + "id": "phase_2", + "name": "Background deep link", + "scenario_ref": "E2E-002", + "description": "App is brought down after Phase 1 SDK start; the deep-link URL re-launches it. On Android the app is HOME-keyed to background and re-entered via VIEW intent. On iOS the app is terminated and re-launched with -deepLinkURL, which UnityAppControllerDeepLink replays through application:openURL:options: — same SDK pipeline as Flutter's AppDelegate. Both paths fire onDeepLinking with status=FOUND.", + "requires_fresh_install": false, + "wait_after_trigger_sec": 90, + "deep_link_url": "afqa-unity://deeplink?deep_link_value=qa_deeplink_bg&af_sub1=background_test&pid=testmedia&c=deeplink_test", + "pre_actions": { + "android": ["adb shell input keyevent KEYCODE_HOME", "sleep 2"], + "ios": ["xcrun simctl terminate {{UDID}} {{BUNDLE_ID}}", "sleep 1"] + }, + "trigger": { + "android": "adb shell am start -W -a android.intent.action.VIEW -d \"{{DEEP_LINK_URL}}\"", + "ios": "xcrun simctl launch {{UDID}} {{BUNDLE_ID}} -deepLinkURL \"{{DEEP_LINK_URL}}\"" + }, + "checks": [ + { + "id": "deeplink_found", + "description": "onDeepLinking fires with status=FOUND", + "type": "log_contains", + "pattern": "status=FOUND", + "fail_action": "fail" + }, + { + "id": "deeplink_value_bg", + "description": "deepLinkValue matches qa_deeplink_bg", + "type": "log_contains", + "pattern": "deepLinkValue=qa_deeplink_bg", + "fail_action": "fail" + }, + { + "id": "no_fatal_errors", + "description": "No fatal exceptions after deep link", + "type": "absent", + "patterns": ["Fatal Exception", "FATAL"], + "fail_action": "fail" + } + ] + }, + + { + "id": "phase_3", + "name": "Foreground deep link", + "scenario_ref": "E2E-003", + "description": "Fresh install. App is in foreground after SDK start. On Android a brief HOME switch triggers onPause and the VIEW intent brings the app back. On iOS the app is terminated and re-launched with -deepLinkURL (same launch-arg path as phase_2); the foreground-vs-killed distinction is Android-only. Both paths fire onDeepLinking with status=FOUND.", + "requires_fresh_install": true, + "wait_after_launch_sec": 240, + "wait_after_trigger_sec": 90, + "deep_link_url": "afqa-unity://deeplink?deep_link_value=qa_deeplink_fg&af_sub1=foreground_test&pid=testmedia&c=deeplink_test", + "pre_actions": { + "android": ["adb shell am start -a android.intent.action.MAIN -c android.intent.category.HOME", "sleep 1"], + "ios": ["xcrun simctl terminate {{UDID}} {{BUNDLE_ID}}", "sleep 1"] + }, + "trigger": { + "android": "adb shell am start -W -a android.intent.action.VIEW -d \"{{DEEP_LINK_URL}}\"", + "ios": "xcrun simctl launch {{UDID}} {{BUNDLE_ID}} -deepLinkURL \"{{DEEP_LINK_URL}}\"" + }, + "checks": [ + { + "id": "sdk_started", + "description": "startSDK returns SUCCESS on fresh install", + "type": "log_contains", + "pattern": "[AF_QA][startSDK] result: SUCCESS", + "fail_action": "abort" + }, + { + "id": "install_conversion_callback", + "description": "onInstallConversionData callback fires before deep link", + "type": "log_contains", + "pattern": "[AF_QA][CALLBACK][onInstallConversionData]", + "fail_action": "abort" + }, + { + "id": "deeplink_found_fg", + "description": "onDeepLinking fires with status=FOUND after foreground deep link", + "type": "log_contains", + "pattern": "status=FOUND", + "fail_action": "fail" + }, + { + "id": "deeplink_value_fg", + "description": "deepLinkValue matches qa_deeplink_fg", + "type": "log_contains", + "pattern": "deepLinkValue=qa_deeplink_fg", + "fail_action": "fail" + }, + { + "id": "no_fatal_errors", + "description": "No fatal exceptions", + "type": "absent", + "patterns": ["Fatal Exception", "FATAL"], + "fail_action": "fail" + } + ] + }, + + { + "id": "phase_4", + "name": "Custom in-app event with parameters", + "scenario_ref": "E2E-004", + "description": "Verifies the custom logEvent with multi-type parameters (revenue, currency, nested metadata string) fired by the test app's auto-run after cold launch. Covers E2E-004.", + "requires_fresh_install": false, + "wait_after_trigger_sec": 5, + "checks": [ + { + "id": "custom_event_logged", + "description": "logEvent invocation with name af_qa_custom_purchase is logged", + "type": "log_contains", + "pattern": "[AF_QA][logEvent] name=af_qa_custom_purchase", + "fail_action": "fail" + }, + { + "id": "param_revenue_present", + "description": "Revenue parameter present in serialized payload", + "type": "log_contains", + "pattern": "af_revenue", + "fail_action": "fail" + }, + { + "id": "param_currency_present", + "description": "Currency parameter present", + "type": "log_contains", + "pattern": "af_currency", + "fail_action": "fail" + }, + { + "id": "param_nested_metadata", + "description": "Nested metadata key preserved in payload", + "type": "log_contains", + "pattern": "metadata", + "fail_action": "fail" + }, + { + "id": "custom_event_http_200", + "description": "Custom event request returns HTTP 200", + "type": "count_matches", + "pattern": "response code:200 OK|response_status=200|responseCode=200", + "minimum": 1, + "fail_action": "fail" + }, + { + "id": "no_log_event_error", + "description": "No logEvent error reported", + "type": "absent", + "patterns": ["[AF_QA][logEvent] error:"], + "fail_action": "fail" + } + ] + }, + + { + "id": "phase_5", + "name": "Identity APIs round-trip", + "scenario_ref": "E2E-005", + "description": "Fresh install. Sets customer user id (e2e_user_42), currency code (EUR), and additional data (tenant: qa_eu) before start. Verifies readback and propagation. Covers E2E-005.", + "requires_fresh_install": true, + "wait_after_launch_sec": 240, + "wait_after_trigger_sec": 5, + "checks": [ + { + "id": "set_customer_user_id_logged", + "description": "setCustomerUserId readback present", + "type": "log_contains", + "pattern": "[AF_QA][setCustomerUserId] result: e2e_user_42", + "fail_action": "fail" + }, + { + "id": "set_currency_code_logged", + "description": "setCurrencyCode readback present", + "type": "log_contains", + "pattern": "[AF_QA][setCurrencyCode] result: EUR", + "fail_action": "fail" + }, + { + "id": "set_additional_data_logged", + "description": "setAdditionalData echoes its keys", + "type": "log_contains", + "pattern": "[AF_QA][setAdditionalData]", + "fail_action": "fail" + }, + { + "id": "user_id_in_payload", + "description": "Post-start event payload carries customer_user_id", + "type": "regex_match", + "pattern": "customer_user_id[ =:]+e2e_user_42", + "fail_action": "fail" + }, + { + "id": "additional_data_in_payload", + "description": "Post-start event payload contains additional data tenant key", + "type": "log_contains", + "pattern": "tenant", + "fail_action": "fail" + }, + { + "id": "install_conversion_callback", + "description": "Install conversion still fires with is_first_launch=true", + "type": "log_contains", + "pattern": "[AF_QA][CALLBACK][onInstallConversionData]", + "payload_check": {"field": "is_first_launch", "expected": "true"}, + "fail_action": "fail" + }, + { + "id": "identity_event_http_200", + "description": "Identity check event receives HTTP 200", + "type": "count_matches", + "pattern": "response code:200 OK|response_status=200|responseCode=200", + "minimum": 1, + "fail_action": "fail" + } + ] + }, + + { + "id": "phase_6", + "name": "Consent / SDK stop toggle", + "scenario_ref": "E2E-006", + "description": "Auto-run dispatches stop(true), an event that must be suppressed, then stop(false), and an event that must succeed. Verifies the privacy kill switch. Covers E2E-006.", + "requires_fresh_install": false, + "wait_after_trigger_sec": 5, + "checks": [ + { + "id": "stop_true_logged", + "description": "stop(true) readback present", + "type": "log_contains", + "pattern": "[AF_QA][stop] result: true", + "fail_action": "fail" + }, + { + "id": "stop_false_logged", + "description": "stop(false) readback present", + "type": "log_contains", + "pattern": "[AF_QA][stop] result: false", + "fail_action": "fail" + }, + { + "id": "suppressed_event_no_http", + "description": "While stopped, af_qa_suppressed must not produce an HTTP 200 response", + "type": "absent", + "patterns": ["af_qa_suppressed.*response code:200", "af_qa_suppressed.*response_status=200", "af_qa_suppressed.*responseCode=200"], + "fail_action": "fail" + }, + { + "id": "resumed_event_http_200", + "description": "After stop(false), af_qa_resumed produces HTTP 200", + "type": "count_matches", + "pattern": "response code:200 OK|response_status=200|responseCode=200", + "minimum": 1, + "fail_action": "fail" + }, + { + "id": "no_fatal_errors", + "description": "No fatal exceptions across the stop/start cycle", + "type": "absent", + "patterns": ["Fatal Exception", "FATAL"], + "fail_action": "fail" + } + ] + } + ], + + "report": { + "output_dir": ".af-e2e/reports/", + "format": "json" + } +} diff --git a/.claude/agents/unity-e2e-tester.md b/.claude/agents/unity-e2e-tester.md new file mode 100644 index 00000000..b931dcb9 --- /dev/null +++ b/.claude/agents/unity-e2e-tester.md @@ -0,0 +1,157 @@ +--- +name: unity-e2e-tester +description: Use this agent when validating the AppsFlyer Unity plugin end-to-end on Android emulators and iOS simulators, covering SDK initialization, in-app events, deep link flows, conversion callbacks, and plugin integration correctness. +tools: Read, Grep, Glob, Bash, Edit, Write +--- + +You are a senior mobile QA automation and end-to-end validation engineer focused on the AppsFlyer Unity plugin. + +Your job is to validate that the Unity plugin works correctly across C#, Android (Java), and iOS (Objective-C/Swift) layers using emulators, simulators, Unity test apps, and integration test flows. + +You validate SDK behavior by **analyzing runtime logs, callback payloads, HTTP request payloads, and HTTP response codes**, and comparing them against **saved baselines**. + +--- + +# Core Mindset + +Validate the full flow: + +``` +Unity app (C#) +→ AppsFlyerSDK C# API +→ P/Invoke / DllImport → Native Android/iOS bridge +→ Native AppsFlyer SDK +→ HTTP request with correct payload → AppsFlyer servers +→ HTTP 200 OK response confirmed +→ callbacks returned to C# +``` + +Evidence must come from **logs, callbacks, HTTP payloads, and HTTP response codes** — not assumptions. + +--- + +# Rule: Every Test Starts With a Fresh Install + +Never test against a running app or warm relaunch. + +A test is only valid if: +1. The app was fully uninstalled before the run +2. The APK/app was reinstalled from a known-good build +3. The app was launched fresh after install +4. `is_first_launch: true` is confirmed in `onInstallConversionData` + +If `is_first_launch` is `false`, **stop immediately**. The test environment is not clean. + +--- + +# Fresh Install Procedure + +## Android + +```bash +adb uninstall +adb logcat -c +adb install .apk +adb shell am start -n / +sleep 20 +adb shell pidof +``` + +Build: Export Android project from Unity → build via Gradle or Android Studio. + +## iOS + +```bash +xcrun simctl uninstall +xcrun simctl install .app +xcrun simctl launch +sleep 25 +``` + +Build: Export iOS project from Unity → build via Xcode. + +--- + +# Log Collection + +## Android + +```bash +adb logcat -d --pid= -t 2000 | grep "AF_QA\|AppsFlyer\|Unity" +adb logcat -d -s AppsFlyer_ --pid= -t 2000 +``` + +## iOS + +```bash +xcrun simctl spawn log show \ + --predicate 'processID == ' \ + --last 60s --style compact | \ +grep -E "AF_QA|CFNetwork:Summary|appsflyer|Unity" +``` + +--- + +# Baseline-Driven Validation + +Baseline files live at `.claude/e2e-baselines/`: +- `android_baseline.json` +- `ios_baseline.json` + +For each run: +1. Read the relevant baseline. +2. Perform fresh install. +3. Collect logs. +4. Validate each section: lifecycle → api_results → http_requests → callbacks → events. +5. Produce a diff table. + +--- + +# Validation Checklist + +- [ ] Fresh install confirmed — `is_first_launch: true` +- [ ] SDK lifecycle sequence correct (C# `Start()` → `initSDK()` → `startSDK()`) +- [ ] `getSDKVersion` matches baseline +- [ ] HTTP endpoints contacted → 200 OK +- [ ] In-app events sent with correct payloads +- [ ] `onInstallConversionData` fired — `af_status=Organic`, `is_first_launch=true` +- [ ] `onDeepLinking` NOT_FOUND on clean launch +- [ ] No fatal errors in logs +- [ ] Billing library variant used is the expected one (v7 or v8) + +--- + +# Output Format + +``` +Platform: Android | iOS +Install method: fresh install (uninstall + reinstall) +PID confirmed: +is_first_launch: true ✓ | false ✗ (INVALID RUN) + +Baseline diff: + +| Item | Expected | Actual | Status | +|-------------------------|------------|------------|--------| +| ... | ... | ... | ... | + +Status: PASS | FAIL | INVALID | Blocked +``` + +--- + +# Testing Rules + +- Do not claim a flow works without evidence. +- HTTP 200 is not optional. +- iOS payload bodies cannot be validated on simulator — validate endpoints and 200s only. +- `is_first_launch: false` = INVALID run, not FAIL. +- Unity builds require export from the Unity Editor — note this dependency. + +## Skill Usage + +- `appsflyer-event-validation` — verify events and callbacks from logs +- `launch-log-analysis` — analyze per-session logs +- `e2e-smoke-test` — basic post-change validation +- `platform-channel-debug` — when the C#/native bridge looks broken +- `plugin-release` — when validating release readiness diff --git a/.claude/agents/unity-plugin-developer.md b/.claude/agents/unity-plugin-developer.md new file mode 100644 index 00000000..8524d16a --- /dev/null +++ b/.claude/agents/unity-plugin-developer.md @@ -0,0 +1,164 @@ +--- +name: unity-plugin-developer +description: Use this agent when implementing or modifying the AppsFlyer Unity plugin. Handles C# API, Unity native bridge, Android Java/Kotlin wrapper, iOS Objective-C/Swift wrapper, SDK version bumps, and release-related changes. +tools: Read, Grep, Glob, Bash, Edit, Write +--- + +You are a senior Unity and mobile SDK integration engineer working on the AppsFlyer Unity plugin. + +You understand: +- Unity plugin development (Unity 2019.4+) +- C# API design under the `AppsFlyerSDK` namespace +- Native SDK bridging for Android (Java) and iOS (Objective-C/Swift) +- Unity editor extensions, assembly definitions, and P/Invoke patterns +- Gradle build system and CocoaPods for Unity + +You build and maintain production mobile SDK integrations used by many games and apps. + +--- + +# Core Mindset + +The repository contains two kinds of code: + +1. **Unity plugin code** + - C# API layer (`Assets/AppsFlyer/`) + - Android Java native bridge (`android-unity-wrapper/`) + - iOS Objective-C/Swift native bridge (`Assets/AppsFlyer/Plugins/iOS/`) + - Unity Editor extensions (`Assets/AppsFlyer/Editor/`) + +2. **Example/integration code** + - `deploy/` — pre-built `.unitypackage` release artifacts + - Unity test suite (`Assets/AppsFlyer/Tests/`) + +You must always understand which layer you are modifying. + +--- + +# Responsibilities + +- Unity plugin development +- C# API design +- Android Java wrapper integration +- iOS native bridge maintenance +- Plugin and SDK version management +- SDK version bumps +- Unity package release +- Integration validation + +--- + +# Plugin Development + +When modifying the plugin, trace functionality across all layers: + +C# API → P/Invoke / DllImport → Native Android/iOS SDK + +### C# Layer (`Assets/AppsFlyer/`) +- Public API correctness in `AppsFlyer.cs` +- Platform delegation to `AppsFlyerAndroid.cs` / `AppsFlyeriOS.cs` +- `#if UNITY_IOS` / `#if UNITY_ANDROID` guards for platform-specific code +- Namespace: `AppsFlyerSDK` — do not change +- Assembly definition boundaries (`.asmdef`) — do not break + +### Android Layer (`android-unity-wrapper/`) +- `AppsFlyerAndroidWrapper.java` — native bridge +- `AppsFlyerAndroid.cs` calls `AndroidJavaClass` / `AndroidJavaObject` +- Gradle API 16-34, Java/Kotlin 17 +- Two billing library variants (v7 and v8) — keep both in sync +- ProGuard rules in `android-unity-wrapper/` + +### iOS Layer (`Assets/AppsFlyer/Plugins/iOS/`) +- `AppsFlyeriOSWrapper.mm` — Objective-C++ bridge +- `AppsFlyeriOS.cs` calls via `[DllImport("__Internal")]` +- CocoaPods integration via podspec + +### Editor Extensions (`Assets/AppsFlyer/Editor/`) +- Unity Inspector configuration helpers +- Do not break editor-only code with runtime changes + +--- + +# SDK Integration Rules + +Verify: +- SDK initialization flow +- Event logging behavior +- Deep link handling (including Unified Deep Links) +- Ad revenue measurement +- User consent management (DMA compliance) +- Parity between Android and iOS implementations + +Never silently change event names or callback behavior. + +--- + +# Version Management + +When updating versions, check all locations: + +Unity plugin: +- Version constant in `AppsFlyer.cs` + +Android wrapper: +- `android-unity-wrapper/build.gradle` (af-android-sdk, billing library) + +iOS wrapper: +- iOS podspec or dependency spec + +Release artifacts: +- `deploy/` `.unitypackage` files (regenerate on release) +- `CHANGELOG.md` + +Always report: +- Previous version +- New version +- Files modified +- Billing library variant impact (v7 vs v8) +- Compatibility/release risk +- Validation steps + +--- + +# Output Expectations + +Provide: +- Summary +- Files changed +- Layer impact: C# | Android | iOS +- Billing library impact if applicable +- Compatibility risk +- Testing steps +- Release notes impact if applicable + +--- + +# Implementation Rules + +- Read repository structure before modifying code. +- Prefer minimal safe changes. +- Do not break public C# API unless explicitly instructed. +- Keep Android and iOS behavior aligned. +- Both billing library variants must remain buildable. +- `.unitypackage` files in `deploy/` are build artifacts — do not edit manually. +- Do not assume conventions — verify them. + +--- + +# Common Tasks + +- Plugin feature implementation +- C# API improvements +- SDK version bumps +- Plugin version bumps +- Integration fixes +- Android wrapper Gradle updates +- iOS native bridge fixes +- Debugging P/Invoke / DllImport issues + +## Skill Usage + +- `sdk-version-bump` — bump wrapped Android/iOS SDK versions +- `plugin-api-change` — add or modify plugin APIs +- `platform-channel-debug` — debug C#/native bridge issues +- `plugin-release` — review release readiness diff --git a/.claude/e2e-reports/README.md b/.claude/e2e-reports/README.md new file mode 100644 index 00000000..23787b6b --- /dev/null +++ b/.claude/e2e-reports/README.md @@ -0,0 +1,8 @@ +# E2E Reports + +This directory stores full E2E test run reports for the AppsFlyer Unity plugin. + +Each report is saved as `full_e2e_report_.json` after a completed run. + +| Date | Platform | Status | Report | +|------|----------|--------|--------| diff --git a/.claude/skills/appsflyer-event-validation/SKILL.md b/.claude/skills/appsflyer-event-validation/SKILL.md new file mode 100644 index 00000000..9912adcd --- /dev/null +++ b/.claude/skills/appsflyer-event-validation/SKILL.md @@ -0,0 +1,40 @@ +--- +name: appsflyer-event-validation +description: Validate that expected AppsFlyer SDK events and callbacks were triggered during an end-to-end Unity app scenario using logs, callback payloads, and test evidence. +--- + +# AppsFlyer Event Validation + +Use this skill when validating whether AppsFlyer-related events actually fired during an app scenario. + +## Goal + +Compare expected AppsFlyer events against real evidence from logs, callbacks, or assertions. + +## Workflow + +1. Identify the scenario under test. +2. Define the expected AppsFlyer events or callbacks for that scenario. +3. Collect evidence from: Unity console logs, Android logcat, iOS simulator logs, C# callback payloads. +4. Match expected events against actual evidence. +5. Classify each expected event as: Confirmed triggered | Missing | Duplicate | Unclear | Blocked by environment. +6. Summarize the result. + +## Output Format + +Return: +- Scenario +- Platform +- Expected AppsFlyer events +- Confirmed triggered events +- Missing events +- Duplicate or unexpected events +- Relevant evidence +- Final status: Passed | Failed | Inconclusive | Blocked + +## Rules + +- Do not assume an event fired because the app launched. +- Confirm events only from logs, callbacks, or assertions. +- If logs are insufficient, say `needs more logging`. +- Separate verified facts from assumptions. diff --git a/.claude/skills/e2e-smoke-test/SKILL.md b/.claude/skills/e2e-smoke-test/SKILL.md new file mode 100644 index 00000000..4c15d985 --- /dev/null +++ b/.claude/skills/e2e-smoke-test/SKILL.md @@ -0,0 +1,39 @@ +--- +name: e2e-smoke-test +description: Run or review a basic end-to-end smoke test for the AppsFlyer Unity plugin on Android emulator or iOS simulator, covering startup, initialization, and basic event flow. +--- + +# E2E Smoke Test + +Use this skill for quick validation that the Unity plugin is basically working after a change. + +## Goal + +Check that the plugin still works end-to-end at a basic level after code, SDK, or version changes. + +## Workflow + +1. Identify the target platform: Android emulator | iOS simulator | both +2. Verify the scenario setup: Unity test app exported, APK/app available, emulator/simulator ready. +3. Run or inspect the basic smoke scenario: app launch → SDK init (`initSDK`) → SDK start (`startSDK`) → one basic AppsFlyer event → callback flow. +4. Collect evidence: logs, callback payloads, Unity console output. +5. Report whether the flow passed or failed. + +## Output Format + +Return: +- Scenario +- Platform +- Steps +- Expected behavior +- Actual behavior +- Evidence +- Status: Passed | Failed | Blocked +- Follow-up recommendation + +## Rules + +- Keep this focused on basic release confidence. +- Unity builds must be exported before running on emulator/simulator. +- Note which billing library variant was used (v7 or v8). +- If a key part cannot be tested locally, say so clearly. diff --git a/.claude/skills/launch-log-analysis/SKILL.md b/.claude/skills/launch-log-analysis/SKILL.md new file mode 100644 index 00000000..60d39135 --- /dev/null +++ b/.claude/skills/launch-log-analysis/SKILL.md @@ -0,0 +1,39 @@ +--- +name: launch-log-analysis +description: Analyze logs for a specific app launch and determine whether AppsFlyer initialization, start flow, callbacks, and expected events occurred correctly in the Unity plugin. +--- + +# Launch Log Analysis + +Use this skill when inspecting logs from one or more app launches. + +## Goal + +Analyze each launch separately and determine whether expected AppsFlyer SDK flows occurred. + +## Workflow + +1. Separate logs by launch/session if multiple launches exist. +2. For each launch inspect evidence for: SDK initialization (`initSDK`), SDK start (`startSDK`), conversion callbacks, deep link callbacks, in-app events, warnings or errors. +3. Compare expected vs actual behavior for that launch. +4. Detect: missing events, duplicate events, unexpected events, inconclusive evidence. +5. Produce a launch-by-launch report. + +## Output Format + +For each launch return: +- Launch number +- Platform +- Expected events +- Confirmed events +- Missing events +- Duplicate or unexpected events +- Relevant log evidence +- Final result: Passed | Failed | Inconclusive + +## Rules + +- Treat each launch separately. +- Do not merge evidence from different launches. +- Do not guess when logs are noisy or incomplete. +- Recommend additional logging if needed. diff --git a/.claude/skills/platform-channel-debug/SKILL.md b/.claude/skills/platform-channel-debug/SKILL.md new file mode 100644 index 00000000..83563e58 --- /dev/null +++ b/.claude/skills/platform-channel-debug/SKILL.md @@ -0,0 +1,53 @@ +--- +name: platform-channel-debug +description: Debug communication issues between C# code and native Android/iOS implementations in the AppsFlyer Unity plugin. +--- + +# Platform Channel Debug + +Use this skill when a C#-to-native or native-to-C# flow is not working correctly. + +## Goal + +Find where the Unity native bridge is broken between C#, the platform-specific wrappers, and native SDK integration. + +## Workflow + +1. Identify the failing flow: C# → native method call | native → C# callback | initialization/registration issue. +2. Trace the path end-to-end: + - C# `AppsFlyer.cs` → `AppsFlyerAndroid.cs` (AndroidJavaClass) | `AppsFlyeriOS.cs` ([DllImport]) + - → `AppsFlyerAndroidWrapper.java` | `AppsFlyeriOSWrapper.mm` + - → Native AppsFlyer SDK callback +3. Verify argument and payload mapping. +4. Check for mismatches: method names, argument types, platform guards (`#if UNITY_IOS` / `#if UNITY_ANDROID`), null handling, callback thread assumptions. +5. Identify the first broken point in the chain. +6. Recommend the smallest safe fix. + +## What to Check + +- `AppsFlyer.cs` — public API call and platform delegation +- `AppsFlyerAndroid.cs` — AndroidJavaClass/AndroidJavaObject call +- `AppsFlyeriOS.cs` — DllImport extern function signature +- `AppsFlyerAndroidWrapper.java` — Java bridge method signature +- `AppsFlyeriOSWrapper.mm` — Objective-C++ bridge function signature +- Callback propagation back to C# (Unity main thread safety) +- Assembly definition boundaries + +## Output Format + +Return: +- Failing scenario +- Expected flow +- Actual break point +- Evidence +- Suspected root cause +- Recommended fix +- Validation steps + +## Rules + +- Do not say the issue is in native SDK behavior unless the bridge has been verified first. +- Unity requires callbacks to be dispatched to the main thread — verify threading. +- Prefer tracing the real execution path over guessing. +- Separate confirmed breakage from hypotheses. +- If evidence is incomplete, say `needs verification`. diff --git a/.claude/skills/plugin-api-change/SKILL.md b/.claude/skills/plugin-api-change/SKILL.md new file mode 100644 index 00000000..6adf1e11 --- /dev/null +++ b/.claude/skills/plugin-api-change/SKILL.md @@ -0,0 +1,61 @@ +--- +name: plugin-api-change +description: Safely implement or modify a Unity plugin API in the AppsFlyer Unity plugin, including C# API, Android Java bridge, and iOS Objective-C bridge changes. +--- + +# Plugin API Change + +Use this skill when adding, removing, or changing a Unity plugin API. + +## Goal + +Make a safe API change across all plugin layers while preserving backward compatibility where possible. + +## Workflow + +1. Identify the public C# API being changed in `Assets/AppsFlyer/AppsFlyer.cs`. +2. Trace the full path: C# API → `AppsFlyerAndroid.cs` (Android) | `AppsFlyeriOS.cs` (iOS) → Native bridge +3. Verify whether the API already exists in some form. +4. Check whether the change affects: method signatures, argument mapping, callback behavior, event payload shape, Android/iOS parity. +5. Implement changes in all required layers. +6. Check whether tests or example code must be updated. +7. Summarize compatibility risk and missing validation. + +## What to Check + +### C# layer +- `AppsFlyer.cs` — public API entry point +- `AppsFlyerAndroid.cs` — Android delegation (uses `AndroidJavaClass`/`AndroidJavaObject`) +- `AppsFlyeriOS.cs` — iOS delegation (uses `[DllImport("__Internal")]`) +- `#if UNITY_IOS` / `#if UNITY_ANDROID` guards +- Assembly definition (`.asmdef`) boundaries — do not break +- Namespace `AppsFlyerSDK` — do not change + +### Android bridge +- `AppsFlyerAndroidWrapper.java` — Java bridge method +- Mapping to AppsFlyer Android SDK +- Threading safety + +### iOS bridge +- `AppsFlyeriOSWrapper.mm` — Objective-C++ bridge function +- Mapping to AppsFlyer iOS SDK +- Delegate/callback wiring + +## Output Format + +Return: +- Summary +- C# API impact +- Android impact +- iOS impact +- Assembly definition impact +- Compatibility risk +- Tests to add or update +- Validation steps + +## Rules + +- Keep Android and iOS behavior aligned unless a platform difference is intentional. +- Do not silently break the public C# API. +- Both billing library variants must remain buildable after the change. +- Do not stop at C#-only changes if native bridge work is required. diff --git a/.claude/skills/plugin-release/SKILL.md b/.claude/skills/plugin-release/SKILL.md new file mode 100644 index 00000000..81c0f8b7 --- /dev/null +++ b/.claude/skills/plugin-release/SKILL.md @@ -0,0 +1,52 @@ +--- +name: plugin-release +description: Review the AppsFlyer Unity plugin for release readiness, including versioning, changelog, Android/iOS parity, billing library variants, .unitypackage artifacts, and integration safety. +--- + +# Plugin Release + +Use this skill before releasing a new version of the AppsFlyer Unity plugin. + +## Goal + +Check whether the plugin is safe and ready for release. + +## Workflow + +1. Inspect the change set or current release candidate. +2. Identify: plugin version changes, Android SDK version changes, iOS SDK version changes, C# API changes, billing library variant changes. +3. Review release readiness: version consistency, changelog presence, compatibility risks, Android/iOS parity, missing validation. +4. Verify both billing library variants (v7 and v8) build correctly. +5. Check whether `.unitypackage` files in `deploy/` need to be regenerated. +6. Check whether new behavior requires release notes or migration guidance. +7. Summarize release blockers and non-blockers. + +## What to Check + +- `Assets/AppsFlyer/AppsFlyer.cs` — version constant +- `CHANGELOG.md` +- `android-unity-wrapper/build.gradle` — Android dependency versions, both billing variants +- iOS podspec — iOS dependency versions +- Public C# API changes +- Assembly definition integrity +- `deploy/` `.unitypackage` artifacts — freshness +- Backward compatibility risk + +## Output Format + +Return: +- Release summary +- Risk level: Low | Medium | High | Critical +- Blocking issues +- Non-blocking issues +- `.unitypackage` regeneration required: Yes | No +- Missing validations +- Recommended release decision + +## Rules + +- Do not approve a release if versioning is inconsistent. +- Both billing library variants must be buildable. +- `.unitypackage` files must be regenerated before tagging a release. +- Explicitly call out C#/API compatibility risk. +- Explicitly call out Android/iOS behavior drift. diff --git a/.claude/skills/sdk-version-bump/SKILL.md b/.claude/skills/sdk-version-bump/SKILL.md new file mode 100644 index 00000000..f3c03201 --- /dev/null +++ b/.claude/skills/sdk-version-bump/SKILL.md @@ -0,0 +1,65 @@ +--- +name: sdk-version-bump +description: Safely bump the wrapped Android SDK or iOS SDK version in the AppsFlyer Unity plugin, update the plugin version if needed, and validate all related files including both billing library variants. +--- + +# SDK Version Bump + +Use this skill when updating the native Android or iOS SDK version wrapped by the AppsFlyer Unity plugin. + +## Goal + +Perform a safe, consistent version bump across C#, Android, and iOS plugin layers, maintaining both billing library variants. + +## Workflow + +1. Identify which SDK is being bumped: Android SDK | iOS SDK | both +2. Locate current versions: + - Version constant in `Assets/AppsFlyer/AppsFlyer.cs` + - `android-unity-wrapper/build.gradle` (af-android-sdk, billing library v7/v8) + - iOS podspec or dependency declarations + - `CHANGELOG.md` +3. Update the native SDK version in all required places. +4. Check both billing library variants (v7 and v8) — both must be updated consistently. +5. Decide whether the Unity plugin version must also be bumped. +6. Check whether changelog must be updated. +7. Note that `deploy/` `.unitypackage` files must be regenerated on release. +8. Summarize: old version, new version, files changed, billing library impact, compatibility risk, validation steps. + +## What to Check + +### Android +- `android-unity-wrapper/build.gradle` — `af-android-sdk` dependency +- Billing library v7 and v8 build variants — both must be updated +- ProGuard rules if new classes are added +- `AppsFlyerAndroidWrapper.java` — version constants if any + +### iOS +- iOS podspec or pod dependency declarations +- `AppsFlyeriOSWrapper.mm` — version constants if any + +### Unity plugin +- `Assets/AppsFlyer/AppsFlyer.cs` — version constant +- `CHANGELOG.md` +- `deploy/` `.unitypackage` files (must regenerate after bump) + +## Output Format + +Return: +- Summary +- SDK bumped: Android | iOS | Both +- Previous version → New version +- Billing library variants impacted: v7 | v8 | both +- Files updated +- Plugin version impact +- Changelog impact +- `.unitypackage` regeneration required: Yes | No +- Validation steps +- Compatibility risk + +## Rules + +- Both billing library variants must be updated together. +- Do not update only one place if multiple version declarations exist. +- `.unitypackage` files are release artifacts — flag them as needing regeneration. +- Prefer a minimal and consistent change set. diff --git a/.github/workflows/rc-e2e-android.yml b/.github/workflows/rc-e2e-android.yml new file mode 100644 index 00000000..c9b3b4df --- /dev/null +++ b/.github/workflows/rc-e2e-android.yml @@ -0,0 +1,110 @@ +name: RC E2E — Android (reusable) + +on: + workflow_call: + inputs: + plugin_version: + description: RC plugin version (e.g. 6.18.0-rc1) + required: true + type: string + release_branch: + description: Release branch to check out + required: true + type: string + secrets: + UNITY_EMAIL: + required: true + UNITY_PASSWORD: + required: true + UNITY_SERIAL: + required: true + ENV_FILE: + required: true + +jobs: + e2e-android: + name: E2E Android — ${{ inputs.plugin_version }} + runs-on: ubuntu-latest + timeout-minutes: 90 + + steps: + - name: Checkout release branch + uses: actions/checkout@v4 + with: + ref: ${{ inputs.release_branch }} + + - name: Write .env from secret + run: | + printf '%s' "${{ secrets.ENV_FILE }}" > test-app/Assets/StreamingAssets/.env + + - name: Activate Unity license + uses: game-ci/unity-activate@v2 + env: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} + + - name: Build Android APK (via Unity) + uses: game-ci/unity-builder@v4 + env: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} + with: + projectPath: test-app + unityVersion: 6000.3.14f1 + targetPlatform: Android + buildName: com.appsflyer.engagement + buildsPath: test-app/Build + buildMethod: BuildScript.BuildAndroid + androidExportType: androidPackage + + - name: Locate APK + run: | + APK=$(find test-app/Build/Android -name "*.apk" | head -1) + if [[ -z "$APK" ]]; then + echo "::error::APK not found after Unity build" + exit 1 + fi + mkdir -p test-app/Build/Android + cp "$APK" test-app/Build/Android/com.appsflyer.engagement.apk + echo "APK_PATH=test-app/Build/Android/com.appsflyer.engagement.apk" >> "$GITHUB_ENV" + + - name: Install jq + run: sudo apt-get install -y jq + + - name: Run E2E scenario runner inside Android emulator + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 31 + arch: x86_64 + profile: Nexus 6 + avd-name: e2e_avd + emulator-options: -no-snapshot -no-window -no-boot-anim -gpu swiftshader_indirect + disable-animations: true + script: | + # Push .env to external storage (Unity persistentDataPath on Android) + adb wait-for-device + adb install -r "${{ env.APK_PATH }}" + adb shell mkdir -p /storage/emulated/0/Android/data/com.appsflyer.engagement/files/ + adb push test-app/Assets/StreamingAssets/.env \ + /storage/emulated/0/Android/data/com.appsflyer.engagement/files/.env + # Cold-launch the app and run scenarios + adb shell am force-stop com.appsflyer.engagement + chmod +x scripts/af-scenario-runner.sh + scripts/af-scenario-runner.sh \ + --platform android \ + --plan .af-e2e/test-plan.json \ + --verbose + + - name: Upload E2E report + if: always() + uses: actions/upload-artifact@v4 + with: + name: e2e-android-report-${{ inputs.plugin_version }} + path: .af-e2e/reports/ + retention-days: 30 + + - name: Return Unity license + if: always() + uses: game-ci/unity-return-license@v2 diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml new file mode 100644 index 00000000..9829713a --- /dev/null +++ b/.github/workflows/rc-e2e-ios.yml @@ -0,0 +1,133 @@ +name: RC E2E — iOS (reusable) + +on: + workflow_call: + inputs: + plugin_version: + description: RC plugin version (e.g. 6.18.0-rc1) + required: true + type: string + release_branch: + description: Release branch to check out + required: true + type: string + secrets: + UNITY_EMAIL: + required: true + UNITY_PASSWORD: + required: true + UNITY_SERIAL: + required: true + ENV_FILE: + required: true + +jobs: + e2e-ios: + name: E2E iOS — ${{ inputs.plugin_version }} + runs-on: macos-14 + timeout-minutes: 90 + + steps: + - name: Checkout release branch + uses: actions/checkout@v4 + with: + ref: ${{ inputs.release_branch }} + + - name: Write .env from secret + run: | + printf '%s' "${{ secrets.ENV_FILE }}" > test-app/Assets/StreamingAssets/.env + + - name: Install jq + run: brew install jq 2>/dev/null || true + + - name: Activate Unity license + uses: game-ci/unity-activate@v2 + env: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} + + - name: Build iOS Xcode project (via Unity) + uses: game-ci/unity-builder@v4 + env: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} + with: + projectPath: test-app + unityVersion: 6000.3.14f1 + targetPlatform: iOS + buildName: UnityQATest + buildsPath: test-app/Build + buildMethod: BuildScript.BuildIOSSimulator + + - name: Locate Xcode project and install iOS pods + run: | + XCODE_PROJ=$(find test-app/Build -name "*.xcodeproj" -not -path "*/DerivedData/*" | head -1) + if [[ -z "$XCODE_PROJ" ]]; then + echo "::error::No .xcodeproj found under test-app/Build" + exit 1 + fi + IOS_BUILD_DIR=$(dirname "$XCODE_PROJ") + echo "IOS_BUILD_DIR=$IOS_BUILD_DIR" >> "$GITHUB_ENV" + echo "Found Xcode project in: $IOS_BUILD_DIR" + + chmod +x scripts/ios-pod-install.sh + scripts/ios-pod-install.sh "$IOS_BUILD_DIR" + + - name: Compile simulator .app from Xcode workspace + run: | + XCODE_WS=$(find "$IOS_BUILD_DIR" -name "Unity-iPhone.xcworkspace" -not -path "*/DerivedData/*" | head -1) + if [[ -z "$XCODE_WS" ]]; then + echo "::error::Unity-iPhone.xcworkspace not found after pod install in $IOS_BUILD_DIR" + exit 1 + fi + xcodebuild \ + -workspace "$XCODE_WS" \ + -scheme Unity-iPhone \ + -sdk iphonesimulator \ + -configuration Debug \ + -derivedDataPath "$IOS_BUILD_DIR/DerivedData" \ + build | xcpretty || true + + APP=$(find "$IOS_BUILD_DIR/DerivedData" -name "UnityQATest.app" -maxdepth 6 | head -1) + if [[ -z "$APP" ]]; then + echo "::error::UnityQATest.app not found after xcodebuild" + exit 1 + fi + mkdir -p test-app/Build/iOS-Simulator + cp -R "$APP" test-app/Build/iOS-Simulator/UnityQATest.app + echo "APP_PATH=test-app/Build/iOS-Simulator/UnityQATest.app" >> "$GITHUB_ENV" + + - name: Boot iOS simulator + run: | + UDID=$(xcrun simctl list devices available -j | \ + jq -r '.devices | to_entries[] | select(.key | contains("iOS-17")) | .value[] | select(.isAvailable == true) | .udid' | head -1) + if [[ -z "$UDID" ]]; then + UDID=$(xcrun simctl list devices available -j | \ + jq -r '.devices[][] | select(.isAvailable == true) | .udid' | head -1) + fi + echo "Booting simulator: $UDID" + xcrun simctl boot "$UDID" || true + xcrun simctl bootstatus "$UDID" -b + echo "SIM_UDID=$UDID" >> "$GITHUB_ENV" + + - name: Run E2E scenario runner — iOS + run: | + chmod +x scripts/af-scenario-runner.sh + scripts/af-scenario-runner.sh \ + --platform ios \ + --plan .af-e2e/test-plan.json \ + --verbose + + - name: Upload E2E report + if: always() + uses: actions/upload-artifact@v4 + with: + name: e2e-ios-report-${{ inputs.plugin_version }} + path: .af-e2e/reports/ + retention-days: 30 + + - name: Return Unity license + if: always() + uses: game-ci/unity-return-license@v2 diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml new file mode 100644 index 00000000..a51354f5 --- /dev/null +++ b/.github/workflows/rc-release.yml @@ -0,0 +1,262 @@ +name: RC Release Pipeline + +on: + workflow_dispatch: + inputs: + plugin_version: + description: "RC plugin version — must match ^\\d+\\.\\d+\\.\\d+-rc\\d+$ (e.g. 6.18.0-rc1)" + required: true + type: string + android_sdk_version: + description: "Android SDK version to pin (e.g. 6.18.0)" + required: true + type: string + ios_sdk_version: + description: "iOS SDK version to pin (e.g. 6.18.0)" + required: true + type: string + base_branch: + description: "Branch to cut the release from" + required: false + default: development + type: string + skip_tests: + description: "Skip unit test job (emergency / hotfix use only)" + required: false + default: "false" + type: string + skip_e2e: + description: "Skip E2E jobs (emergency / hotfix use only)" + required: false + default: "false" + type: string + dry_run: + description: "Dry run — validate inputs and print plan without pushing or opening a PR" + required: false + default: "true" + type: string + +jobs: + # ── 1. Validate inputs ────────────────────────────────────────────────────── + validate-release: + name: Validate inputs + runs-on: ubuntu-latest + outputs: + base_version: ${{ steps.parse.outputs.base_version }} + steps: + - name: Check plugin_version format + run: | + if ! echo "${{ github.event.inputs.plugin_version }}" | grep -qE '^\d+\.\d+\.\d+-rc\d+$'; then + echo "::error::plugin_version '${{ github.event.inputs.plugin_version }}' does not match ^\d+\.\d+\.\d+-rc\d+$" + exit 1 + fi + echo "plugin_version OK: ${{ github.event.inputs.plugin_version }}" + + - name: Parse base_version (strip -rcN suffix) + id: parse + run: | + BASE=$(echo "${{ github.event.inputs.plugin_version }}" | sed 's/-rc[0-9]*$//') + echo "base_version=$BASE" >> "$GITHUB_OUTPUT" + echo "base_version=$BASE" + + # ── 2. Prepare release branch ─────────────────────────────────────────────── + prepare-branch: + name: Prepare release branch + needs: validate-release + runs-on: ubuntu-latest + outputs: + release_branch: ${{ steps.branch.outputs.release_branch }} + steps: + - name: Checkout base branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.base_branch }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure git + run: | + git config user.name "${{ secrets.CI_USERNAME }}" + git config user.email "${{ secrets.CI_EMAIL }}" + + - name: Derive release branch name + id: branch + run: | + VERSION="${{ github.event.inputs.plugin_version }}" + MAJOR=$(echo "$VERSION" | cut -d. -f1) + MINOR=$(echo "$VERSION" | cut -d. -f2) + BRANCH="releases/${MAJOR}.x.x/${MINOR}.x/${VERSION}" + echo "release_branch=$BRANCH" >> "$GITHUB_OUTPUT" + echo "release_branch=$BRANCH" + + - name: Create or reset release branch + run: | + BRANCH="${{ steps.branch.outputs.release_branch }}" + if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then + echo "Branch exists — resetting to ${{ github.event.inputs.base_branch }}" + git checkout -B "$BRANCH" "origin/${{ github.event.inputs.base_branch }}" + else + echo "Creating new branch: $BRANCH" + git checkout -b "$BRANCH" + fi + + - name: Bump versions + run: | + chmod +x scripts/bump-version.sh + scripts/bump-version.sh \ + --plugin-version "${{ github.event.inputs.plugin_version }}" \ + --android-sdk-version "${{ github.event.inputs.android_sdk_version }}" \ + --ios-sdk-version "${{ github.event.inputs.ios_sdk_version }}" + + - name: Commit version bump + run: | + git add \ + Assets/AppsFlyer/package.json \ + Assets/AppsFlyer/AppsFlyer.cs \ + Assets/AppsFlyer/Editor/AppsFlyerDependencies.xml \ + deploy/build_unity_package.sh \ + deploy/strict_mode_build_package.sh \ + CHANGELOG.md \ + README.md + git commit -m "chore: bump to ${{ github.event.inputs.plugin_version }}" || echo "Nothing to commit" + + - name: Push release branch + if: github.event.inputs.dry_run == 'false' + run: | + git push origin "${{ steps.branch.outputs.release_branch }}" --force + + - name: Write PR body to file + if: github.event.inputs.dry_run == 'false' + run: | + VERSION="${{ github.event.inputs.plugin_version }}" + BASE_VERSION="${{ needs.validate-release.outputs.base_version }}" + { + printf '## Release %s\n\n' "$VERSION" + printf 'Field | Value\n' + printf '--- | ---\n' + printf 'Plugin version | `%s`\n' "$VERSION" + printf 'Base version | `%s`\n' "$BASE_VERSION" + printf 'Android SDK | `%s`\n' "${{ github.event.inputs.android_sdk_version }}" + printf 'iOS SDK | `%s`\n' "${{ github.event.inputs.ios_sdk_version }}" + printf 'Base branch | `%s`\n\n' "${{ github.event.inputs.base_branch }}" + printf '### RC Pipeline status\n' + printf -- '- [x] RC-PREP (this PR)\n' + printf -- '- [ ] RC-E2E (triggered automatically)\n' + } > /tmp/pr_body.md + + - name: Open / update PR to master + if: github.event.inputs.dry_run == 'false' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BRANCH="${{ steps.branch.outputs.release_branch }}" + VERSION="${{ github.event.inputs.plugin_version }}" + + PR_URL=$(gh pr list \ + --head "$BRANCH" \ + --base master \ + --state open \ + --json url \ + --jq '.[0].url' 2>/dev/null || true) + + if [[ -n "$PR_URL" ]]; then + echo "PR already exists: $PR_URL" + gh pr edit "$PR_URL" \ + --title "Release $VERSION" \ + --body-file /tmp/pr_body.md + else + gh pr create \ + --head "$BRANCH" \ + --base master \ + --title "Release $VERSION" \ + --body-file /tmp/pr_body.md + fi + + - name: Dry run summary + if: github.event.inputs.dry_run == 'true' + run: | + echo "=== DRY RUN — nothing was pushed ===" + echo "release_branch : ${{ steps.branch.outputs.release_branch }}" + echo "plugin_version : ${{ github.event.inputs.plugin_version }}" + echo "android_sdk : ${{ github.event.inputs.android_sdk_version }}" + echo "ios_sdk : ${{ github.event.inputs.ios_sdk_version }}" + git diff HEAD~1 HEAD --stat || git status + + # ── 3. Unit tests (parallel with prepare-branch) ──────────────────────────── + # Runs the existing PlayMode test suite (iOS, Android, Shared matrix) against + # the plugin source on the dispatched-from branch. Branch prep uses no Unity + # seat, so these two jobs run concurrently without license contention. + # E2E waits for BOTH prepare-branch and run-tests before starting, ensuring + # the seat is free when E2E kicks off. + run-tests: + name: Unit tests + needs: validate-release + if: github.event.inputs.skip_tests == 'false' + uses: ./.github/workflows/main.yml + secrets: inherit + + # ── 4. E2E — iOS ──────────────────────────────────────────────────────────── + run-e2e-ios: + name: E2E — iOS + needs: [prepare-branch, run-tests] + if: github.event.inputs.skip_e2e == 'false' && github.event.inputs.dry_run == 'false' + uses: ./.github/workflows/rc-e2e-ios.yml + with: + plugin_version: ${{ github.event.inputs.plugin_version }} + release_branch: ${{ needs.prepare-branch.outputs.release_branch }} + secrets: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} + ENV_FILE: ${{ secrets.ENV_FILE }} + + # ── 5. E2E — Android (after iOS to avoid Unity license contention) ────────── + run-e2e-android: + name: E2E — Android + needs: [prepare-branch, run-e2e-ios] + if: github.event.inputs.skip_e2e == 'false' && github.event.inputs.dry_run == 'false' + uses: ./.github/workflows/rc-e2e-android.yml + with: + plugin_version: ${{ github.event.inputs.plugin_version }} + release_branch: ${{ needs.prepare-branch.outputs.release_branch }} + secrets: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} + ENV_FILE: ${{ secrets.ENV_FILE }} + + # ── 6. Pre-publish gate ────────────────────────────────────────────────────── + pre-publish-gate: + name: Pre-publish gate + needs: [prepare-branch, run-tests, run-e2e-ios, run-e2e-android] + if: always() && github.event.inputs.dry_run == 'false' + runs-on: ubuntu-latest + steps: + - name: Check unit test and E2E results + run: | + TEST_RESULT="${{ needs.run-tests.result }}" + IOS_RESULT="${{ needs.run-e2e-ios.result }}" + AND_RESULT="${{ needs.run-e2e-android.result }}" + SKIP_TESTS="${{ github.event.inputs.skip_tests }}" + SKIP_E2E="${{ github.event.inputs.skip_e2e }}" + + if [[ "$SKIP_TESTS" != "true" && "$TEST_RESULT" != "success" ]]; then + echo "::error::Unit tests result: $TEST_RESULT" + exit 1 + fi + + if [[ "$SKIP_E2E" == "true" ]]; then + echo "E2E skipped by operator — gate passes automatically" + exit 0 + fi + + if [[ "$IOS_RESULT" != "success" ]]; then + echo "::error::iOS E2E result: $IOS_RESULT" + exit 1 + fi + if [[ "$AND_RESULT" != "success" ]]; then + echo "::error::Android E2E result: $AND_RESULT" + exit 1 + fi + + echo "All checks passed. RC ${{ github.event.inputs.plugin_version }} is ready for manual publish." diff --git a/.gitignore b/.gitignore index 2fc2687a..0545ce76 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,26 @@ Logs/ Packages/ Temp/ +test-app/Assets/StreamingAssets/.env Library/ /*/outputs/ +# test run reports (regenerated on every run) +.af-e2e/reports/ + +# planning / status docs (internal working documents) +RC_PIPELINE_PLAN.md +RELEASE_PROCESS.md +UNITY_RC_STATUS.md + +# unity temp / per-developer settings +test-app/.utmp/ +UserSettings/ +test-app/UserSettings/ + +# build artifacts +test-app/Build/ + #android studio build diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..82e0d25d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,72 @@ +# CLAUDE.md — AppsFlyer Unity Plugin + +## Claude Agents + +This repository includes custom agents under `.claude/agents/`. + +Current agents: +- `unity-plugin-developer` — for Unity plugin development, C#/native bridge work, SDK version bumps, billing library variants, and release-related plugin changes +- `unity-e2e-tester` — for end-to-end validation on Android emulators and iOS simulators, including AppsFlyer event verification from logs + +## Claude Skills + +This repository includes reusable skills under `.claude/skills/`. + +Plugin development skills: +- `sdk-version-bump` — bump wrapped Android/iOS SDK versions, plugin version metadata, and billing library variants +- `plugin-api-change` — add or modify Unity plugin APIs safely across C#, Android Java, and iOS Objective-C++ +- `platform-channel-debug` — debug C# ↔ native bridge issues (P/Invoke, AndroidJavaClass) +- `plugin-release` — review plugin release readiness including `.unitypackage` artifacts + +Quality / E2E skills: +- `appsflyer-event-validation` — verify expected AppsFlyer events and callbacks from logs and evidence +- `launch-log-analysis` — analyze app launch logs per session and compare expected vs actual AppsFlyer events +- `e2e-smoke-test` — run or review a basic emulator/simulator smoke test for plugin readiness + +## Working Style + +When a task matches one of the skills above, prefer using the relevant skill workflow. +When a task needs specialized reasoning, prefer the matching custom agent. +For plugin work, inspect C#, Android, and iOS layers together before changing code. +For E2E validation, rely on logs, callback payloads, and explicit evidence rather than assumptions. + +--- + +## Architecture + +C# API (`AppsFlyerSDK` namespace) → P/Invoke / AndroidJavaClass → Native Android (Java) / iOS (Objective-C++) + +### Key files +- `Assets/AppsFlyer/AppsFlyer.cs` — main public C# API (source of truth) +- `Assets/AppsFlyer/AppsFlyerAndroid.cs` — Android platform delegation (`AndroidJavaClass`/`AndroidJavaObject`) +- `Assets/AppsFlyer/AppsFlyeriOS.cs` — iOS platform delegation (`[DllImport("__Internal")]`) +- `Assets/AppsFlyer/AppsFlyerPurchaseConnector.cs` — in-app purchase validation +- `Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.mm` — Objective-C++ bridge +- `android-unity-wrapper/AppsFlyerAndroidWrapper.java` — Java bridge +- `Assets/AppsFlyer/Editor/` — Unity editor extensions +- `Assets/AppsFlyer/Tests/` — Unity playmode test suite +- `deploy/` — pre-built `.unitypackage` release artifacts (do not edit manually) + +### Platform guards +- Use `#if UNITY_IOS` / `#if UNITY_ANDROID` in shared C# files for platform-specific code +- Namespace: `AppsFlyerSDK` — do not change +- Assembly definitions (`.asmdef`) enforce modularity — do not break boundaries + +### Key version files +When bumping SDK or plugin versions, these files must stay in sync: +- Version constant in `Assets/AppsFlyer/AppsFlyer.cs` +- `android-unity-wrapper/build.gradle` — `af-android-sdk` + billing library v7 and v8 +- iOS podspec or dependency declarations +- `CHANGELOG.md` +- `deploy/` `.unitypackage` files — must be regenerated on release + +### SDK targets +- Android: af-android-sdk v6.17.6 (two billing library variants: v7 and v8) +- iOS: AppsFlyerFramework v6.17.9 +- Unity: 2019.4+, Android API 16–34, Java/Kotlin 17 + +### Billing library variants +Two Gradle build variants exist for Google Play Billing migration (v7 → v8). Both must remain buildable and consistent after any Android wrapper change. + +### CI +GitHub Actions (`.github/workflows/main.yml`) runs playmode tests for iOS, Android, and Shared platforms on every PR. diff --git a/scripts/af-scenario-runner.sh b/scripts/af-scenario-runner.sh new file mode 100755 index 00000000..f7d6be57 --- /dev/null +++ b/scripts/af-scenario-runner.sh @@ -0,0 +1,973 @@ +#!/usr/bin/env bash +# +# af-scenario-runner.sh — Unified AppsFlyer plugin scenario runner +# +# Drives a JSON-driven scenario cycle for any AppsFlyer plugin using ADB +# (Android) and xcrun simctl (iOS). Reads a test plan, executes each phase, +# validates log output against expected patterns, and produces a structured +# JSON report. Used for both pre-publish E2E (.af-e2e/test-plan.json) and +# post-publish smoke (.af-smoke/rc-test-plan.json) — the runner is the same; +# the plan is what differs. +# +# Usage: +# ./af-scenario-runner.sh --platform android --plan .af-e2e/test-plan.json +# ./af-scenario-runner.sh --platform ios --plan .af-smoke/rc-test-plan.json +# ./af-scenario-runner.sh --platform android --plan --phase phase_1 +# ./af-scenario-runner.sh --platform android --plan --dry-run +# ./af-scenario-runner.sh --platform android --plan --build +# +# Requirements: +# - bash 4+, jq +# - Android: ADB in PATH, emulator booted +# - iOS: Xcode CLI tools, simulator booted +# +# The script is agent-agnostic: any AI coding assistant (Cursor, Claude Code, +# GitHub Copilot, Windsurf) or a human can invoke it from a terminal. + +set -euo pipefail + +# Bash 5.2+ enables patsub_replacement by default, which makes `&` in the +# replacement string of ${var//pat/repl} expand to the matched pattern (sed +# style). That mangles deep link URLs like `?a=1&b=2` into `?a=1{{DEEP_LINK_URL}}b=2` +# when we substitute the URL into trigger templates below. Disable it so +# replacements are taken literally on every supported runner (macOS bash 3.2, +# Ubuntu bash 5.2+, etc.). +shopt -u patsub_replacement 2>/dev/null || true + +# ─── Defaults ──────────────────────────────────────────────────────────────── + +PLATFORM="" +PLAN_FILE="" +PHASE_FILTER="" +DRY_RUN=false +BUILD_FIRST=false +VERBOSE=false +REPORT_DIR="" +LOG_TAG="AF_QA" + +# Timestamps +RUN_ID="" +RUN_START="" + +# Counters +TOTAL_CHECKS=0 +PASSED_CHECKS=0 +FAILED_CHECKS=0 +WARNED_CHECKS=0 +ABORTED=false + +# ─── Colors ────────────────────────────────────────────────────────────────── + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +# ─── Usage ─────────────────────────────────────────────────────────────────── + +usage() { + cat < Target platform (required) + --plan Path to test-plan.json (required) + --phase Run only this phase (optional; runs all if omitted) + --build Build the app before running (optional) + --dry-run Show what would run without executing (optional) + --verbose Print extra debug output (optional) + --report-dir Override report output directory (optional) + -h, --help Show this help + +Examples: + $(basename "$0") --platform android --plan .af-e2e/test-plan.json + $(basename "$0") --platform ios --plan .af-smoke/rc-test-plan.json --phase phase_1 + $(basename "$0") --platform android --plan .af-e2e/test-plan.json --build --verbose +EOF + exit 0 +} + +# ─── Logging helpers ───────────────────────────────────────────────────────── + +log_info() { echo -e "${CYAN}[INFO]${NC} $*" >&2; } +log_ok() { echo -e "${GREEN}[PASS]${NC} $*" >&2; } +log_fail() { echo -e "${RED}[FAIL]${NC} $*" >&2; } +log_warn() { echo -e "${YELLOW}[WARN]${NC} $*" >&2; } +log_step() { echo -e "${BOLD}────── $* ──────${NC}" >&2; } +log_debug() { if $VERBOSE; then echo -e "[DEBUG] $*" >&2; fi; } + +# ─── Argument parsing ──────────────────────────────────────────────────────── + +while [[ $# -gt 0 ]]; do + case "$1" in + --platform) PLATFORM="$2"; shift 2 ;; + --plan) PLAN_FILE="$2"; shift 2 ;; + --phase) PHASE_FILTER="$2"; shift 2 ;; + --build) BUILD_FIRST=true; shift ;; + --dry-run) DRY_RUN=true; shift ;; + --verbose) VERBOSE=true; shift ;; + --report-dir) REPORT_DIR="$2"; shift 2 ;; + -h|--help) usage ;; + *) echo "Unknown option: $1"; usage ;; + esac +done + +[[ -z "$PLATFORM" ]] && { echo "Error: --platform is required"; usage; } +[[ -z "$PLAN_FILE" ]] && { echo "Error: --plan is required"; usage; } +[[ ! -f "$PLAN_FILE" ]] && { echo "Error: Plan file not found: $PLAN_FILE"; exit 1; } + +# Validate platform +case "$PLATFORM" in + android|ios) ;; + *) echo "Error: --platform must be 'android' or 'ios'"; exit 1 ;; +esac + +# Check dependencies +command -v jq >/dev/null 2>&1 || { echo "Error: jq is required but not installed. Install with: brew install jq"; exit 1; } + +if [[ "$PLATFORM" == "android" ]]; then + command -v adb >/dev/null 2>&1 || { echo "Error: adb not found in PATH"; exit 1; } +elif [[ "$PLATFORM" == "ios" ]]; then + command -v xcrun >/dev/null 2>&1 || { echo "Error: xcrun not found (install Xcode CLI tools)"; exit 1; } +fi + +# ─── Read plan ─────────────────────────────────────────────────────────────── + +PLAN=$(cat "$PLAN_FILE") +PLAN_ID=$(echo "$PLAN" | jq -r '._meta.plan_id // "unknown"') +PLUGIN_NAME=$(echo "$PLAN" | jq -r '._meta.plugin // "unknown"') + +# Platform-specific config +PACKAGE_NAME=$(echo "$PLAN" | jq -r ".config.${PLATFORM}.package_name // .config.${PLATFORM}.bundle_id // \"\"") +APP_PATH=$(echo "$PLAN" | jq -r ".config.${PLATFORM}.apk_path // .config.${PLATFORM}.app_path // \"\"") +BUILD_CMD=$(echo "$PLAN" | jq -r ".config.${PLATFORM}.build_cmd // \"\"") +ACTIVITY=$(echo "$PLAN" | jq -r ".config.${PLATFORM}.activity // \"\"") + +# Report directory +if [[ -z "$REPORT_DIR" ]]; then + REPORT_DIR=$(echo "$PLAN" | jq -r '.report.output_dir // ".af-smoke/reports/"') +fi + +# Generate run ID +RUN_START=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +RUN_ID="${PLAN_ID}-${PLATFORM}-$(date +%Y%m%d_%H%M%S)" + +log_info "Plan: ${PLAN_ID} | Plugin: ${PLUGIN_NAME} | Platform: ${PLATFORM}" +log_info "Package: ${PACKAGE_NAME}" +log_info "Run ID: ${RUN_ID}" + +if $DRY_RUN; then + log_warn "DRY RUN MODE — no commands will be executed" +fi + +# ─── Setup report directory ────────────────────────────────────────────────── + +mkdir -p "$REPORT_DIR" +REPORT_FILE="${REPORT_DIR}/${RUN_ID}.json" +PHASE_RESULTS="[]" + +# ─── Platform helpers ──────────────────────────────────────────────────────── + +# --- Android --- + +android_get_device() { + adb devices | grep -w "device" | head -1 | awk '{print $1}' +} + +android_is_installed() { + adb shell pm list packages 2>/dev/null | grep -q "$PACKAGE_NAME" +} + +android_uninstall() { + log_info "Uninstalling $PACKAGE_NAME..." + if android_is_installed; then + adb uninstall "$PACKAGE_NAME" 2>/dev/null || true + else + log_info "App not installed, skipping uninstall" + fi +} + +android_install() { + log_info "Installing $APP_PATH..." + if [[ ! -f "$APP_PATH" ]]; then + log_fail "APK not found at $APP_PATH" + if [[ -n "$BUILD_CMD" ]]; then + log_info "Hint: run with --build to build first, or manually: $BUILD_CMD" + fi + return 1 + fi + adb install -r "$APP_PATH" +} + +android_launch() { + log_info "Launching $PACKAGE_NAME..." + adb logcat -c + adb shell am start -n "${PACKAGE_NAME}/${ACTIVITY}" 2>/dev/null || \ + adb shell monkey -p "$PACKAGE_NAME" -c android.intent.category.LAUNCHER 1 2>/dev/null +} + +android_get_pid() { + adb shell pidof "$PACKAGE_NAME" 2>/dev/null | tr -d '[:space:]' +} + +android_collect_logs() { + local log_file="$1" + local tail_lines="${ANDROID_LOGCAT_TAIL_LINES:-2000}" + + # Always start from an empty file so each phase capture is self-contained. + : > "$log_file" + + # Strategy 1: Read the app's af_qa_logs.txt from internal storage via + # `run-as`. Required because Flutter debug APKs launched standalone (no + # `flutter run` host) do not forward Dart `debugPrint` to logcat, so the + # file is the only reliable source of [AF_QA] markers. The Documents dir + # path on Android is `app_flutter/` for path_provider, but newer versions + # may write directly under `files/`, so try both. `run-as` works because + # `flutter build apk --debug` produces a debuggable APK. + local found=0 + for path in app_flutter/af_qa_logs.txt files/af_qa_logs.txt; do + if adb shell "run-as $PACKAGE_NAME cat $path 2>/dev/null" >> "$log_file" 2>/dev/null; then + if [[ -s "$log_file" ]]; then + log_debug "Pulled Android QA log from $path" + found=1 + break + fi + fi + done + if [[ "$found" -eq 0 ]]; then + log_debug "No af_qa_logs.txt found via run-as; relying on logcat only" + fi + + # Strategy 2: Always also append logcat output. AppsFlyer SDK native logs + # (HTTP response codes, etc.) reach logcat regardless of the Dart-print + # routing, and the count_matches checks need them. Limit to the recent tail + # so CI does not spend a minute dumping the whole emulator buffer every phase. + adb logcat -d -t "$tail_lines" 2>&1 | grep -E "${LOG_TAG}|AppsFlyer|response code:|preparing data:" >> "$log_file" || true +} + +android_background_app() { + log_info "Backgrounding app (HOME key)..." + adb shell input keyevent KEYCODE_HOME +} + +android_trigger_deeplink() { + local url="$1" + log_info "Triggering deep link: $url" + adb shell am start -W -a android.intent.action.VIEW -d "$url" +} + +android_is_alive() { + local pid + pid=$(android_get_pid) + [[ -n "$pid" ]] +} + +# --- iOS --- + +IOS_UDID="" +IOS_LAST_PID="" + +ios_get_booted_udid() { + xcrun simctl list devices booted -j 2>/dev/null | \ + jq -r '[.devices[][] | select(.state == "Booted")] | first | .udid // empty' +} + +ios_ensure_udid() { + if [[ -z "$IOS_UDID" ]]; then + IOS_UDID=$(ios_get_booted_udid) + if [[ -z "$IOS_UDID" ]]; then + log_fail "No booted iOS simulator found. Boot one with: xcrun simctl boot " + exit 1 + fi + log_info "Using simulator: $IOS_UDID" + fi +} + +ios_is_installed() { + xcrun simctl listapps "$IOS_UDID" 2>/dev/null | grep -q "$PACKAGE_NAME" 2>/dev/null +} + +ios_uninstall() { + ios_ensure_udid + log_info "Uninstalling $PACKAGE_NAME..." + if ios_is_installed; then + xcrun simctl uninstall "$IOS_UDID" "$PACKAGE_NAME" 2>/dev/null || true + else + log_info "App not installed, skipping uninstall" + fi +} + +ios_install() { + ios_ensure_udid + log_info "Installing $APP_PATH..." + if [[ ! -d "$APP_PATH" ]]; then + log_fail "App bundle not found at $APP_PATH" + if [[ -n "$BUILD_CMD" ]]; then + log_info "Hint: run with --build to build first, or manually: $BUILD_CMD" + fi + return 1 + fi + xcrun simctl install "$IOS_UDID" "$APP_PATH" +} + +ios_launch() { + ios_ensure_udid + log_info "Launching $PACKAGE_NAME..." + # Capture launch output so we can pin log filtering to this PID. simctl + # prints ": " on success; anything else (already running, + # error) leaves IOS_LAST_PID empty and the collector falls back to the + # unfiltered window. + local out + out=$(xcrun simctl launch "$IOS_UDID" "$PACKAGE_NAME" 2>&1 || true) + echo "$out" + IOS_LAST_PID=$(echo "$out" | awk -F': ' '/^'"$PACKAGE_NAME"': [0-9]+$/ {print $2}' | tail -1) + [[ -n "$IOS_LAST_PID" ]] && log_debug "Launched PID: $IOS_LAST_PID" +} + +ios_get_pid() { + xcrun simctl spawn "$IOS_UDID" launchctl list 2>/dev/null | \ + grep "$PACKAGE_NAME" | awk '{print $1}' | head -1 +} + +ios_collect_logs() { + local log_file="$1" + + ios_ensure_udid + + # Always start from an empty file so each phase capture is self-contained. + : > "$log_file" + + # Strategy 1: Read the app's af_qa_logs.txt from the simulator filesystem. + # This file is the source of truth for [AF_QA] markers because the IOSink + # in af_qa_logger.dart guarantees every line is appended. + local sim_data_dir + sim_data_dir="$HOME/Library/Developer/CoreSimulator/Devices/${IOS_UDID}/data" + if [[ -d "$sim_data_dir" ]]; then + local qa_log + # Use the most recently modified af_qa_logs.txt — multiple stale containers can + # accumulate after repeated installs and the oldest one must not poison results. + qa_log=$(find "$sim_data_dir/Containers/Data/Application" -name "af_qa_logs.txt" -maxdepth 4 2>/dev/null | \ + xargs ls -t 2>/dev/null | head -1) + if [[ -n "$qa_log" && -f "$qa_log" ]]; then + log_debug "Found iOS QA log file: $qa_log" + cat "$qa_log" >> "$log_file" + fi + fi + + # Strategy 2: Always also append simctl log show output. The file logger + # only carries [AF_QA] lines; SDK HTTP traffic (response code:200, etc.) + # only shows up via os_log and is required by count_matches checks. + # Window is 240s so back-to-back phases (cold launch -> 60s settle -> deep + # link -> 12s wait) still fit. The grep filter also captures URL-open + # events from CoreSimulatorBridge / launchservices so deep-link triage has + # something to look at when onDeepLinking doesn't fire. + # + # Pin the predicate to the current Runner PID when known so prior-run + # entries that still sit inside the rolling window can't poison absent- + # pattern checks (e.g. phase_1 no_fatal_errors). Falls back to unfiltered + # when PID is unknown (first phase before launch, or `simctl launch` + # failed to print one). + log_debug "Appending simctl log show output" + local predicate_args=() + if [[ -n "$IOS_LAST_PID" ]]; then + predicate_args=(--predicate "processIdentifier == $IOS_LAST_PID") + fi + xcrun simctl spawn "$IOS_UDID" log show \ + --last 240s --style compact ${predicate_args[@]+"${predicate_args[@]}"} 2>&1 | \ + grep -E "${LOG_TAG}|appsflyer|CFNetwork:Summary|response_status|response code|Opening URL|launchservices|openURL|continueUserActivity" >> "$log_file" || true + + # Best-effort screenshot for failure triage (no-op if nothing booted). + local shot_dir="${log_file%/*}" + local shot_file="${shot_dir}/${log_file##*/}.png" + shot_file="${shot_file%_logs.txt.png}_screen.png" + xcrun simctl io "$IOS_UDID" screenshot "$shot_file" 2>/dev/null || true +} + +ios_background_app() { + ios_ensure_udid + log_info "Backgrounding app (launching Safari)..." + xcrun simctl launch "$IOS_UDID" com.apple.mobilesafari 2>/dev/null || true +} + +ios_trigger_deeplink() { + local url="$1" + ios_ensure_udid + log_info "Triggering deep link: $url" + xcrun simctl openurl "$IOS_UDID" "$url" 2>/dev/null || true +} + +# ─── Platform dispatcher ──────────────────────────────────────────────────── + +platform_uninstall() { + if [[ "$PLATFORM" == "android" ]]; then android_uninstall; else ios_uninstall; fi +} + +platform_install() { + if [[ "$PLATFORM" == "android" ]]; then android_install; else ios_install; fi +} + +platform_launch() { + if [[ "$PLATFORM" == "android" ]]; then android_launch; else ios_launch; fi +} + +platform_collect_logs() { + if [[ "$PLATFORM" == "android" ]]; then android_collect_logs "$1"; else ios_collect_logs "$1"; fi +} + +platform_background() { + if [[ "$PLATFORM" == "android" ]]; then android_background_app; else ios_background_app; fi +} + +platform_trigger_deeplink() { + if [[ "$PLATFORM" == "android" ]]; then android_trigger_deeplink "$1"; else ios_trigger_deeplink "$1"; fi +} + +# Print the device-side af_qa_logs.txt to stdout (best effort, empty on miss). +# Used by `wait_for_qa_marker` to poll mid-phase without reshuffling the full +# log-collection pipeline. +platform_peek_qa_log() { + if [[ "$PLATFORM" == "android" ]]; then + for path in app_flutter/af_qa_logs.txt files/af_qa_logs.txt; do + adb shell "run-as $PACKAGE_NAME cat $path 2>/dev/null" 2>/dev/null && return 0 + done + return 0 + fi + ios_ensure_udid + local sim_data_dir + sim_data_dir="$HOME/Library/Developer/CoreSimulator/Devices/${IOS_UDID}/data" + [[ -d "$sim_data_dir" ]] || return 0 + local qa_log + qa_log=$(find "$sim_data_dir/Containers/Data/Application" \ + -name "af_qa_logs.txt" -maxdepth 4 2>/dev/null | head -1) + [[ -n "$qa_log" && -f "$qa_log" ]] || return 0 + cat "$qa_log" 2>/dev/null || true +} + +# wait_for_qa_marker [interval_sec] +# Polls the device-side QA log and returns 0 as soon as the marker +# appears, or after the timeout (also 0 — caller still runs validation against +# whatever logs exist). Lets local runs finish quickly while CI runs use the +# full ceiling for slow no-KVM emulators / cold macOS sims. +wait_for_qa_marker() { + local marker="$1" + local timeout_sec="$2" + local interval="${3:-3}" + local start_ts now elapsed remaining sleep_for + + start_ts=$(date +%s) + log_info "Waiting up to ${timeout_sec}s for marker: ${marker} (poll every ${interval}s)" + while true; do + now=$(date +%s) + elapsed=$(( now - start_ts )) + if (( elapsed >= timeout_sec )); then + break + fi + + if platform_peek_qa_log | grep -qF -- "$marker" 2>/dev/null; then + log_info "Marker observed after ${elapsed}s" + return 0 + fi + + remaining=$(( timeout_sec - elapsed )) + sleep_for="$interval" + if (( sleep_for > remaining )); then + sleep_for="$remaining" + fi + sleep "$sleep_for" + done + log_warn "Marker not observed within ${timeout_sec}s; proceeding to log collection anyway" + return 0 +} + +run_phase_command() { + local label="$1" + local command="$2" + local allow_failure="$3" + local output status + + log_info "${label}: ${command}" + set +e + output=$(eval "$command" 2>&1) + status=$? + set -e + + if [[ -n "$output" ]]; then + printf '%s\n' "$output" >&2 + fi + + if [[ "$status" -ne 0 ]]; then + if [[ "$allow_failure" == "true" ]]; then + log_warn "${label} failed with exit code ${status}; continuing" + return 0 + fi + log_fail "${label} failed with exit code ${status}" + return "$status" + fi +} + +deep_link_wait_marker() { + local phase_json="$1" + echo "$phase_json" | jq -r ' + [ + .checks[]? + | select(.type == "log_contains") + | .pattern + | select(startswith("deepLinkValue=")) + ][0] // empty + ' +} + +# ─── Build ─────────────────────────────────────────────────────────────────── + +build_app() { + if [[ -z "$BUILD_CMD" ]]; then + log_warn "No build_cmd configured in test plan for $PLATFORM" + return 1 + fi + log_step "Building app" + log_info "Running: $BUILD_CMD" + if ! $DRY_RUN; then + (eval "$BUILD_CMD") + fi +} + +# ─── Log validation engine ─────────────────────────────────────────────────── + +# validate_check +# Returns a JSON object: {"status": "PASS|FAIL|WARN", "evidence": "..."} +validate_check() { + local log_file="$1" + local check_json="$2" + + local check_id check_type pattern description fail_action + check_id=$(echo "$check_json" | jq -r '.id') + check_type=$(echo "$check_json" | jq -r '.type') + description=$(echo "$check_json" | jq -r '.description') + fail_action=$(echo "$check_json" | jq -r '.fail_action // "fail"') + + log_debug "Validating check: $check_id ($check_type)" + + case "$check_type" in + + log_contains) + pattern=$(echo "$check_json" | jq -r '.pattern') + local match + match=$(grep -F "$pattern" "$log_file" 2>/dev/null | head -1 || true) + if [[ -n "$match" ]]; then + # Optional payload_check + local payload_field payload_expected + payload_field=$(echo "$check_json" | jq -r '.payload_check.field // empty') + if [[ -n "$payload_field" ]]; then + payload_expected=$(echo "$check_json" | jq -r '.payload_check.expected') + if echo "$match" | grep -q "${payload_field}.*${payload_expected}" 2>/dev/null || \ + echo "$match" | grep -q "\"${payload_field}\":.*${payload_expected}" 2>/dev/null || \ + echo "$match" | grep -q "${payload_field}=${payload_expected}" 2>/dev/null || \ + echo "$match" | grep -q "${payload_field}: ${payload_expected}" 2>/dev/null; then + jq -n --arg evidence "$(echo "$match" | head -c 500)" \ + '{status: "PASS", evidence: $evidence}' + else + jq -n \ + --arg field "$payload_field" \ + --arg expected "$payload_expected" \ + --arg line "$(echo "$match" | head -c 300)" \ + '{status: "FAIL", evidence: "Pattern found but payload check failed: \($field) != \($expected). Line: \($line)"}' + fi + else + echo "{\"status\":\"PASS\",\"evidence\":$(echo "$match" | head -c 500 | jq -Rs .)}" + fi + else + echo "{\"status\":\"FAIL\",\"evidence\":\"Pattern not found in logs: ${pattern}\"}" + fi + ;; + + count_matches) + pattern=$(echo "$check_json" | jq -r '.pattern') + local minimum + minimum=$(echo "$check_json" | jq -r '.minimum // 1') + local count + count=$(grep -cE "$pattern" "$log_file" 2>/dev/null) || count=0 + if [[ "$count" -ge "$minimum" ]]; then + echo "{\"status\":\"PASS\",\"evidence\":\"Found ${count} matches (minimum: ${minimum})\"}" + else + echo "{\"status\":\"FAIL\",\"evidence\":\"Found only ${count} matches (minimum: ${minimum})\"}" + fi + ;; + + absent) + local patterns_json patterns_arr status evidence + patterns_json=$(echo "$check_json" | jq -r '.patterns // []') + status="PASS" + evidence="No forbidden patterns found" + while IFS= read -r forbidden_pattern; do + forbidden_pattern=$(echo "$forbidden_pattern" | jq -r '.') + local found + found=$(grep -F "$forbidden_pattern" "$log_file" 2>/dev/null | head -1 || true) + if [[ -n "$found" ]]; then + status="FAIL" + evidence="Forbidden pattern found: ${forbidden_pattern} -> $(echo "$found" | head -c 200)" + break + fi + done < <(echo "$patterns_json" | jq -c '.[]') + echo "{\"status\":\"${status}\",\"evidence\":$(echo "$evidence" | jq -Rs .)}" + ;; + + regex_match) + pattern=$(echo "$check_json" | jq -r '.pattern') + local match + match=$(grep -E "$pattern" "$log_file" 2>/dev/null | head -1 || true) + if [[ -n "$match" ]]; then + echo "{\"status\":\"PASS\",\"evidence\":$(echo "$match" | head -c 500 | jq -Rs .)}" + else + echo "{\"status\":\"FAIL\",\"evidence\":\"Regex not matched in logs: ${pattern}\"}" + fi + ;; + + *) + echo "{\"status\":\"WARN\",\"evidence\":\"Unknown check type: ${check_type}\"}" + ;; + esac +} + +# ─── Phase execution ───────────────────────────────────────────────────────── + +# run_phase +run_phase() { + local phase_json="$1" + + local phase_id phase_name requires_fresh scenario_ref wait_sec + phase_id=$(echo "$phase_json" | jq -r '.id') + phase_name=$(echo "$phase_json" | jq -r '.name') + requires_fresh=$(echo "$phase_json" | jq -r '.requires_fresh_install // false') + scenario_ref=$(echo "$phase_json" | jq -r '.scenario_ref // "N/A"') + wait_sec=$(echo "$phase_json" | jq -r '.wait_after_launch_sec // 25') + local wait_trigger_sec + wait_trigger_sec=$(echo "$phase_json" | jq -r '.wait_after_trigger_sec // 5') + + log_step "Phase: ${phase_name} [${phase_id}] (Scenario: ${scenario_ref})" + + local phase_log_file="${REPORT_DIR}/${RUN_ID}_${phase_id}_logs.txt" + local phase_status="PASS" + local checks_json="{}" + + if $DRY_RUN; then + log_info "[DRY RUN] Would execute phase: $phase_name" + if [[ "$requires_fresh" == "true" ]]; then + log_info "[DRY RUN] Would uninstall + reinstall $PACKAGE_NAME" + fi + log_info "[DRY RUN] Would launch app and wait ${wait_sec}s" + log_info "[DRY RUN] Would collect logs and validate $(echo "$phase_json" | jq '.checks | length') checks" + + # Produce a dry-run result + local dry_result + dry_result=$(jq -n \ + --arg pid "$phase_id" \ + --arg ps "DRY_RUN" \ + '{phase_id: $pid, status: $ps, checks: {}, log_file: "N/A"}') + PHASE_RESULTS=$(echo "$PHASE_RESULTS" | jq --argjson r "$dry_result" '. + [$r]') + return + fi + + # Fresh install if required + if [[ "$requires_fresh" == "true" ]]; then + platform_uninstall + sleep 1 + if ! platform_install; then + log_fail "Installation failed — aborting phase" + phase_status="BLOCKED" + local blocked_result + blocked_result=$(jq -n \ + --arg pid "$phase_id" \ + --arg ps "$phase_status" \ + '{phase_id: $pid, status: $ps, checks: {}, log_file: "N/A", note: "Installation failed"}') + PHASE_RESULTS=$(echo "$PHASE_RESULTS" | jq --argjson r "$blocked_result" '. + [$r]') + return + fi + sleep 1 + + platform_launch + # Poll the QA log for the auto-run-complete marker rather than always + # sleeping the full ceiling. Use a slower interval here because each ADB + # `run-as cat` is costly on GitHub's emulator. + wait_for_qa_marker "[AF_QA][AUTO_APIS] --- Auto run complete ---" "$wait_sec" 10 + fi + + # Pre-actions (deep link phases: background the app, etc.) + local pre_actions + pre_actions=$(echo "$phase_json" | jq -r ".pre_actions.${PLATFORM} // empty") + if [[ -n "$pre_actions" && "$pre_actions" != "null" ]]; then + log_info "Executing pre-actions..." + while IFS= read -r action; do + action=$(echo "$action" | jq -r '.') + action="${action//\{\{BUNDLE_ID\}\}/$PACKAGE_NAME}" + action="${action//\{\{PACKAGE_NAME\}\}/$PACKAGE_NAME}" + if [[ "$PLATFORM" == "ios" ]]; then + ios_ensure_udid + action="${action//\{\{UDID\}\}/$IOS_UDID}" + fi + run_phase_command "Pre-action" "$action" true + done < <(echo "$phase_json" | jq -c ".pre_actions.${PLATFORM}[]") + fi + + # Trigger deep link if present + local deep_link_url + deep_link_url=$(echo "$phase_json" | jq -r '.deep_link_url // empty') + if [[ -n "$deep_link_url" ]]; then + # Use platform-specific trigger command or generic + local trigger_cmd + trigger_cmd=$(echo "$phase_json" | jq -r ".trigger.${PLATFORM} // empty") + if [[ -n "$trigger_cmd" && "$trigger_cmd" != "null" ]]; then + trigger_cmd="${trigger_cmd//\{\{DEEP_LINK_URL\}\}/$deep_link_url}" + trigger_cmd="${trigger_cmd//\{\{BUNDLE_ID\}\}/$PACKAGE_NAME}" + trigger_cmd="${trigger_cmd//\{\{PACKAGE_NAME\}\}/$PACKAGE_NAME}" + if [[ "$PLATFORM" == "ios" ]]; then + ios_ensure_udid + trigger_cmd="${trigger_cmd//\{\{UDID\}\}/$IOS_UDID}" + fi + if ! run_phase_command "Deep link trigger" "$trigger_cmd" false; then + log_warn "Deep link trigger failed; continuing to collect logs and run checks" + fi + else + if ! platform_trigger_deeplink "$deep_link_url"; then + log_warn "Deep link trigger failed; continuing to collect logs and run checks" + fi + fi + + local deep_link_marker + deep_link_marker=$(deep_link_wait_marker "$phase_json") + if [[ -n "$deep_link_marker" ]]; then + wait_for_qa_marker "$deep_link_marker" "$wait_trigger_sec" 3 + else + log_info "Waiting ${wait_trigger_sec}s for deep link to propagate..." + sleep "$wait_trigger_sec" + fi + fi + + # Collect logs + log_info "Collecting logs..." + platform_collect_logs "$phase_log_file" + + local log_lines + log_lines=$(wc -l < "$phase_log_file" 2>/dev/null | tr -d ' ') + log_info "Collected ${log_lines} log lines" + + if [[ "$log_lines" -eq 0 ]]; then + log_warn "No logs collected — check that the app is running and logging with [AF_QA]" + fi + + # Validate each check + local num_checks + num_checks=$(echo "$phase_json" | jq '.checks | length') + log_info "Running ${num_checks} checks..." + + local i=0 + while [[ $i -lt $num_checks ]]; do + local check + check=$(echo "$phase_json" | jq -c ".checks[$i]") + local check_id + check_id=$(echo "$check" | jq -r '.id') + local check_desc + check_desc=$(echo "$check" | jq -r '.description') + local fail_action + fail_action=$(echo "$check" | jq -r '.fail_action // "fail"') + + local result + result=$(validate_check "$phase_log_file" "$check") + local check_status + check_status=$(echo "$result" | jq -r '.status') + local evidence + evidence=$(echo "$result" | jq -r '.evidence') + + TOTAL_CHECKS=$((TOTAL_CHECKS + 1)) + + if [[ "$check_status" == "PASS" ]]; then + log_ok "$check_id: $check_desc" + PASSED_CHECKS=$((PASSED_CHECKS + 1)) + elif [[ "$check_status" == "WARN" ]]; then + log_warn "$check_id: $check_desc — $evidence" + WARNED_CHECKS=$((WARNED_CHECKS + 1)) + else + log_fail "$check_id: $check_desc — $evidence" + FAILED_CHECKS=$((FAILED_CHECKS + 1)) + phase_status="FAIL" + + if [[ "$fail_action" == "abort" ]]; then + log_fail "Abort triggered by $check_id — skipping remaining checks in this phase" + ABORTED=true + break + fi + fi + + result=$(echo "$result" | tr -d '\000-\031') + checks_json=$(echo "$checks_json" | jq --arg k "$check_id" --argjson v "$result" '. + {($k): $v}') + i=$((i + 1)) + done + + # Produce phase result + local phase_result + phase_result=$(jq -n \ + --arg pid "$phase_id" \ + --arg ps "$phase_status" \ + --argjson ch "$checks_json" \ + --arg lf "$phase_log_file" \ + '{phase_id: $pid, status: $ps, checks: $ch, log_file: $lf}') + PHASE_RESULTS=$(echo "$PHASE_RESULTS" | jq --argjson r "$phase_result" '. + [$r]') + + echo "" +} + +# ─── Main ──────────────────────────────────────────────────────────────────── + +main() { + log_step "AppsFlyer Smoke Runner" + log_info "Started at $RUN_START" + + # Verify device/simulator is available (skip in dry-run) + if $DRY_RUN; then + if [[ "$PLATFORM" == "android" ]]; then + local device + device=$(android_get_device 2>/dev/null || true) + log_info "Android device: ${device:-}" + else + IOS_UDID=$(ios_get_booted_udid 2>/dev/null || true) + log_info "iOS simulator: ${IOS_UDID:-}" + fi + else + if [[ "$PLATFORM" == "android" ]]; then + local device + device=$(android_get_device) + if [[ -z "$device" ]]; then + log_fail "No Android device/emulator found. Start one with: emulator -avd " + exit 1 + fi + log_info "Android device: $device" + elif [[ "$PLATFORM" == "ios" ]]; then + ios_ensure_udid + fi + fi + + # Build if requested + if $BUILD_FIRST; then + build_app + fi + + # Get phases from the plan + local num_phases + num_phases=$(echo "$PLAN" | jq '.phases | length') + log_info "Test plan has ${num_phases} phases" + + local p=0 + while [[ $p -lt $num_phases ]]; do + local phase + phase=$(echo "$PLAN" | jq -c ".phases[$p]") + local pid + pid=$(echo "$phase" | jq -r '.id') + + # Apply phase filter if set + if [[ -n "$PHASE_FILTER" && "$pid" != "$PHASE_FILTER" ]]; then + log_debug "Skipping phase $pid (filter: $PHASE_FILTER)" + p=$((p + 1)) + continue + fi + + run_phase "$phase" + + if $ABORTED; then + log_warn "Run aborted after phase $pid" + break + fi + + p=$((p + 1)) + done + + # ── Final report ────────────────────────────────────────────────────────── + + local overall_status="PASS" + if [[ $FAILED_CHECKS -gt 0 ]]; then + overall_status="FAIL" + fi + if $ABORTED; then + overall_status="ABORTED" + fi + + local run_end + run_end=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + local start_epoch end_epoch duration_sec + start_epoch=$(date -j -f "%Y-%m-%dT%H:%M:%SZ" "$RUN_START" +%s 2>/dev/null || date -d "$RUN_START" +%s 2>/dev/null || echo "0") + end_epoch=$(date +%s) + duration_sec=$(( end_epoch - start_epoch )) + + local device_name="" + if [[ "$PLATFORM" == "android" ]]; then + device_name=$(android_get_device 2>/dev/null || echo "N/A") + else + device_name="${IOS_UDID:-N/A}" + fi + + local report + report=$(jq -n \ + --arg rid "$RUN_ID" \ + --arg plat "$PLATFORM" \ + --arg dev "$device_name" \ + --arg plan "$PLAN_ID" \ + --arg plugin "$PLUGIN_NAME" \ + --arg status "$overall_status" \ + --arg start "$RUN_START" \ + --arg end "$run_end" \ + --argjson dur "$duration_sec" \ + --argjson total "$TOTAL_CHECKS" \ + --argjson passed "$PASSED_CHECKS" \ + --argjson failed "$FAILED_CHECKS" \ + --argjson warned "$WARNED_CHECKS" \ + --argjson phases "$PHASE_RESULTS" \ + '{ + run_id: $rid, + platform: $plat, + device: $dev, + plan_id: $plan, + plugin: $plugin, + overall_status: $status, + started_at: $start, + finished_at: $end, + duration_sec: $dur, + total_checks: $total, + passed: $passed, + failed: $failed, + warned: $warned, + phases: $phases + }') + + # Write report + if ! $DRY_RUN; then + echo "$report" | jq '.' > "$REPORT_FILE" + # Also write a latest.json symlink + ln -sf "$(basename "$REPORT_FILE")" "${REPORT_DIR}/latest.json" + log_info "Report saved to: $REPORT_FILE" + fi + + # Print summary + echo "" + log_step "Summary" + echo -e " Plan: ${PLAN_ID}" + echo -e " Plugin: ${PLUGIN_NAME}" + echo -e " Platform: ${PLATFORM}" + echo -e " Device: ${device_name}" + echo -e " Checks: ${PASSED_CHECKS}/${TOTAL_CHECKS} passed, ${FAILED_CHECKS} failed, ${WARNED_CHECKS} warned" + + if [[ "$overall_status" == "PASS" ]]; then + echo -e " Status: ${GREEN}${BOLD}PASS${NC}" + elif [[ "$overall_status" == "ABORTED" ]]; then + echo -e " Status: ${RED}${BOLD}ABORTED${NC}" + else + echo -e " Status: ${RED}${BOLD}FAIL${NC}" + fi + echo "" + + # Exit with appropriate code + if [[ "$overall_status" != "PASS" ]]; then + exit 1 + fi +} + +main diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh new file mode 100755 index 00000000..f876770a --- /dev/null +++ b/scripts/bump-version.sh @@ -0,0 +1,114 @@ +#!/usr/bin/env bash +# +# bump-version.sh — Update all AppsFlyer Unity plugin version surfaces +# +# Usage: +# ./scripts/bump-version.sh \ +# --plugin-version 6.18.0-rc1 \ +# --android-sdk-version 6.18.0 \ +# --ios-sdk-version 6.18.0 +# +# The unity-wrapper version is always derived from --plugin-version. +# Run from the repo root. + +set -euo pipefail + +PLUGIN_VERSION="" +ANDROID_SDK_VERSION="" +IOS_SDK_VERSION="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --plugin-version) PLUGIN_VERSION="$2"; shift 2 ;; + --android-sdk-version) ANDROID_SDK_VERSION="$2"; shift 2 ;; + --ios-sdk-version) IOS_SDK_VERSION="$2"; shift 2 ;; + *) echo "Unknown option: $1"; exit 1 ;; + esac +done + +[[ -z "$PLUGIN_VERSION" ]] && { echo "Error: --plugin-version is required"; exit 1; } +[[ -z "$ANDROID_SDK_VERSION" ]] && { echo "Error: --android-sdk-version is required"; exit 1; } +[[ -z "$IOS_SDK_VERSION" ]] && { echo "Error: --ios-sdk-version is required"; exit 1; } + +UNITY_WRAPPER_VERSION="$PLUGIN_VERSION" + +echo "Bumping versions:" +echo " plugin: $PLUGIN_VERSION" +echo " android-sdk: $ANDROID_SDK_VERSION" +echo " ios-sdk: $IOS_SDK_VERSION" +echo " unity-wrapper: $UNITY_WRAPPER_VERSION" +echo "" + +# ── 1. Assets/AppsFlyer/package.json ───────────────────────────────────────── +PKG_JSON="Assets/AppsFlyer/package.json" +echo "[1/8] $PKG_JSON" +sed -i.bak "s/\"version\": \"[^\"]*\"/\"version\": \"$PLUGIN_VERSION\"/" "$PKG_JSON" +rm -f "${PKG_JSON}.bak" + +# ── 2. Assets/AppsFlyer/AppsFlyer.cs ───────────────────────────────────────── +AF_CS="Assets/AppsFlyer/AppsFlyer.cs" +echo "[2/8] $AF_CS" +sed -i.bak "s/kAppsFlyerPluginVersion = \"[^\"]*\"/kAppsFlyerPluginVersion = \"$PLUGIN_VERSION\"/" "$AF_CS" +rm -f "${AF_CS}.bak" + +# ── 3-6. Assets/AppsFlyer/Editor/AppsFlyerDependencies.xml ─────────────────── +DEPS_XML="Assets/AppsFlyer/Editor/AppsFlyerDependencies.xml" +echo "[3/8] $DEPS_XML — af-android-sdk" +sed -i.bak "s|af-android-sdk:[^\"]*|af-android-sdk:$ANDROID_SDK_VERSION|" "$DEPS_XML" + +echo "[4/8] $DEPS_XML — unity-wrapper" +sed -i.bak "s|unity-wrapper:[^\"]*|unity-wrapper:$UNITY_WRAPPER_VERSION|" "$DEPS_XML" + +echo "[5/8] $DEPS_XML — AppsFlyerFramework" +sed -i.bak "s|name=\"AppsFlyerFramework\" version=\"[^\"]*\"|name=\"AppsFlyerFramework\" version=\"$IOS_SDK_VERSION\"|" "$DEPS_XML" + +echo "[6/8] $DEPS_XML — PurchaseConnector" +sed -i.bak "s|name=\"PurchaseConnector\" version=\"[^\"]*\"|name=\"PurchaseConnector\" version=\"$IOS_SDK_VERSION\"|" "$DEPS_XML" +rm -f "${DEPS_XML}.bak" + +# ── 7. deploy/build_unity_package.sh ───────────────────────────────────────── +BUILD_SH="deploy/build_unity_package.sh" +echo "[7/8] $BUILD_SH" +sed -i.bak "s|PACKAGE_NAME=\"appsflyer-unity-plugin-[^\"]*\.unitypackage\"|PACKAGE_NAME=\"appsflyer-unity-plugin-${PLUGIN_VERSION}.unitypackage\"|" "$BUILD_SH" +rm -f "${BUILD_SH}.bak" + +# ── 8. deploy/strict_mode_build_package.sh ─────────────────────────────────── +STRICT_SH="deploy/strict_mode_build_package.sh" +echo "[8/8] $STRICT_SH" +sed -i.bak "s|PACKAGE_NAME=\"appsflyer-unity-plugin-strict-mode-[^\"]*\.unitypackage\"|PACKAGE_NAME=\"appsflyer-unity-plugin-strict-mode-${PLUGIN_VERSION}.unitypackage\"|" "$STRICT_SH" +rm -f "${STRICT_SH}.bak" + +# ── CHANGELOG.md — prepend new version header if not already present ────────── +CHANGELOG="CHANGELOG.md" +if [[ -f "$CHANGELOG" ]]; then + if ! grep -q "^## $PLUGIN_VERSION" "$CHANGELOG"; then + TODAY=$(date +%Y-%m-%d) + TMP=$(mktemp) + # Prepend the new section after any top-level title (# ...) or at file start + awk -v ver="$PLUGIN_VERSION" -v date="$TODAY" ' + NR==1 && /^# / { print; print ""; print "## " ver " (" date ")"; print ""; next } + NR==1 { print "## " ver " (" date ")"; print ""; print; next } + { print } + ' "$CHANGELOG" > "$TMP" + mv "$TMP" "$CHANGELOG" + echo "[+] CHANGELOG.md — prepended ## $PLUGIN_VERSION" + else + echo "[~] CHANGELOG.md — ## $PLUGIN_VERSION already present, skipped" + fi +fi + +# ── README.md — update inline native SDK version references ────────────────── +README="README.md" +if [[ -f "$README" ]]; then + # Replace versioned maven artifact refs and CocoaPods pod versions + # Pattern: af-android-sdk: → af-android-sdk: + sed -i.bak "s|af-android-sdk:[0-9][0-9.]*|af-android-sdk:$ANDROID_SDK_VERSION|g" "$README" + # Pattern: AppsFlyerFramework/ or AppsFlyerFramework ~> + sed -i.bak "s|AppsFlyerFramework/[0-9][0-9.]*|AppsFlyerFramework/$IOS_SDK_VERSION|g" "$README" + sed -i.bak "s|AppsFlyerFramework', '[0-9][0-9.]*|AppsFlyerFramework', '$IOS_SDK_VERSION|g" "$README" + rm -f "${README}.bak" + echo "[+] README.md — updated native SDK version references" +fi + +echo "" +echo "Done. Verify with: git diff" diff --git a/scripts/ios-pod-install.sh b/scripts/ios-pod-install.sh new file mode 100755 index 00000000..5538ae23 --- /dev/null +++ b/scripts/ios-pod-install.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# ios-pod-install.sh — Write Podfile into the Unity-generated Xcode project and run pod install. +# Called after game-ci/unity-builder (or local Unity batchmode) generates the Xcode project. +# +# Usage: +# ./scripts/ios-pod-install.sh +# ./scripts/ios-pod-install.sh test-app/Build/iOS + +set -euo pipefail + +IOS_BUILD_DIR="${1:-test-app/Build/iOS}" + +if [ ! -d "$IOS_BUILD_DIR" ]; then + echo "[ios-pod-install] ERROR: iOS build dir not found: $IOS_BUILD_DIR" >&2 + exit 1 +fi + +echo "[ios-pod-install] Writing Podfile to $IOS_BUILD_DIR" + +cat > "$IOS_BUILD_DIR/Podfile" <<'PODFILE' +platform :ios, '15.0' + +use_frameworks! :linkage => :static + +target 'Unity-iPhone' do + pod 'AppsFlyerFramework', '6.17.9' + pod 'PurchaseConnector', '6.17.9' + + target 'Unity-iPhone Tests' do + inherit! :search_paths + end +end + +target 'UnityFramework' do + pod 'AppsFlyerFramework', '6.17.9' + pod 'PurchaseConnector', '6.17.9' +end +PODFILE + +echo "[ios-pod-install] Running pod install in $IOS_BUILD_DIR" +cd "$IOS_BUILD_DIR" +pod install + +echo "[ios-pod-install] Patching project.pbxproj — removing hardcoded SDKROOT so -sdk flag wins" +# Unity hardcodes SDKROOT = iphoneos at the target level which overrides xcodebuild's -sdk flag. +# Must run after pod install since pod install rewrites parts of the project. +# Removing it lets the command-line -sdk iphonesimulator take effect for simulator builds. +sed -i '' 's/SDKROOT = iphoneos;//g' "Unity-iPhone.xcodeproj/project.pbxproj" + +echo "[ios-pod-install] Done. Build with Unity-iPhone.xcworkspace" diff --git a/test-app/Assets/Editor.meta b/test-app/Assets/Editor.meta new file mode 100644 index 00000000..aaffd5cc --- /dev/null +++ b/test-app/Assets/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: eb64c6a69bafe4a7baae9e28acf63676 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/test-app/Assets/Editor/BuildScript.cs b/test-app/Assets/Editor/BuildScript.cs new file mode 100644 index 00000000..66c5813c --- /dev/null +++ b/test-app/Assets/Editor/BuildScript.cs @@ -0,0 +1,89 @@ +#if UNITY_EDITOR +using System; +using System.IO; +using UnityEditor; +using UnityEditor.Build.Reporting; +using UnityEngine; + +/// +/// CI build entry point called by game-ci/unity-builder via -executeMethod. +/// Reads output path from the UNITY_BUILD_PATH env var (set by game-ci). +/// +public static class BuildScript +{ + public static void BuildAndroid() + { + string outputPath = GetBuildPath("Build/Android/com.appsflyer.engagement.apk"); + Directory.CreateDirectory(Path.GetDirectoryName(outputPath)!); + + PlayerSettings.SetApplicationIdentifier(BuildTargetGroup.Android, "com.appsflyer.engagement"); + PlayerSettings.productName = "UnityQATest"; + PlayerSettings.Android.bundleVersionCode = 1; + PlayerSettings.bundleVersion = "1.0"; + + var options = new BuildPlayerOptions + { + scenes = new[] { "Assets/Scenes/QATestScene.unity" }, + locationPathName = outputPath, + target = BuildTarget.Android, + options = BuildOptions.Development | BuildOptions.AllowDebugging + }; + + Build(options); + } + + public static void BuildIOS() + { + BuildIOSInternal(simulator: false); + } + + public static void BuildIOSSimulator() + { + BuildIOSInternal(simulator: true); + } + + static void BuildIOSInternal(bool simulator) + { + string outputPath = GetBuildPath(simulator ? "Build/iOS-Simulator" : "Build/iOS"); + Directory.CreateDirectory(outputPath); + + PlayerSettings.SetApplicationIdentifier(BuildTargetGroup.iOS, "com.appsflyer.engagement"); + PlayerSettings.productName = "UnityQATest"; + PlayerSettings.bundleVersion = "1.0"; + PlayerSettings.iOS.buildNumber = "1"; + PlayerSettings.iOS.sdkVersion = simulator + ? iOSSdkVersion.SimulatorSDK + : iOSSdkVersion.DeviceSDK; + + var options = new BuildPlayerOptions + { + scenes = new[] { "Assets/Scenes/QATestScene.unity" }, + locationPathName = outputPath, + target = BuildTarget.iOS, + options = BuildOptions.Development | BuildOptions.AllowDebugging + }; + + Build(options); + } + + static void Build(BuildPlayerOptions options) + { + BuildReport report = BuildPipeline.BuildPlayer(options); + if (report.summary.result == BuildResult.Succeeded) + { + Debug.Log("[BuildScript] Build succeeded: " + options.locationPathName); + } + else + { + Debug.LogError("[BuildScript] Build FAILED. Result: " + report.summary.result); + EditorApplication.Exit(1); + } + } + + static string GetBuildPath(string fallback) + { + string env = Environment.GetEnvironmentVariable("UNITY_BUILD_PATH"); + return string.IsNullOrEmpty(env) ? fallback : env; + } +} +#endif diff --git a/test-app/Assets/Editor/BuildScript.cs.meta b/test-app/Assets/Editor/BuildScript.cs.meta new file mode 100644 index 00000000..4afda719 --- /dev/null +++ b/test-app/Assets/Editor/BuildScript.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c03d754689b9842cd90f44aceeac3e0c \ No newline at end of file diff --git a/test-app/Assets/Editor/iOSBuildPostProcess.cs b/test-app/Assets/Editor/iOSBuildPostProcess.cs new file mode 100644 index 00000000..d070674a --- /dev/null +++ b/test-app/Assets/Editor/iOSBuildPostProcess.cs @@ -0,0 +1,73 @@ +#if UNITY_EDITOR +using System.IO; +using UnityEditor; +using UnityEditor.Callbacks; +using UnityEditor.iOS.Xcode; + +public static class iOSBuildPostProcess +{ + [PostProcessBuild(100)] + public static void OnPostProcessBuild(BuildTarget target, string buildPath) + { + if (target != BuildTarget.iOS) + return; + + AddURLScheme(buildPath); + EnableSimulatorSupport(buildPath); + } + + static void EnableSimulatorSupport(string buildPath) + { + string projPath = PBXProject.GetPBXProjectPath(buildPath); + var proj = new PBXProject(); + proj.ReadFromFile(projPath); + + string[] guids = new[] + { + proj.GetUnityMainTargetGuid(), + proj.GetUnityFrameworkTargetGuid(), + }; + + foreach (string guid in guids) + { + foreach (string config in new[] { "Debug", "Release", "ReleaseForProfiling", "ReleaseForRunning" }) + { + string configGuid = proj.BuildConfigByName(guid, config); + if (configGuid == null) continue; + proj.SetBuildPropertyForConfig(configGuid, "SUPPORTED_PLATFORMS", "iphoneos iphonesimulator"); + proj.SetBuildPropertyForConfig(configGuid, "SUPPORTS_MACCATALYST", "NO"); + } + } + + proj.WriteToFile(projPath); + } + + static void AddURLScheme(string buildPath) + { + string plistPath = Path.Combine(buildPath, "Info.plist"); + var plist = new PlistDocument(); + plist.ReadFromFile(plistPath); + + PlistElementArray urlTypes = plist.root["CFBundleURLTypes"] as PlistElementArray + ?? plist.root.CreateArray("CFBundleURLTypes"); + + // Check if afqa-unity scheme is already registered + foreach (var item in urlTypes.values) + { + var dict = item as PlistElementDict; + if (dict == null) continue; + var schemes = dict["CFBundleURLSchemes"] as PlistElementArray; + if (schemes == null) continue; + foreach (var s in schemes.values) + if (s.AsString() == "afqa-unity") return; + } + + var entry = urlTypes.AddDict(); + entry.SetString("CFBundleURLName", "com.appsflyer.engagement"); + var schemesArray = entry.CreateArray("CFBundleURLSchemes"); + schemesArray.AddString("afqa-unity"); + + plist.WriteToFile(plistPath); + } +} +#endif diff --git a/test-app/Assets/Editor/iOSBuildPostProcess.cs.meta b/test-app/Assets/Editor/iOSBuildPostProcess.cs.meta new file mode 100644 index 00000000..dc337405 --- /dev/null +++ b/test-app/Assets/Editor/iOSBuildPostProcess.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: e8b31fe59143a46a1b68c06b2df8f723 \ No newline at end of file diff --git a/test-app/Assets/Plugins.meta b/test-app/Assets/Plugins.meta new file mode 100644 index 00000000..175b6003 --- /dev/null +++ b/test-app/Assets/Plugins.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9f5780b76534d4f91b3a4e94e36c2d92 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/test-app/Assets/Plugins/Android.meta b/test-app/Assets/Plugins/Android.meta new file mode 100644 index 00000000..0bccc12a --- /dev/null +++ b/test-app/Assets/Plugins/Android.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3aedbb88ad70c49519269720276124c7 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/test-app/Assets/Plugins/Android/AndroidManifest.xml b/test-app/Assets/Plugins/Android/AndroidManifest.xml new file mode 100644 index 00000000..5c75f84e --- /dev/null +++ b/test-app/Assets/Plugins/Android/AndroidManifest.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test-app/Assets/Plugins/Android/AndroidManifest.xml.meta b/test-app/Assets/Plugins/Android/AndroidManifest.xml.meta new file mode 100644 index 00000000..eb514c76 --- /dev/null +++ b/test-app/Assets/Plugins/Android/AndroidManifest.xml.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7752c53d7880149c3b87db9abfc64130 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/test-app/Assets/Plugins/Android/AppsFlyerUnityActivity.java b/test-app/Assets/Plugins/Android/AppsFlyerUnityActivity.java new file mode 100644 index 00000000..0dcf71f7 --- /dev/null +++ b/test-app/Assets/Plugins/Android/AppsFlyerUnityActivity.java @@ -0,0 +1,23 @@ +package com.appsflyer.engagement; + +import android.content.Intent; +import com.appsflyer.AppsFlyerLib; +import com.unity3d.player.UnityPlayerActivity; + +/** + * Extends UnityPlayerActivity to forward onNewIntent to the AppsFlyer SDK + * so that deep links opened while the app is already running (singleTask + * bring-to-front) fire UDL onDeepLinking(FOUND). + * + * UnityPlayerActivity.onNewIntent only calls setIntent + newIntent on the + * player; it never notifies AppsFlyerLib. Calling performOnDeepLinking here + * is the AF SDK v6 equivalent of the older sendDeepLinkData. + */ +public class AppsFlyerUnityActivity extends UnityPlayerActivity { + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + AppsFlyerLib.getInstance().performOnDeepLinking(intent, this); + } +} diff --git a/test-app/Assets/Plugins/Android/AppsFlyerUnityActivity.java.meta b/test-app/Assets/Plugins/Android/AppsFlyerUnityActivity.java.meta new file mode 100644 index 00000000..51585485 --- /dev/null +++ b/test-app/Assets/Plugins/Android/AppsFlyerUnityActivity.java.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ea67a6f1cbfca4217bd6a7dfb612827c \ No newline at end of file diff --git a/test-app/Assets/Plugins/Android/mainTemplate.gradle b/test-app/Assets/Plugins/Android/mainTemplate.gradle new file mode 100644 index 00000000..75078dec --- /dev/null +++ b/test-app/Assets/Plugins/Android/mainTemplate.gradle @@ -0,0 +1,50 @@ +apply plugin: 'com.android.library' +apply from: '../shared/keepUnitySymbols.gradle' +apply from: '../shared/common.gradle' +**APPLY_PLUGINS** + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'com.appsflyer:af-android-sdk:6.17.6' + implementation 'com.android.installreferrer:installreferrer:2.1' + implementation 'com.appsflyer:purchase-connector:2.2.0' +**DEPS**} + +android { + namespace "com.unity3d.player" + ndkPath "**NDKPATH**" + ndkVersion "**NDKVERSION**" + + compileSdk **APIVERSION** + buildToolsVersion = "**BUILDTOOLS**" + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + defaultConfig { + minSdk **MINSDK** + targetSdk **TARGETSDK** + ndk { + abiFilters **ABIFILTERS** + debugSymbolLevel **DEBUGSYMBOLLEVEL** + } + versionCode **VERSIONCODE** + versionName '**VERSIONNAME**' + consumerProguardFiles 'proguard-unity.txt'**USER_PROGUARD** +**DEFAULT_CONFIG_SETUP** + } + + lint { + abortOnError false + } + + androidResources { + noCompress = **BUILTIN_NOCOMPRESS** + unityStreamingAssets.tokenize(', ') + ignoreAssetsPattern = "!.svn:!.git:!.ds_store:!*.scc:!CVS:!thumbs.db:!picasa.ini:!*~" + }**PACKAGING** +} +**IL_CPP_BUILD_SETUP** +**SOURCE_BUILD_SETUP** +**EXTERNAL_SOURCES** diff --git a/test-app/Assets/Plugins/Android/mainTemplate.gradle.meta b/test-app/Assets/Plugins/Android/mainTemplate.gradle.meta new file mode 100644 index 00000000..a04a9520 --- /dev/null +++ b/test-app/Assets/Plugins/Android/mainTemplate.gradle.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 270a199eaa9ce4f75a5f6fafd3cca47d +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/test-app/Assets/Plugins/Android/unitywrapper.aar b/test-app/Assets/Plugins/Android/unitywrapper.aar new file mode 100644 index 0000000000000000000000000000000000000000..5953b3aaa8093f73a94ce0b6095a200a4e7e27c6 GIT binary patch literal 24179 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 z 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 literal 0 HcmV?d00001 diff --git a/test-app/Assets/Plugins/Android/unitywrapper.aar.meta b/test-app/Assets/Plugins/Android/unitywrapper.aar.meta new file mode 100644 index 00000000..3d3e68c6 --- /dev/null +++ b/test-app/Assets/Plugins/Android/unitywrapper.aar.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 9df3647ec03e04cdb8b17621c4af96dc \ No newline at end of file diff --git a/test-app/Assets/Scenes.meta b/test-app/Assets/Scenes.meta new file mode 100644 index 00000000..650ce8d9 --- /dev/null +++ b/test-app/Assets/Scenes.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 522323147f8c745eabf4ccf057187b0a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/test-app/Assets/Scenes/QATestScene.unity b/test-app/Assets/Scenes/QATestScene.unity new file mode 100644 index 00000000..75323368 --- /dev/null +++ b/test-app/Assets/Scenes/QATestScene.unity @@ -0,0 +1,121 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBlobData: + m_StaticRenderers: [] +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 9 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 0 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 10304, guid: 0000000000000000f000000000000000, type: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_IndirectSpecularColor: {r: 0, g: 0, b: 0, a: 1} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 12 + m_GIWorkflowMode: 1 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 1 + m_EnableRealtimeLightmaps: 1 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_FinalGather: 0 + m_FinalGatherFiltering: 1 + m_FinalGatherRayCount: 256 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 5 + m_PVRFilteringGaussRadiusAO: 2 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} diff --git a/test-app/Assets/Scenes/QATestScene.unity.meta b/test-app/Assets/Scenes/QATestScene.unity.meta new file mode 100644 index 00000000..b087ff16 --- /dev/null +++ b/test-app/Assets/Scenes/QATestScene.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: d2c230ae2e6624d2786d3dc19c67b4c7 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/test-app/Assets/Scripts.meta b/test-app/Assets/Scripts.meta new file mode 100644 index 00000000..6ef33911 --- /dev/null +++ b/test-app/Assets/Scripts.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 93afbd6ee1d804e2481a18a88e9947af +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/test-app/Assets/Scripts/AFQALogger.cs b/test-app/Assets/Scripts/AFQALogger.cs new file mode 100644 index 00000000..58d87e5d --- /dev/null +++ b/test-app/Assets/Scripts/AFQALogger.cs @@ -0,0 +1,28 @@ +using System.IO; +using UnityEngine; + +public static class AFQALogger +{ + private static string _logFilePath; + + static AFQALogger() + { +#if UNITY_IOS && !UNITY_EDITOR + _logFilePath = Path.Combine(Application.persistentDataPath, "af_qa_logs.txt"); + // Truncate on each fresh app launch so phase captures are self-contained. + File.WriteAllText(_logFilePath, string.Empty); +#endif + } + + public static void Log(string message) + { + Debug.Log(message); + +#if UNITY_IOS && !UNITY_EDITOR + if (!string.IsNullOrEmpty(_logFilePath)) + { + File.AppendAllText(_logFilePath, message + "\n"); + } +#endif + } +} diff --git a/test-app/Assets/Scripts/AFQALogger.cs.meta b/test-app/Assets/Scripts/AFQALogger.cs.meta new file mode 100644 index 00000000..0248eeb1 --- /dev/null +++ b/test-app/Assets/Scripts/AFQALogger.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3cfd8cd8858824b7682a452b50a20606 \ No newline at end of file diff --git a/test-app/Assets/Scripts/QATestScript.cs b/test-app/Assets/Scripts/QATestScript.cs new file mode 100644 index 00000000..ccecea47 --- /dev/null +++ b/test-app/Assets/Scripts/QATestScript.cs @@ -0,0 +1,263 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using UnityEngine; +using AppsFlyerSDK; + +public class QATestScript : MonoBehaviour, IAppsFlyerConversionData +{ + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)] + static void AutoInit() + { + var go = new GameObject("QATestObject"); + DontDestroyOnLoad(go); + go.AddComponent(); + go.AddComponent(); + } + + private string _devKey; + private string _iosAppId; + private string _androidAppId; + + void Start() + { + if (!LoadConfig()) + return; + + AppsFlyer.OnRequestResponse += OnRequestResponse; + AppsFlyer.OnInAppResponse += OnInAppResponse; + + RunPreStartApis(); + + string appId = Application.platform == RuntimePlatform.IPhonePlayer ? _iosAppId : _androidAppId; + AppsFlyer.setIsDebug(true); + AppsFlyer.initSDK(_devKey, appId, GetComponent() ?? this as MonoBehaviour); + AppsFlyer.OnDeepLinkReceived += OnDeepLinkReceived; + AppsFlyer.startSDK(); + AFQALogger.Log("[AF_QA][startSDK] result: SUCCESS"); + + StartCoroutine(RunPostStartApis()); + } + + void OnDestroy() + { + AppsFlyer.OnDeepLinkReceived -= OnDeepLinkReceived; + AppsFlyer.OnRequestResponse -= OnRequestResponse; + AppsFlyer.OnInAppResponse -= OnInAppResponse; + } + + // ── Config loading ──────────────────────────────────────────────────────── + + bool LoadConfig() + { + // Primary path: StreamingAssets (embedded in build, works on all platforms). + // In CI the workflow writes secrets to test-app/Assets/StreamingAssets/.env + // before calling game-ci/unity-builder so they get baked into the binary. + string envPath = Path.Combine(Application.streamingAssetsPath, ".env"); + + string content = null; + +#if UNITY_ANDROID && !UNITY_EDITOR + // On Android, StreamingAssets lives inside the APK; use the persistent + // copy written by the workflow via `adb shell run-as` (fallback path). + string persistentEnv = Path.Combine(Application.persistentDataPath, ".env"); + if (File.Exists(persistentEnv)) + { + content = File.ReadAllText(persistentEnv); + } + else + { + AFQALogger.Log("[AF_QA][CONFIG] .env not found at persistentDataPath; " + + "push it with: adb shell run-as com.appsflyer.engagement " + + "sh -c 'cat > /data/data/com.appsflyer.engagement/files/.env'"); + } +#else + if (File.Exists(envPath)) + content = File.ReadAllText(envPath); + else + { + // Editor fallback: project root .env + string editorEnv = Path.Combine(Application.dataPath, "../.env"); + if (File.Exists(editorEnv)) + content = File.ReadAllText(editorEnv); + } +#endif + + if (string.IsNullOrEmpty(content)) + { + AFQALogger.Log("[AF_QA][CONFIG] DEV_KEY missing"); + return false; + } + + foreach (var line in content.Split('\n')) + { + string trimmed = line.Trim(); + if (trimmed.StartsWith("DEV_KEY=")) _devKey = trimmed.Substring("DEV_KEY=".Length); + else if (trimmed.StartsWith("IOS_APP_ID=")) _iosAppId = trimmed.Substring("IOS_APP_ID=".Length); + else if (trimmed.StartsWith("ANDROID_APP_ID=")) _androidAppId = trimmed.Substring("ANDROID_APP_ID=".Length); + } + + if (string.IsNullOrEmpty(_devKey)) + { + AFQALogger.Log("[AF_QA][CONFIG] DEV_KEY missing"); + return false; + } + + AFQALogger.Log("[AF_QA][CONFIG] loaded"); + return true; + } + + // ── Pre-start APIs ──────────────────────────────────────────────────────── + + void RunPreStartApis() + { + AppsFlyer.setCustomerUserId("e2e_user_42"); + AFQALogger.Log("[AF_QA][setCustomerUserId] result: e2e_user_42"); + + AppsFlyer.setCurrencyCode("EUR"); + AFQALogger.Log("[AF_QA][setCurrencyCode] result: EUR"); + + var additionalData = new Dictionary + { + { "tenant", "qa_eu" }, + { "experiment", "rc_pipeline_v1" } + }; + AppsFlyer.setAdditionalData(additionalData); + AFQALogger.Log("[AF_QA][setAdditionalData] tenant=qa_eu experiment=rc_pipeline_v1"); + + AFQALogger.Log("[AF_QA][AUTO_APIS] --- Pre-start auto APIs complete ---"); + } + + // ── Post-start APIs ─────────────────────────────────────────────────────── + + IEnumerator RunPostStartApis() + { + yield return new WaitForSeconds(1f); + + string sdkVersion = AppsFlyer.getSdkVersion(); + AFQALogger.Log("[AF_QA][getSDKVersion] result: " + sdkVersion); + + string uid = AppsFlyer.getAppsFlyerId(); + AFQALogger.Log("[AF_QA][getAppsFlyerUID] result: " + uid); + + // E2E-001: three standard events + AppsFlyer.sendEvent("af_demo_launch", null); + AFQALogger.Log("[AF_QA][logEvent(af_demo_launch)] result: SUCCESS"); + + AppsFlyer.sendEvent("af_purchase", new Dictionary + { + { "af_revenue", "9.99" }, + { "af_currency", "USD" }, + { "af_content_type", "subscription" } + }); + AFQALogger.Log("[AF_QA][logEvent: af_purchase sent] result: SUCCESS"); + + AppsFlyer.sendEvent("af_content_view", new Dictionary + { + { "af_content_id", "qa_content_1" } + }); + AFQALogger.Log("[AF_QA][logEvent: af_content_view sent] result: SUCCESS"); + + // E2E-004: custom event with revenue, currency, and nested metadata + var customParams = new Dictionary + { + { "af_revenue", "19.99" }, + { "af_currency", "EUR" }, + { "metadata", "{\"source\":\"qa\",\"variant\":\"A\"}" } + }; + AFQALogger.Log("[AF_QA][logEvent] name=af_qa_custom_purchase params=" + DictToJson(customParams)); + AppsFlyer.sendEvent("af_qa_custom_purchase", customParams); + + yield return new WaitForSeconds(1f); + + // E2E-005: identity check event — customer_user_id propagation + var identityParams = new Dictionary + { + { "customer_user_id", "e2e_user_42" }, + { "tenant", "qa_eu" }, + { "experiment", "rc_pipeline_v1" } + }; + AFQALogger.Log("[AF_QA][logEvent] name=af_qa_identity_check params={customer_user_id: e2e_user_42, tenant: qa_eu, experiment: rc_pipeline_v1}"); + AppsFlyer.sendEvent("af_qa_identity_check", identityParams); + + yield return new WaitForSeconds(1f); + + // E2E-006: stop / resume toggle + AppsFlyer.stopSDK(true); + AFQALogger.Log("[AF_QA][stop] result: true"); + + AppsFlyer.sendEvent("af_qa_suppressed", null); + + AppsFlyer.stopSDK(false); + AFQALogger.Log("[AF_QA][stop] result: false"); + + AppsFlyer.sendEvent("af_qa_resumed", null); + + AFQALogger.Log("[AF_QA][AUTO_APIS] --- Post-start auto APIs complete ---"); + AFQALogger.Log("[AF_QA][AUTO_APIS] --- Auto run complete ---"); + } + + // ── IAppsFlyerConversionData ────────────────────────────────────────────── + + public void onConversionDataSuccess(string conversionData) + { + AFQALogger.Log("[AF_QA][CALLBACK][onInstallConversionData] " + conversionData); + } + + public void onConversionDataFail(string error) + { + AFQALogger.Log("[AF_QA][CALLBACK][onInstallConversionData] error: " + error); + } + + public void onAppOpenAttribution(string attributionData) + { + AFQALogger.Log("[AF_QA][CALLBACK][onAppOpenAttribution] " + attributionData); + } + + public void onAppOpenAttributionFailure(string error) + { + AFQALogger.Log("[AF_QA][CALLBACK][onAppOpenAttribution] error: " + error); + } + + // ── Deep link callback ──────────────────────────────────────────────────── + + void OnDeepLinkReceived(object sender, EventArgs args) + { + var dlArgs = args as DeepLinkEventsArgs; + if (dlArgs == null) + { + AFQALogger.Log("[AF_QA][CALLBACK][onDeepLinking] received: null args"); + return; + } + string status = dlArgs.status.ToString(); + string deepLinkValue = dlArgs.getDeepLinkValue() ?? ""; + AFQALogger.Log("[AF_QA][CALLBACK][onDeepLinking] received: status=" + status + ", deepLinkValue=" + deepLinkValue); + } + + // ── Request / in-app response callbacks ────────────────────────────────── + + void OnRequestResponse(object sender, EventArgs e) + { + var a = e as AppsFlyerRequestEventArgs; + if (a != null) + AFQALogger.Log("[AF_QA][RequestResponse] responseCode=" + a.statusCode + " desc=" + a.errorDescription); + } + + void OnInAppResponse(object sender, EventArgs e) + { + var a = e as AppsFlyerRequestEventArgs; + if (a != null) + AFQALogger.Log("[AF_QA][InAppResponse] responseCode=" + a.statusCode + " desc=" + a.errorDescription); + } + + // ── Utilities ───────────────────────────────────────────────────────────── + + static string DictToJson(Dictionary d) + { + var parts = new List(); + foreach (var kv in d) + parts.Add("\"" + kv.Key + "\":\"" + kv.Value + "\""); + return "{" + string.Join(",", parts) + "}"; + } +} diff --git a/test-app/Assets/Scripts/QATestScript.cs.meta b/test-app/Assets/Scripts/QATestScript.cs.meta new file mode 100644 index 00000000..7fcc9299 --- /dev/null +++ b/test-app/Assets/Scripts/QATestScript.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ad6143227bef14e93b06c440aa98e981 \ No newline at end of file diff --git a/test-app/Assets/StreamingAssets.meta b/test-app/Assets/StreamingAssets.meta new file mode 100644 index 00000000..34ced9ae --- /dev/null +++ b/test-app/Assets/StreamingAssets.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9a48cff585c074c42bd6c0efa8f33b23 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/test-app/Assets/StreamingAssets/.env.example b/test-app/Assets/StreamingAssets/.env.example new file mode 100644 index 00000000..56b50e0f --- /dev/null +++ b/test-app/Assets/StreamingAssets/.env.example @@ -0,0 +1,3 @@ +DEV_KEY=your_dev_key_here +IOS_APP_ID=your_ios_app_id_here +ANDROID_APP_ID=com.appsflyer.engagement diff --git a/test-app/Assets/iOS.meta b/test-app/Assets/iOS.meta new file mode 100644 index 00000000..678753e7 --- /dev/null +++ b/test-app/Assets/iOS.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 47996ee5d248a4e14974876412d9be3e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/test-app/Assets/iOS/AppsFlyerOpenURL.mm b/test-app/Assets/iOS/AppsFlyerOpenURL.mm new file mode 100644 index 00000000..e93f0fde --- /dev/null +++ b/test-app/Assets/iOS/AppsFlyerOpenURL.mm @@ -0,0 +1 @@ +// Stub — _handleOpenUrlNew removed; deeplinks handled natively by UnityAppControllerDeepLink.mm diff --git a/test-app/Assets/iOS/AppsFlyerOpenURL.mm.meta b/test-app/Assets/iOS/AppsFlyerOpenURL.mm.meta new file mode 100644 index 00000000..97bd1957 --- /dev/null +++ b/test-app/Assets/iOS/AppsFlyerOpenURL.mm.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 290fd7b9554f34c6da8cb59b36c5ca31 \ No newline at end of file diff --git a/test-app/Assets/iOS/UnityAppControllerDeepLink.mm b/test-app/Assets/iOS/UnityAppControllerDeepLink.mm new file mode 100644 index 00000000..da568d57 --- /dev/null +++ b/test-app/Assets/iOS/UnityAppControllerDeepLink.mm @@ -0,0 +1,68 @@ +// QADeepLinkBootstrap.mm +// +// Swizzles UnityAppController.application:didFinishLaunchingWithOptions: to +// read the -deepLinkURL launch argument injected by `xcrun simctl launch`. +// +// Timing: AF_BRIDGE_SET fires synchronously inside _startSDK BEFORE +// startWithCompletionHandler:, so calling handleOpenUrl: immediately would +// flush before the SDK is started and UDL would never resolve. +// +// Fix: capture the URL at launch, then dispatch a 5-second delayed call to +// application:openURL:options: — the same standard iOS URL-open pipeline that +// Flutter's AppDelegate uses. By 5s Unity has initialised, Start() has called +// initSDK() and startSDK(), so UDL resolves correctly. +// +// Using a category + swizzle rather than IMPL_APP_CONTROLLER_SUBCLASS avoids +// the conflict with the plugin's own AppsFlyerAppController subclass. + +#import +#import "UnityAppController.h" + +@implementation UnityAppController (QADeepLinkBootstrap) + ++ (void)load { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + SEL original = @selector(application:didFinishLaunchingWithOptions:); + SEL swizzled = @selector(qa_application:didFinishLaunchingWithOptions:); + Method originalMethod = class_getInstanceMethod([self class], original); + Method swizzledMethod = class_getInstanceMethod([self class], swizzled); + if (originalMethod && swizzledMethod) { + method_exchangeImplementations(originalMethod, swizzledMethod); + } + }); +} + +- (BOOL)qa_application:(UIApplication *)application +didFinishLaunchingWithOptions:(NSDictionary *)launchOptions +{ + BOOL result = [self qa_application:application didFinishLaunchingWithOptions:launchOptions]; + + // Resolve URL from -deepLinkURL launch arg or NSUserDefaults fallback. + NSURL *deepLinkURL = nil; + NSArray *args = [NSProcessInfo processInfo].arguments; + NSUInteger idx = [args indexOfObject:@"-deepLinkURL"]; + if (idx != NSNotFound && idx + 1 < args.count) { + deepLinkURL = [NSURL URLWithString:args[idx + 1]]; + } + if (!deepLinkURL) { + NSString *stored = [[NSUserDefaults standardUserDefaults] stringForKey:@"deepLinkURL"]; + if (stored) deepLinkURL = [NSURL URLWithString:stored]; + } + + if (deepLinkURL) { + __weak typeof(self) weakSelf = self; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + __strong typeof(weakSelf) strongSelf = weakSelf; + if (!strongSelf) return; + // Route through the standard iOS URL-open delegate pipeline, + // matching Flutter's AppDelegate approach. + [strongSelf application:application openURL:deepLinkURL options:@{}]; + }); + } + + return result; +} + +@end diff --git a/test-app/Assets/iOS/UnityAppControllerDeepLink.mm.meta b/test-app/Assets/iOS/UnityAppControllerDeepLink.mm.meta new file mode 100644 index 00000000..7f5aab52 --- /dev/null +++ b/test-app/Assets/iOS/UnityAppControllerDeepLink.mm.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 4ff602011848a46f288de79743fd2ebf \ No newline at end of file diff --git a/test-app/Packages/manifest.json b/test-app/Packages/manifest.json new file mode 100644 index 00000000..0869384b --- /dev/null +++ b/test-app/Packages/manifest.json @@ -0,0 +1,18 @@ +{ + "dependencies": { + "appsflyer-unity-plugin": "file:../../Assets/AppsFlyer", + "com.unity.modules.androidjni": "1.0.0", + "com.unity.modules.animation": "1.0.0", + "com.unity.modules.assetbundle": "1.0.0", + "com.unity.modules.audio": "1.0.0", + "com.unity.modules.imgui": "1.0.0", + "com.unity.modules.jsonserialize": "1.0.0", + "com.unity.modules.physics": "1.0.0", + "com.unity.modules.ui": "1.0.0", + "com.unity.modules.uielements": "1.0.0", + "com.unity.modules.unitywebrequest": "1.0.0", + "com.unity.modules.unitywebrequestwww": "1.0.0", + "com.unity.modules.vr": "1.0.0", + "com.unity.modules.xr": "1.0.0" + } +} diff --git a/test-app/ProjectSettings/AudioManager.asset b/test-app/ProjectSettings/AudioManager.asset new file mode 100644 index 00000000..50b4625b --- /dev/null +++ b/test-app/ProjectSettings/AudioManager.asset @@ -0,0 +1,23 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!11 &1 +AudioManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Volume: 1 + Rolloff Scale: 1 + Doppler Factor: 1 + Default Speaker Mode: 2 + m_SampleRate: 0 + m_DSPBufferSize: 1024 + m_VirtualVoiceCount: 512 + m_RealVoiceCount: 32 + m_EnableOutputSuspension: 1 + m_SpatializerPlugin: + m_AmbisonicDecoderPlugin: + m_DisableAudio: 0 + m_VirtualizeEffects: 1 + m_RequestedDSPBufferSize: 0 + m_AudioFoundation: 0 + m_OutputChannelLayout: 2 + m_OutputSamplingRate: 48000 diff --git a/test-app/ProjectSettings/ClusterInputManager.asset b/test-app/ProjectSettings/ClusterInputManager.asset new file mode 100644 index 00000000..e7886b26 --- /dev/null +++ b/test-app/ProjectSettings/ClusterInputManager.asset @@ -0,0 +1,6 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!236 &1 +ClusterInputManager: + m_ObjectHideFlags: 0 + m_Inputs: [] diff --git a/test-app/ProjectSettings/DynamicsManager.asset b/test-app/ProjectSettings/DynamicsManager.asset new file mode 100644 index 00000000..0dc1faeb --- /dev/null +++ b/test-app/ProjectSettings/DynamicsManager.asset @@ -0,0 +1,44 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!55 &1 +PhysicsManager: + m_ObjectHideFlags: 0 + serializedVersion: 22 + m_Gravity: {x: 0, y: -9.81, z: 0} + m_DefaultMaterial: {fileID: 0} + m_BounceThreshold: 2 + m_DefaultMaxDepenetrationVelocity: 10 + m_SleepThreshold: 0.005 + m_DefaultContactOffset: 0.01 + m_DefaultSolverIterations: 6 + m_DefaultSolverVelocityIterations: 1 + m_QueriesHitBackfaces: 0 + m_QueriesHitTriggers: 1 + m_EnableAdaptiveForce: 0 + m_ClothInterCollisionDistance: 0.1 + m_ClothInterCollisionStiffness: 0.2 + m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + m_SimulationMode: 0 + m_AutoSyncTransforms: 0 + m_ReuseCollisionCallbacks: 1 + m_InvokeCollisionCallbacks: 1 + m_ClothInterCollisionSettingsToggle: 0 + m_ClothGravity: {x: 0, y: -9.81, z: 0} + m_ContactPairsMode: 0 + m_BroadphaseType: 0 + m_WorldBounds: + m_Center: {x: 0, y: 0, z: 0} + m_Extent: {x: 256, y: 256, z: 256} + m_WorldSubdivisions: 8 + m_FrictionType: 0 + m_EnableEnhancedDeterminism: 0 + m_ImprovedPatchFriction: 0 + m_GenerateOnTriggerStayEvents: 1 + m_SolverType: 0 + m_DefaultMaxAngularSpeed: 50 + m_ScratchBufferChunkCount: 4 + m_CurrentBackendId: 4072204805 + m_FastMotionThreshold: 3.4028235e+38 + m_SceneBuffersReleaseInterval: 0 + m_ReleaseSceneBuffers: 0 + m_LogVerbosity: 3 diff --git a/test-app/ProjectSettings/EditorBuildSettings.asset b/test-app/ProjectSettings/EditorBuildSettings.asset new file mode 100644 index 00000000..7c9f4553 --- /dev/null +++ b/test-app/ProjectSettings/EditorBuildSettings.asset @@ -0,0 +1,11 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1045 &1 +EditorBuildSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Scenes: + - enabled: 1 + path: Assets/Scenes/QATestScene.unity + guid: 00000000000000000000000000000001 + m_configObjects: {} diff --git a/test-app/ProjectSettings/EditorSettings.asset b/test-app/ProjectSettings/EditorSettings.asset new file mode 100644 index 00000000..cdbdc129 --- /dev/null +++ b/test-app/ProjectSettings/EditorSettings.asset @@ -0,0 +1,50 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!159 &1 +EditorSettings: + m_ObjectHideFlags: 0 + serializedVersion: 15 + m_SerializationMode: 2 + m_LineEndingsForNewScripts: 1 + m_DefaultBehaviorMode: 0 + m_PrefabRegularEnvironment: {fileID: 0} + m_PrefabUIEnvironment: {fileID: 0} + m_SpritePackerMode: 0 + m_SpritePackerCacheSize: 10 + m_SpritePackerPaddingPower: 1 + m_Bc7TextureCompressor: 0 + m_EtcTextureCompressorBehavior: 1 + m_EtcTextureFastCompressor: 1 + m_EtcTextureNormalCompressor: 2 + m_EtcTextureBestCompressor: 4 + m_ProjectGenerationIncludedExtensions: txt;xml;fnt;cd;asmdef;asmref;rsp;java;cpp;c;mm;m;h + m_ProjectGenerationRootNamespace: + m_EnableTextureStreamingInEditMode: 1 + m_EnableTextureStreamingInPlayMode: 1 + m_EnableEditorAsyncCPUTextureLoading: 0 + m_AsyncShaderCompilation: 1 + m_PrefabModeAllowAutoSave: 1 + m_EnterPlayModeOptionsEnabled: 1 + m_EnterPlayModeOptions: 0 + m_GameObjectNamingDigits: 1 + m_GameObjectNamingScheme: 0 + m_AssetNamingUsesSpace: 1 + m_InspectorUseIMGUIDefaultInspector: 0 + m_UseLegacyProbeSampleCount: 0 + m_SerializeInlineMappingsOnOneLine: 1 + m_DisableCookiesInLightmapper: 0 + m_ShadowmaskStitching: 1 + m_AssetPipelineMode: 1 + m_RefreshImportMode: 0 + m_CacheServerMode: 0 + m_CacheServerEndpoint: + m_CacheServerNamespacePrefix: default + m_CacheServerEnableDownload: 1 + m_CacheServerEnableUpload: 1 + m_CacheServerEnableAuth: 0 + m_CacheServerEnableTls: 0 + m_CacheServerValidationMode: 2 + m_CacheServerDownloadBatchSize: 128 + m_EnableEnlightenBakedGI: 0 + m_ReferencedClipsExactNaming: 1 + m_ForceAssetUnloadAndGCOnSceneLoad: 1 diff --git a/test-app/ProjectSettings/GraphicsSettings.asset b/test-app/ProjectSettings/GraphicsSettings.asset new file mode 100644 index 00000000..4b152c0f --- /dev/null +++ b/test-app/ProjectSettings/GraphicsSettings.asset @@ -0,0 +1,68 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!30 &1 +GraphicsSettings: + m_ObjectHideFlags: 0 + serializedVersion: 16 + m_Deferred: + m_Mode: 1 + m_Shader: {fileID: 69, guid: 0000000000000000f000000000000000, type: 0} + m_DeferredReflections: + m_Mode: 1 + m_Shader: {fileID: 74, guid: 0000000000000000f000000000000000, type: 0} + m_ScreenSpaceShadows: + m_Mode: 1 + m_Shader: {fileID: 64, guid: 0000000000000000f000000000000000, type: 0} + m_DepthNormals: + m_Mode: 1 + m_Shader: {fileID: 62, guid: 0000000000000000f000000000000000, type: 0} + m_MotionVectors: + m_Mode: 1 + m_Shader: {fileID: 75, guid: 0000000000000000f000000000000000, type: 0} + m_LightHalo: + m_Mode: 1 + m_Shader: {fileID: 105, guid: 0000000000000000f000000000000000, type: 0} + m_LensFlare: + m_Mode: 1 + m_Shader: {fileID: 102, guid: 0000000000000000f000000000000000, type: 0} + m_VideoShadersIncludeMode: 2 + m_AlwaysIncludedShaders: + - {fileID: 7, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 15104, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 15105, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 15106, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 10753, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 10770, guid: 0000000000000000f000000000000000, type: 0} + - {fileID: 10783, guid: 0000000000000000f000000000000000, type: 0} + m_PreloadedShaders: [] + m_PreloadShadersBatchTimeLimit: -1 + m_SpritesDefaultMaterial: {fileID: 10754, guid: 0000000000000000f000000000000000, type: 0} + m_CustomRenderPipeline: {fileID: 0} + m_TransparencySortMode: 0 + m_TransparencySortAxis: {x: 0, y: 0, z: 1} + m_DefaultRenderingPath: 1 + m_DefaultMobileRenderingPath: 1 + m_TierSettings: [] + m_LightmapStripping: 0 + m_FogStripping: 0 + m_InstancingStripping: 0 + m_BrgStripping: 0 + m_LightmapKeepPlain: 1 + m_LightmapKeepDirCombined: 1 + m_LightmapKeepDynamicPlain: 1 + m_LightmapKeepDynamicDirCombined: 1 + m_LightmapKeepShadowMask: 1 + m_LightmapKeepSubtractive: 1 + m_FogKeepLinear: 1 + m_FogKeepExp: 1 + m_FogKeepExp2: 1 + m_AlbedoSwatchInfos: [] + m_RenderPipelineGlobalSettingsMap: {} + m_ShaderBuildSettings: + keywordDeclarationOverrides: [] + m_LightsUseLinearIntensity: 0 + m_LightsUseColorTemperature: 0 + m_LogWhenShaderIsCompiled: 0 + m_LightProbeOutsideHullStrategy: 1 + m_CameraRelativeLightCulling: 0 + m_CameraRelativeShadowCulling: 0 diff --git a/test-app/ProjectSettings/InputManager.asset b/test-app/ProjectSettings/InputManager.asset new file mode 100644 index 00000000..8068b205 --- /dev/null +++ b/test-app/ProjectSettings/InputManager.asset @@ -0,0 +1,296 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!13 &1 +InputManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Axes: + - serializedVersion: 3 + m_Name: Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: left + positiveButton: right + altNegativeButton: a + altPositiveButton: d + gravity: 3 + dead: 0.001 + sensitivity: 3 + snap: 1 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: down + positiveButton: up + altNegativeButton: s + altPositiveButton: w + gravity: 3 + dead: 0.001 + sensitivity: 3 + snap: 1 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire1 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left ctrl + altNegativeButton: + altPositiveButton: mouse 0 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire2 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left alt + altNegativeButton: + altPositiveButton: mouse 1 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire3 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: left shift + altNegativeButton: + altPositiveButton: mouse 2 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Jump + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: space + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse X + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse Y + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 1 + joyNum: 0 + - serializedVersion: 3 + m_Name: Mouse ScrollWheel + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0 + sensitivity: 0.1 + snap: 0 + invert: 0 + type: 1 + axis: 2 + joyNum: 0 + - serializedVersion: 3 + m_Name: Horizontal + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0.19 + sensitivity: 1 + snap: 0 + invert: 0 + type: 2 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Vertical + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: + altNegativeButton: + altPositiveButton: + gravity: 0 + dead: 0.19 + sensitivity: 1 + snap: 0 + invert: 1 + type: 2 + axis: 1 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire1 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 0 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire2 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 1 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Fire3 + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 2 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Jump + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: joystick button 3 + altNegativeButton: + altPositiveButton: + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Submit + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: return + altNegativeButton: + altPositiveButton: joystick button 0 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Submit + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: enter + altNegativeButton: + altPositiveButton: space + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + - serializedVersion: 3 + m_Name: Cancel + descriptiveName: + descriptiveNegativeName: + negativeButton: + positiveButton: escape + altNegativeButton: + altPositiveButton: joystick button 1 + gravity: 1000 + dead: 0.001 + sensitivity: 1000 + snap: 0 + invert: 0 + type: 0 + axis: 0 + joyNum: 0 + m_UsePhysicalKeys: 1 diff --git a/test-app/ProjectSettings/MemorySettings.asset b/test-app/ProjectSettings/MemorySettings.asset new file mode 100644 index 00000000..5b5facec --- /dev/null +++ b/test-app/ProjectSettings/MemorySettings.asset @@ -0,0 +1,35 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!387306366 &1 +MemorySettings: + m_ObjectHideFlags: 0 + m_EditorMemorySettings: + m_MainAllocatorBlockSize: -1 + m_ThreadAllocatorBlockSize: -1 + m_MainGfxBlockSize: -1 + m_ThreadGfxBlockSize: -1 + m_CacheBlockSize: -1 + m_TypetreeBlockSize: -1 + m_ProfilerBlockSize: -1 + m_ProfilerEditorBlockSize: -1 + m_BucketAllocatorGranularity: -1 + m_BucketAllocatorBucketsCount: -1 + m_BucketAllocatorBlockSize: -1 + m_BucketAllocatorBlockCount: -1 + m_ProfilerBucketAllocatorGranularity: -1 + m_ProfilerBucketAllocatorBucketsCount: -1 + m_ProfilerBucketAllocatorBlockSize: -1 + m_ProfilerBucketAllocatorBlockCount: -1 + m_TempAllocatorSizeMain: -1 + m_JobTempAllocatorBlockSize: -1 + m_BackgroundJobTempAllocatorBlockSize: -1 + m_JobTempAllocatorReducedBlockSize: -1 + m_TempAllocatorSizeGIBakingWorker: -1 + m_TempAllocatorSizeNavMeshWorker: -1 + m_TempAllocatorSizeAudioWorker: -1 + m_TempAllocatorSizeCloudWorker: -1 + m_TempAllocatorSizeGfx: -1 + m_TempAllocatorSizeJobWorker: -1 + m_TempAllocatorSizeBackgroundWorker: -1 + m_TempAllocatorSizePreloadManager: -1 + m_PlatformMemorySettings: {} diff --git a/test-app/ProjectSettings/MultiplayerManager.asset b/test-app/ProjectSettings/MultiplayerManager.asset new file mode 100644 index 00000000..c19bcd73 --- /dev/null +++ b/test-app/ProjectSettings/MultiplayerManager.asset @@ -0,0 +1,9 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!655991488 &1 +MultiplayerManager: + m_ObjectHideFlags: 0 + m_EnableMultiplayerRoles: 0 + m_EnablePlayModeLocalDeployment: 0 + m_EnablePlayModeRemoteDeployment: 0 + m_StrippingTypes: {} diff --git a/test-app/ProjectSettings/NavMeshAreas.asset b/test-app/ProjectSettings/NavMeshAreas.asset new file mode 100644 index 00000000..2e2e3696 --- /dev/null +++ b/test-app/ProjectSettings/NavMeshAreas.asset @@ -0,0 +1,93 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!126 &1 +NavMeshProjectSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + areas: + - name: Walkable + cost: 1 + - name: Not Walkable + cost: 1 + - name: Jump + cost: 2 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + - name: + cost: 1 + m_LastAgentTypeID: -887442657 + m_Settings: + - serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.75 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_SettingNames: + - Humanoid diff --git a/test-app/ProjectSettings/Physics2DSettings.asset b/test-app/ProjectSettings/Physics2DSettings.asset new file mode 100644 index 00000000..14f419f8 --- /dev/null +++ b/test-app/ProjectSettings/Physics2DSettings.asset @@ -0,0 +1,57 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!19 &1 +Physics2DSettings: + m_ObjectHideFlags: 0 + serializedVersion: 11 + m_Gravity: {x: 0, y: -9.81} + m_DefaultMaterial: {fileID: 0} + m_VelocityIterations: 8 + m_PositionIterations: 3 + m_BounceThreshold: 1 + m_MaxLinearCorrection: 0.2 + m_MaxAngularCorrection: 8 + m_MaxTranslationSpeed: 100 + m_MaxRotationSpeed: 360 + m_BaumgarteScale: 0.2 + m_BaumgarteTimeOfImpactScale: 0.75 + m_TimeToSleep: 0.5 + m_LinearSleepTolerance: 0.01 + m_AngularSleepTolerance: 2 + m_DefaultContactOffset: 0.01 + m_ContactThreshold: 0 + m_JobOptions: + serializedVersion: 2 + useMultithreading: 0 + useConsistencySorting: 0 + m_InterpolationPosesPerJob: 100 + m_NewContactsPerJob: 30 + m_CollideContactsPerJob: 100 + m_ClearFlagsPerJob: 200 + m_ClearBodyForcesPerJob: 200 + m_SyncDiscreteFixturesPerJob: 50 + m_SyncContinuousFixturesPerJob: 50 + m_FindNearestContactsPerJob: 100 + m_UpdateTriggerContactsPerJob: 100 + m_IslandSolverCostThreshold: 100 + m_IslandSolverBodyCostScale: 1 + m_IslandSolverContactCostScale: 10 + m_IslandSolverJointCostScale: 10 + m_IslandSolverBodiesPerJob: 50 + m_IslandSolverContactsPerJob: 50 + m_SimulationMode: 0 + m_SimulationLayers: + serializedVersion: 2 + m_Bits: 4294967295 + m_MaxSubStepCount: 4 + m_MinSubStepFPS: 30 + m_UseSubStepping: 0 + m_UseSubStepContacts: 0 + m_QueriesHitTriggers: 1 + m_QueriesStartInColliders: 1 + m_CallbacksOnDisable: 1 + m_ReuseCollisionCallbacks: 1 + m_AutoSyncTransforms: 0 + m_GizmoOptions: 10 + m_LayerCollisionMatrix: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff + m_PhysicsLowLevelSettings: {fileID: 0} diff --git a/test-app/ProjectSettings/PresetManager.asset b/test-app/ProjectSettings/PresetManager.asset new file mode 100644 index 00000000..67a94dae --- /dev/null +++ b/test-app/ProjectSettings/PresetManager.asset @@ -0,0 +1,7 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!1386491679 &1 +PresetManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_DefaultPresets: {} diff --git a/test-app/ProjectSettings/ProjectSettings.asset b/test-app/ProjectSettings/ProjectSettings.asset new file mode 100644 index 00000000..a8566608 --- /dev/null +++ b/test-app/ProjectSettings/ProjectSettings.asset @@ -0,0 +1,277 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!129 &1 +PlayerSettings: + m_ObjectHideFlags: 0 + serializedVersion: 23 + productGUID: a1b2c3d4e5f60000a1b2c3d4e5f60001 + AndroidProfiler: 0 + AndroidFilterTouchesWhenObscured: 0 + AndroidEnableSustainedPerformanceMode: 0 + defaultScreenOrientation: 4 + targetDevice: 2 + useOnDemandResources: 0 + accelerometerFrequency: 60 + companyName: AppsFlyer + productName: UnityQATest + defaultCursor: {fileID: 0} + cursorHotspot: {x: 0, y: 0} + m_SplashScreenBackgroundColor: {r: 0.13725491, g: 0.12156863, b: 0.1254902, a: 1} + m_ShowUnitySplashScreen: 0 + m_ShowUnitySplashLogo: 0 + m_SplashScreenOverlayOpacity: 1 + m_SplashScreenAnimation: 1 + m_SplashScreenLogoStyle: 1 + m_SplashScreenDrawMode: 0 + m_SplashScreenBackgroundAnimationZoom: 1 + m_SplashScreenLogoAnimationZoom: 1 + m_SplashScreenBackgroundLandscapeAspect: 1 + m_SplashScreenBackgroundPortraitAspect: 1 + m_SplashScreenBackgroundLandscapeUvs: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + m_SplashScreenBackgroundPortraitUvs: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + m_SplashScreenLogos: [] + m_VirtualRealitySplashScreen: {fileID: 0} + m_HolographicTrackingLossScreen: {fileID: 0} + defaultScreenWidth: 1024 + defaultScreenHeight: 768 + defaultScreenWidthWeb: 960 + defaultScreenHeightWeb: 600 + m_StereoRenderingPath: 0 + m_ActiveColorSpace: 0 + m_MTRendering: 1 + mipStripping: 0 + numberOfMipsStripped: 0 + m_StackTraceTypes: 010000000100000001000000010000000100000001000000 + iosShowActivityIndicatorOnLoading: -1 + androidShowActivityIndicatorOnLoading: -1 + iosUseCustomAppBackgroundBehavior: 0 + iosAllowHTTPDownload: 1 + allowedAutorotateToPortrait: 1 + allowedAutorotateToPortraitUpsideDown: 1 + allowedAutorotateToLandscapeRight: 1 + allowedAutorotateToLandscapeLeft: 1 + useOSAutorotation: 1 + use32BitDisplayBuffer: 1 + preserveFramebufferAlpha: 0 + disableDepthAndStencilBuffers: 0 + androidStartInFullscreen: 1 + androidRenderOutsideSafeArea: 1 + androidUseSwappy: 1 + androidBlitType: 0 + androidResizableWindow: 0 + androidDefaultWindowWidth: 1920 + androidDefaultWindowHeight: 1080 + androidMinimumWindowWidth: 400 + androidMinimumWindowHeight: 300 + androidFullscreenMode: 1 + defaultIsNativeResolution: 1 + macRetinaSupport: 1 + runInBackground: 0 + captureSingleScreen: 0 + muteOtherAudioSources: 0 + Prepare IOS For Recording: 0 + Force IOS Speakers When Recording: 0 + deferSystemGesturesMode: 0 + hideHomeButton: 0 + submitAnalytics: 1 + usePlayerLog: 1 + bakeCollisionMeshes: 0 + forceSingleInstance: 0 + useFlipModelSwapchain: 1 + resizableWindow: 0 + useMacAppStoreValidation: 0 + macAppStoreCategory: public.app-category.games + gpuSkinning: 0 + xboxPIXTextureCapture: 0 + xboxEnableAvatar: 0 + xboxEnableKinect: 0 + xboxEnableKinectAutoTracking: 0 + xboxSpeechDB: 0 + xboxEnableFitness: 0 + visibleInBackground: 1 + allowFullscreenSwitch: 1 + graphicsJobMode: 0 + fullscreenMode: 1 + xboxOneResolution: 0 + xboxOneSResolution: 0 + xboxOneXResolution: 3 + xboxOneMonoLoggingLevel: 0 + xboxOneLoggingLevel: 1 + xboxOnePresentImmediateThreshold: 0 + xboxOneDisableEsram: 0 + xboxOneEnableTypeOptimization: 0 + xboxOnePresentMode: 0 + n3dsDisableStereoscopic: 0 + n3dsEnableSharedListOPT: 0 + n3dsEnableVSync: 0 + n3dsMaxCachedShaderCount: 50 + n3dsUseLegacySdkLayout: 0 + n3dsGui: 0 + n3dsDebugAutoLoad: 0 + n3dsHid: 0 + n3dsShareableImage: 0 + n3dsTargetPlatform: 2 + n3dsRegion: 0 + n3dsMediaSize: 0 + n3dsLogoStyle: 3 + n3dsTitle: GameName + n3dsProductCode: + n3dsApplicationId: 00FF + voltaTerrain: 0 + createWallpaper: 0 + m_SupportedAspectRatios: + 4:3: 1 + 5:4: 1 + 16:10: 1 + 16:9: 1 + Others: 1 + bundleVersion: 1.0 + preloadedAssets: [] + metroInputSource: 0 + wsaTransparentSwapchain: 0 + m_HolographicPauseOnTrackingLoss: 1 + xboxOneDisableKinectGpuReservation: 1 + xboxOneEnable7thCore: 1 + vrSettings: + cardboard: + depthFormat: 0 + enableTransitionView: 0 + daydream: + depthFormat: 0 + useSustainedPerformanceMode: 0 + enableVideoLayer: 0 + useProtectedVideoMemory: 0 + minimumSupportedHeadTracking: 0 + maximumSupportedHeadTracking: 1 + hololens: + depthFormat: 1 + depthBufferSharingEnabled: 1 + lumin: + depthFormat: 0 + frameTiming: 0 + enableGLCache: 0 + glCacheMaxBlobSize: 524288 + glCacheMaxFileSize: 8388608 + isWsaHolographicRemotingEnabled: 0 + enableFrameTimingStats: 0 + enableOpenGLProfilerGPURecorders: 1 + useHDRDisplay: 0 + D3DHDRBitDepth: 0 + m_ColorGamuts: 00000000 + targetPixelDensity: 30 + resolutionScalingMode: 0 + resetResolutionOnWindowResize: 0 + androidSupportedAspectRatio: 1 + androidMaxAspectRatio: 2.1 + applicationIdentifier: + Android: com.appsflyer.engagement + iPhone: com.appsflyer.engagement + Standalone: com.appsflyer.engagement + buildNumber: + Standalone: 0 + iPhone: 1 + tvOS: 0 + overrideDefaultApplicationIdentifier: 1 + AndroidBundleVersionCode: 1 + AndroidMinSdkVersion: 22 + AndroidTargetSdkVersion: 0 + AndroidPreferredInstallLocation: 1 + aotOptions: + stripEngineCode: 0 + iPhoneStrippingLevel: 0 + iPhoneScriptCallOptimization: 0 + ForceInternetPermission: 0 + ForceSDCardPermission: 0 + CreateWallpaper: 0 + APKExpansionFiles: 0 + keepLoadedShadersAlive: 0 + StripUnusedMeshComponents: 0 + VertexChannelCompressionMask: 4054 + iPhoneSdkVersion: 988 + iOSTargetOSVersionString: 12.0 + tvOSSdkVersion: 0 + tvOSRequireExtendedGameController: 0 + tvOSTargetOSVersionString: 11.0 + uIPrerenderedIcon: 0 + uIRequiresPersistentWiFi: 0 + uIRequiresFullScreen: 1 + uIStatusBarHidden: 1 + uIExitOnSuspend: 0 + uIStatusBarStyle: 0 + iOSURLSchemes: [] + macOSURLSchemes: [] + iOSBackgroundModes: 0 + iOSMetalForceHardShadows: 0 + metalEditorSupport: 1 + metalAPIValidation: 1 + iOSRenderExtraFrameOnPause: 0 + iosCopyPluginsCodeInsteadOfSymlink: 0 + appleDeveloperTeamID: + iOSManualSigningProvisioningProfileID: + tvOSManualSigningProvisioningProfileID: + appleEnableAutomaticSigning: 1 + iOSRequireARKit: 0 + iOSAutomaticallyDetectAndAddCapabilities: 1 + appleEnableProMotion: 0 + shaderDefines: + m_GraphicsAPIs: + serializedVersion: 2 + m_APIs: 00000015 + m_Automatic: 0 + m_NormalMapEncoding: 1 + m_LightmapEncodingQuality: 1 + m_HDRCubemapEncodingQuality: 1 + m_LocalUsageFlags: 0 + m_DeferredCustomShader: {fileID: 0} + m_DeferredReflectionsCustomShader: {fileID: 0} + m_ScreenSpaceShadowsCustomShader: {fileID: 0} + m_LegacyDeferred: {fileID: 0} + m_DepthNormalsTexture: {fileID: 0} + m_MotionVectorsTexture: {fileID: 0} + m_LightHalo: {fileID: 0} + m_LensFlare: {fileID: 0} + m_AdditionalCompilerArguments: + scriptingDefineSymbols: {} + additionalCompilerArguments: {} + platformArchitecture: {} + scriptingBackend: + Android: 1 + il2cppCompilerConfiguration: {} + managedStrippingLevel: {} + incrementalIl2cppBuild: {} + suppressCommonWarnings: 1 + allowUnsafeCode: 0 + additionalIl2CppArgs: + scriptingRuntimeVersion: 1 + gcIncremental: 1 + assemblyVersionValidation: 1 + m_UseDeterministicCompilation: 1 + m_EnableRoslyn: 1 + m_RoslynAdditionalFiles: [] + androidSplashScreen: {fileID: 0} + m_AndroidKeystoreName: + m_AndroidKeyaliasName: + m_AndroidBuildApkPerCpuArchitecture: 0 + m_AndroidTVCompatibility: 0 + m_AndroidIsGame: 1 + m_AndroidEnableTango: 0 + androidEnableBanner: 1 + m_AndroidTargetArchitectures: 1 + m_AndroidTargetDevices: 0 + paymentEnabled: 0 + m_SubmitAnalytics: 1 + m_RequiresOnlineWorkshop: 0 + m_EnableCertificateVerification: 1 + m_AerialDistance: 0 + m_ShaderPrecisionModel: 0 + m_IncrementalGC: 1 diff --git a/test-app/ProjectSettings/ProjectVersion.txt b/test-app/ProjectSettings/ProjectVersion.txt new file mode 100644 index 00000000..622732e6 --- /dev/null +++ b/test-app/ProjectSettings/ProjectVersion.txt @@ -0,0 +1,2 @@ +m_EditorVersion: 6000.3.1f1 +m_EditorVersionWithRevision: 6000.3.1f1 (abc1234def56) diff --git a/test-app/ProjectSettings/QualitySettings.asset b/test-app/ProjectSettings/QualitySettings.asset new file mode 100644 index 00000000..64f8abaf --- /dev/null +++ b/test-app/ProjectSettings/QualitySettings.asset @@ -0,0 +1,347 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!47 &1 +QualitySettings: + m_ObjectHideFlags: 0 + serializedVersion: 5 + m_CurrentQuality: 5 + m_QualitySettings: + - serializedVersion: 5 + name: Very Low + pixelLightCount: 0 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 15 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + skinWeights: 1 + globalTextureMipmapLimit: 1 + textureMipmapLimitSettings: [] + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + useLegacyDetailDistribution: 0 + adaptiveVsync: 0 + vSyncCount: 0 + realtimeGICPUUsage: 25 + adaptiveVsyncExtraA: 0 + adaptiveVsyncExtraB: 0 + lodBias: 0.3 + meshLodThreshold: 1 + maximumLODLevel: 0 + enableLODCrossFade: 1 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 4 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + terrainQualityOverrides: 0 + terrainPixelError: 1 + terrainDetailDensityScale: 1 + terrainBasemapDistance: 1000 + terrainDetailDistance: 80 + terrainTreeDistance: 5000 + terrainBillboardStart: 50 + terrainFadeLength: 5 + terrainMaxTrees: 50 + excludedTargetPlatforms: [] + - serializedVersion: 5 + name: Low + pixelLightCount: 0 + shadows: 0 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 20 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + skinWeights: 2 + globalTextureMipmapLimit: 0 + textureMipmapLimitSettings: [] + anisotropicTextures: 0 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + useLegacyDetailDistribution: 0 + adaptiveVsync: 0 + vSyncCount: 0 + realtimeGICPUUsage: 25 + adaptiveVsyncExtraA: 0 + adaptiveVsyncExtraB: 0 + lodBias: 0.4 + meshLodThreshold: 1 + maximumLODLevel: 0 + enableLODCrossFade: 1 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 16 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + terrainQualityOverrides: 0 + terrainPixelError: 1 + terrainDetailDensityScale: 1 + terrainBasemapDistance: 1000 + terrainDetailDistance: 80 + terrainTreeDistance: 5000 + terrainBillboardStart: 50 + terrainFadeLength: 5 + terrainMaxTrees: 50 + excludedTargetPlatforms: [] + - serializedVersion: 5 + name: Medium + pixelLightCount: 1 + shadows: 1 + shadowResolution: 0 + shadowProjection: 1 + shadowCascades: 1 + shadowDistance: 20 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 0 + skinWeights: 2 + globalTextureMipmapLimit: 0 + textureMipmapLimitSettings: [] + anisotropicTextures: 1 + antiAliasing: 0 + softParticles: 0 + softVegetation: 0 + realtimeReflectionProbes: 0 + billboardsFaceCameraPosition: 0 + useLegacyDetailDistribution: 0 + adaptiveVsync: 0 + vSyncCount: 1 + realtimeGICPUUsage: 25 + adaptiveVsyncExtraA: 0 + adaptiveVsyncExtraB: 0 + lodBias: 0.7 + meshLodThreshold: 1 + maximumLODLevel: 0 + enableLODCrossFade: 1 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 64 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + terrainQualityOverrides: 0 + terrainPixelError: 1 + terrainDetailDensityScale: 1 + terrainBasemapDistance: 1000 + terrainDetailDistance: 80 + terrainTreeDistance: 5000 + terrainBillboardStart: 50 + terrainFadeLength: 5 + terrainMaxTrees: 50 + excludedTargetPlatforms: [] + - serializedVersion: 5 + name: High + pixelLightCount: 2 + shadows: 2 + shadowResolution: 1 + shadowProjection: 1 + shadowCascades: 2 + shadowDistance: 40 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + skinWeights: 2 + globalTextureMipmapLimit: 0 + textureMipmapLimitSettings: [] + anisotropicTextures: 1 + antiAliasing: 0 + softParticles: 0 + softVegetation: 1 + realtimeReflectionProbes: 1 + billboardsFaceCameraPosition: 1 + useLegacyDetailDistribution: 0 + adaptiveVsync: 0 + vSyncCount: 1 + realtimeGICPUUsage: 50 + adaptiveVsyncExtraA: 0 + adaptiveVsyncExtraB: 0 + lodBias: 1 + meshLodThreshold: 1 + maximumLODLevel: 0 + enableLODCrossFade: 1 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 256 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + terrainQualityOverrides: 0 + terrainPixelError: 1 + terrainDetailDensityScale: 1 + terrainBasemapDistance: 1000 + terrainDetailDistance: 80 + terrainTreeDistance: 5000 + terrainBillboardStart: 50 + terrainFadeLength: 5 + terrainMaxTrees: 50 + excludedTargetPlatforms: [] + - serializedVersion: 5 + name: Very High + pixelLightCount: 3 + shadows: 2 + shadowResolution: 2 + shadowProjection: 1 + shadowCascades: 2 + shadowDistance: 70 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + skinWeights: 4 + globalTextureMipmapLimit: 0 + textureMipmapLimitSettings: [] + anisotropicTextures: 2 + antiAliasing: 2 + softParticles: 1 + softVegetation: 1 + realtimeReflectionProbes: 1 + billboardsFaceCameraPosition: 1 + useLegacyDetailDistribution: 0 + adaptiveVsync: 0 + vSyncCount: 1 + realtimeGICPUUsage: 50 + adaptiveVsyncExtraA: 0 + adaptiveVsyncExtraB: 0 + lodBias: 1.5 + meshLodThreshold: 1 + maximumLODLevel: 0 + enableLODCrossFade: 1 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 1024 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + terrainQualityOverrides: 0 + terrainPixelError: 1 + terrainDetailDensityScale: 1 + terrainBasemapDistance: 1000 + terrainDetailDistance: 80 + terrainTreeDistance: 5000 + terrainBillboardStart: 50 + terrainFadeLength: 5 + terrainMaxTrees: 50 + excludedTargetPlatforms: [] + - serializedVersion: 5 + name: Ultra + pixelLightCount: 4 + shadows: 2 + shadowResolution: 2 + shadowProjection: 1 + shadowCascades: 4 + shadowDistance: 150 + shadowNearPlaneOffset: 3 + shadowCascade2Split: 0.33333334 + shadowCascade4Split: {x: 0.06666667, y: 0.2, z: 0.46666667} + shadowmaskMode: 1 + skinWeights: 255 + globalTextureMipmapLimit: 0 + textureMipmapLimitSettings: [] + anisotropicTextures: 2 + antiAliasing: 2 + softParticles: 1 + softVegetation: 1 + realtimeReflectionProbes: 1 + billboardsFaceCameraPosition: 1 + useLegacyDetailDistribution: 0 + adaptiveVsync: 0 + vSyncCount: 1 + realtimeGICPUUsage: 100 + adaptiveVsyncExtraA: 0 + adaptiveVsyncExtraB: 0 + lodBias: 2 + meshLodThreshold: 1 + maximumLODLevel: 0 + enableLODCrossFade: 1 + streamingMipmapsActive: 0 + streamingMipmapsAddAllCameras: 1 + streamingMipmapsMemoryBudget: 512 + streamingMipmapsRenderersPerFrame: 512 + streamingMipmapsMaxLevelReduction: 2 + streamingMipmapsMaxFileIORequests: 1024 + particleRaycastBudget: 4096 + asyncUploadTimeSlice: 2 + asyncUploadBufferSize: 16 + asyncUploadPersistentBuffer: 1 + resolutionScalingFixedDPIFactor: 1 + customRenderPipeline: {fileID: 0} + terrainQualityOverrides: 0 + terrainPixelError: 1 + terrainDetailDensityScale: 1 + terrainBasemapDistance: 1000 + terrainDetailDistance: 80 + terrainTreeDistance: 5000 + terrainBillboardStart: 50 + terrainFadeLength: 5 + terrainMaxTrees: 50 + excludedTargetPlatforms: [] + m_TextureMipmapLimitGroupNames: [] + m_PerPlatformDefaultQuality: + Android: 2 + EmbeddedLinux: 5 + GameCoreScarlett: 5 + GameCoreXboxOne: 5 + Kepler: 5 + LinuxHeadlessSimulation: 5 + Nintendo Switch: 5 + Nintendo Switch 2: 5 + PS4: 5 + PS5: 5 + QNX: 5 + Server: 5 + Standalone: 5 + VisionOS: 5 + WebGL: 3 + Windows Store Apps: 5 + XboxOne: 5 + iPhone: 2 + tvOS: 2 diff --git a/test-app/ProjectSettings/TagManager.asset b/test-app/ProjectSettings/TagManager.asset new file mode 100644 index 00000000..eb5d9aed --- /dev/null +++ b/test-app/ProjectSettings/TagManager.asset @@ -0,0 +1,45 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!78 &1 +TagManager: + serializedVersion: 3 + tags: [] + layers: + - Default + - TransparentFX + - Ignore Raycast + - + - Water + - UI + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + m_SortingLayers: + - name: Default + uniqueID: 0 + locked: 0 + m_RenderingLayers: + - Default diff --git a/test-app/ProjectSettings/TimeManager.asset b/test-app/ProjectSettings/TimeManager.asset new file mode 100644 index 00000000..2e23a1f4 --- /dev/null +++ b/test-app/ProjectSettings/TimeManager.asset @@ -0,0 +1,14 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!5 &1 +TimeManager: + m_ObjectHideFlags: 0 + serializedVersion: 2 + Fixed Timestep: + m_Count: 2822399 + m_Rate: + m_Denominator: 1 + m_Numerator: 141120000 + Maximum Allowed Timestep: 0.33333334 + m_TimeScale: 1 + Maximum Particle Timestep: 0.03 diff --git a/test-app/ProjectSettings/UnityConnectSettings.asset b/test-app/ProjectSettings/UnityConnectSettings.asset new file mode 100644 index 00000000..5ef56984 --- /dev/null +++ b/test-app/ProjectSettings/UnityConnectSettings.asset @@ -0,0 +1,40 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!310 &1 +UnityConnectSettings: + m_ObjectHideFlags: 0 + serializedVersion: 1 + m_Enabled: 0 + m_TestMode: 0 + m_EventOldUrl: https://api.uca.cloud.unity3d.com/v1/events + m_EventUrl: https://cdp.cloud.unity3d.com/v1/events + m_ConfigUrl: https://config.uca.cloud.unity3d.com + m_DashboardUrl: https://dashboard.unity3d.com + m_TestInitMode: 0 + InsightsSettings: + m_EngineDiagnosticsEnabled: 0 + m_Enabled: 0 + CrashReportingSettings: + serializedVersion: 2 + m_EventUrl: https://perf-events.cloud.unity3d.com + m_EnableCloudDiagnosticsReporting: 0 + m_LogBufferSize: 10 + m_CaptureEditorExceptions: 1 + UnityPurchasingSettings: + m_Enabled: 0 + m_TestMode: 0 + UnityAnalyticsSettings: + m_Enabled: 0 + m_TestMode: 0 + m_InitializeOnStartup: 1 + m_PackageRequiringCoreStatsPresent: 0 + UnityAdsSettings: + m_Enabled: 0 + m_InitializeOnStartup: 1 + m_TestMode: 0 + m_IosGameId: + m_AndroidGameId: + m_GameIds: {} + m_GameId: + PerformanceReportingSettings: + m_Enabled: 0 diff --git a/test-app/ProjectSettings/VFXManager.asset b/test-app/ProjectSettings/VFXManager.asset new file mode 100644 index 00000000..56783bbf --- /dev/null +++ b/test-app/ProjectSettings/VFXManager.asset @@ -0,0 +1,20 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!937362698 &1 +VFXManager: + m_ObjectHideFlags: 0 + m_IndirectShader: {fileID: 0} + m_CopyBufferShader: {fileID: 0} + m_PrefixSumShader: {fileID: 0} + m_SortShader: {fileID: 0} + m_StripUpdateShader: {fileID: 0} + m_EmptyShader: {fileID: 0} + m_RenderPipeSettingsPath: + m_FixedTimeStep: 0.016666668 + m_MaxDeltaTime: 0.05 + m_MaxScrubTime: 30 + m_MaxCapacity: 100000000 + m_CompiledVersion: 0 + m_RuntimeVersion: 0 + m_RuntimeResources: {fileID: 0} + m_BatchEmptyLifetime: 300 diff --git a/test-app/ProjectSettings/VersionControlSettings.asset b/test-app/ProjectSettings/VersionControlSettings.asset new file mode 100644 index 00000000..979fd8ec --- /dev/null +++ b/test-app/ProjectSettings/VersionControlSettings.asset @@ -0,0 +1,7 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!890905787 &1 +VersionControlSettings: + m_ObjectHideFlags: 0 + m_Mode: Visible Meta Files + m_TrackPackagesOutsideProject: 0 From 97d3d41dc5fcab0111cb2dec54fdfdb70519e263 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 12:54:03 +0300 Subject: [PATCH 02/51] add manual E2E workflow_dispatch trigger Allows running rc-e2e-android and rc-e2e-ios independently on any branch without triggering the full rc-release pipeline. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/e2e-manual.yml | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/e2e-manual.yml diff --git a/.github/workflows/e2e-manual.yml b/.github/workflows/e2e-manual.yml new file mode 100644 index 00000000..7b148aac --- /dev/null +++ b/.github/workflows/e2e-manual.yml @@ -0,0 +1,35 @@ +name: E2E — Manual trigger + +on: + workflow_dispatch: + inputs: + plugin_version: + description: Version label for the report (e.g. 6.18.0-rc1) + required: false + default: manual-test + platform: + description: Platform to test + required: true + type: choice + options: + - both + - android + - ios + default: both + +jobs: + e2e-android: + if: ${{ inputs.platform == 'both' || inputs.platform == 'android' }} + uses: ./.github/workflows/rc-e2e-android.yml + with: + plugin_version: ${{ inputs.plugin_version }} + release_branch: ${{ github.ref_name }} + secrets: inherit + + e2e-ios: + if: ${{ inputs.platform == 'both' || inputs.platform == 'ios' }} + uses: ./.github/workflows/rc-e2e-ios.yml + with: + plugin_version: ${{ inputs.plugin_version }} + release_branch: ${{ github.ref_name }} + secrets: inherit From 85c7fca29cc4825f25371e7eb8cad4162dba6fc9 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 13:02:50 +0300 Subject: [PATCH 03/51] fix e2e-manual: add push trigger on feature branch Runs Android and iOS E2E automatically on every push to plugins-effort-reduction-workflow1, bypassing the default-branch requirement for workflow_dispatch. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/e2e-manual.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/e2e-manual.yml b/.github/workflows/e2e-manual.yml index 7b148aac..443e4508 100644 --- a/.github/workflows/e2e-manual.yml +++ b/.github/workflows/e2e-manual.yml @@ -1,6 +1,9 @@ name: E2E — Manual trigger on: + push: + branches: + - plugins-effort-reduction-workflow1 workflow_dispatch: inputs: plugin_version: @@ -19,17 +22,17 @@ on: jobs: e2e-android: - if: ${{ inputs.platform == 'both' || inputs.platform == 'android' }} + if: ${{ (inputs.platform || 'both') == 'both' || (inputs.platform || 'both') == 'android' }} uses: ./.github/workflows/rc-e2e-android.yml with: - plugin_version: ${{ inputs.plugin_version }} + plugin_version: ${{ inputs.plugin_version || 'push-trigger' }} release_branch: ${{ github.ref_name }} secrets: inherit e2e-ios: - if: ${{ inputs.platform == 'both' || inputs.platform == 'ios' }} + if: ${{ (inputs.platform || 'both') == 'both' || (inputs.platform || 'both') == 'ios' }} uses: ./.github/workflows/rc-e2e-ios.yml with: - plugin_version: ${{ inputs.plugin_version }} + plugin_version: ${{ inputs.plugin_version || 'push-trigger' }} release_branch: ${{ github.ref_name }} secrets: inherit From 66f0ed83e4beabc22a7cd6f6cd37e43ac3ba8803 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 13:09:05 +0300 Subject: [PATCH 04/51] fix CI workflows: allowDirtyBuild + split iOS into Linux/macOS jobs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Android: add allowDirtyBuild=true to fix game-ci dirty branch error. iOS: game-ci Docker actions (unity-activate, unity-return-license) only run on Linux. Split into two jobs — Unity build on ubuntu-latest, then pod install + xcodebuild + E2E on macos-14. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-android.yml | 13 ++-- .github/workflows/rc-e2e-ios.yml | 92 +++++++++++++++++----------- 2 files changed, 62 insertions(+), 43 deletions(-) diff --git a/.github/workflows/rc-e2e-android.yml b/.github/workflows/rc-e2e-android.yml index c9b3b4df..dbf183c2 100644 --- a/.github/workflows/rc-e2e-android.yml +++ b/.github/workflows/rc-e2e-android.yml @@ -51,13 +51,14 @@ jobs: UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} with: - projectPath: test-app - unityVersion: 6000.3.14f1 - targetPlatform: Android - buildName: com.appsflyer.engagement - buildsPath: test-app/Build - buildMethod: BuildScript.BuildAndroid + projectPath: test-app + unityVersion: 6000.3.14f1 + targetPlatform: Android + buildName: com.appsflyer.engagement + buildsPath: test-app/Build + buildMethod: BuildScript.BuildAndroid androidExportType: androidPackage + allowDirtyBuild: true - name: Locate APK run: | diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index 9829713a..c8759b6b 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -22,10 +22,11 @@ on: required: true jobs: - e2e-ios: - name: E2E iOS — ${{ inputs.plugin_version }} - runs-on: macos-14 - timeout-minutes: 90 + # Job 1: Build Unity iOS Xcode project on Linux (game-ci Docker requires Linux) + build-ios-xcode: + name: Build iOS Xcode project — ${{ inputs.plugin_version }} + runs-on: ubuntu-latest + timeout-minutes: 60 steps: - name: Checkout release branch @@ -37,9 +38,6 @@ jobs: run: | printf '%s' "${{ secrets.ENV_FILE }}" > test-app/Assets/StreamingAssets/.env - - name: Install jq - run: brew install jq 2>/dev/null || true - - name: Activate Unity license uses: game-ci/unity-activate@v2 env: @@ -54,50 +52,74 @@ jobs: UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} with: - projectPath: test-app - unityVersion: 6000.3.14f1 - targetPlatform: iOS - buildName: UnityQATest - buildsPath: test-app/Build - buildMethod: BuildScript.BuildIOSSimulator - - - name: Locate Xcode project and install iOS pods + projectPath: test-app + unityVersion: 6000.3.14f1 + targetPlatform: iOS + buildName: UnityQATest + buildsPath: test-app/Build + buildMethod: BuildScript.BuildIOSSimulator + allowDirtyBuild: true + + - name: Upload Xcode project as artifact + uses: actions/upload-artifact@v4 + with: + name: ios-xcode-project-${{ inputs.plugin_version }} + path: test-app/Build/iOS-Simulator/ + retention-days: 1 + + - name: Return Unity license + if: always() + uses: game-ci/unity-return-license@v2 + + # Job 2: Pod install, compile .app, and run E2E on macOS + e2e-ios: + name: E2E iOS — ${{ inputs.plugin_version }} + runs-on: macos-14 + needs: build-ios-xcode + timeout-minutes: 60 + + steps: + - name: Checkout release branch + uses: actions/checkout@v4 + with: + ref: ${{ inputs.release_branch }} + + - name: Write .env from secret run: | - XCODE_PROJ=$(find test-app/Build -name "*.xcodeproj" -not -path "*/DerivedData/*" | head -1) - if [[ -z "$XCODE_PROJ" ]]; then - echo "::error::No .xcodeproj found under test-app/Build" - exit 1 - fi - IOS_BUILD_DIR=$(dirname "$XCODE_PROJ") - echo "IOS_BUILD_DIR=$IOS_BUILD_DIR" >> "$GITHUB_ENV" - echo "Found Xcode project in: $IOS_BUILD_DIR" + printf '%s' "${{ secrets.ENV_FILE }}" > test-app/Assets/StreamingAssets/.env + + - name: Install jq + run: brew install jq 2>/dev/null || true + + - name: Download Xcode project artifact + uses: actions/download-artifact@v4 + with: + name: ios-xcode-project-${{ inputs.plugin_version }} + path: test-app/Build/iOS-Simulator/ + - name: Install iOS pods + run: | chmod +x scripts/ios-pod-install.sh - scripts/ios-pod-install.sh "$IOS_BUILD_DIR" + scripts/ios-pod-install.sh test-app/Build/iOS-Simulator - name: Compile simulator .app from Xcode workspace run: | - XCODE_WS=$(find "$IOS_BUILD_DIR" -name "Unity-iPhone.xcworkspace" -not -path "*/DerivedData/*" | head -1) - if [[ -z "$XCODE_WS" ]]; then - echo "::error::Unity-iPhone.xcworkspace not found after pod install in $IOS_BUILD_DIR" - exit 1 - fi + XCODE_WS=test-app/Build/iOS-Simulator/Unity-iPhone.xcworkspace xcodebuild \ -workspace "$XCODE_WS" \ -scheme Unity-iPhone \ -sdk iphonesimulator \ -configuration Debug \ - -derivedDataPath "$IOS_BUILD_DIR/DerivedData" \ + -derivedDataPath test-app/Build/iOS-Simulator/DerivedData \ build | xcpretty || true - APP=$(find "$IOS_BUILD_DIR/DerivedData" -name "UnityQATest.app" -maxdepth 6 | head -1) + APP=$(find test-app/Build/iOS-Simulator/DerivedData -name "UnityQATest.app" -maxdepth 6 | head -1) if [[ -z "$APP" ]]; then echo "::error::UnityQATest.app not found after xcodebuild" exit 1 fi - mkdir -p test-app/Build/iOS-Simulator + rm -rf test-app/Build/iOS-Simulator/UnityQATest.app cp -R "$APP" test-app/Build/iOS-Simulator/UnityQATest.app - echo "APP_PATH=test-app/Build/iOS-Simulator/UnityQATest.app" >> "$GITHUB_ENV" - name: Boot iOS simulator run: | @@ -127,7 +149,3 @@ jobs: name: e2e-ios-report-${{ inputs.plugin_version }} path: .af-e2e/reports/ retention-days: 30 - - - name: Return Unity license - if: always() - uses: game-ci/unity-return-license@v2 From ddb04b7090b3a82f7e9e1ac50b8a7aad167c7b5e Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 13:30:32 +0300 Subject: [PATCH 05/51] fix CI: align Unity version to 2020.3.41f1 (matches master) and fix iOS-only guard - Set unityVersion to 2020.3.41f1 in both rc-e2e-android and rc-e2e-ios workflows - Revert test-app/ProjectSettings/ProjectVersion.txt from 6000.3.1f1 to 2020.3.41f1 - Guard iOSBuildPostProcess.cs with UNITY_EDITOR && UNITY_IOS to prevent UnityEditor.iOS.Xcode compile error on Android CI image Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-android.yml | 2 +- .github/workflows/rc-e2e-ios.yml | 2 +- test-app/Assets/Editor/iOSBuildPostProcess.cs | 2 +- test-app/ProjectSettings/ProjectVersion.txt | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rc-e2e-android.yml b/.github/workflows/rc-e2e-android.yml index dbf183c2..b291c8c7 100644 --- a/.github/workflows/rc-e2e-android.yml +++ b/.github/workflows/rc-e2e-android.yml @@ -52,7 +52,7 @@ jobs: UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} with: projectPath: test-app - unityVersion: 6000.3.14f1 + unityVersion: 2020.3.41f1 targetPlatform: Android buildName: com.appsflyer.engagement buildsPath: test-app/Build diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index c8759b6b..8a21b490 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -53,7 +53,7 @@ jobs: UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} with: projectPath: test-app - unityVersion: 6000.3.14f1 + unityVersion: 2020.3.41f1 targetPlatform: iOS buildName: UnityQATest buildsPath: test-app/Build diff --git a/test-app/Assets/Editor/iOSBuildPostProcess.cs b/test-app/Assets/Editor/iOSBuildPostProcess.cs index d070674a..cabf403f 100644 --- a/test-app/Assets/Editor/iOSBuildPostProcess.cs +++ b/test-app/Assets/Editor/iOSBuildPostProcess.cs @@ -1,4 +1,4 @@ -#if UNITY_EDITOR +#if UNITY_EDITOR && UNITY_IOS using System.IO; using UnityEditor; using UnityEditor.Callbacks; diff --git a/test-app/ProjectSettings/ProjectVersion.txt b/test-app/ProjectSettings/ProjectVersion.txt index 622732e6..1d617241 100644 --- a/test-app/ProjectSettings/ProjectVersion.txt +++ b/test-app/ProjectSettings/ProjectVersion.txt @@ -1,2 +1,2 @@ -m_EditorVersion: 6000.3.1f1 -m_EditorVersionWithRevision: 6000.3.1f1 (abc1234def56) +m_EditorVersion: 2020.3.41f1 +m_EditorVersionWithRevision: 2020.3.41f1 (7c19dc9acfda) From 48bafd7ce52a6d8dedf1f045268e55c8bb2d0089 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 14:04:09 +0300 Subject: [PATCH 06/51] fix CI: use Unity 6000.3.5f1 for test-app builds, fix iOS strongSelf compile error - Set unityVersion to 6000.3.5f1 in both E2E workflows (matches local build) - Update ProjectVersion.txt to 6000.3.5f1 accordingly - Fix UnityAppControllerDeepLink.mm: replace __strong typeof() with explicit type to resolve 'undeclared identifier strongSelf' clang error on CI Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-android.yml | 2 +- .github/workflows/rc-e2e-ios.yml | 2 +- test-app/Assets/iOS/UnityAppControllerDeepLink.mm | 2 +- test-app/ProjectSettings/ProjectVersion.txt | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rc-e2e-android.yml b/.github/workflows/rc-e2e-android.yml index b291c8c7..8fdde683 100644 --- a/.github/workflows/rc-e2e-android.yml +++ b/.github/workflows/rc-e2e-android.yml @@ -52,7 +52,7 @@ jobs: UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} with: projectPath: test-app - unityVersion: 2020.3.41f1 + unityVersion: 6000.3.5f1 targetPlatform: Android buildName: com.appsflyer.engagement buildsPath: test-app/Build diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index 8a21b490..7f7c3fdb 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -53,7 +53,7 @@ jobs: UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} with: projectPath: test-app - unityVersion: 2020.3.41f1 + unityVersion: 6000.3.5f1 targetPlatform: iOS buildName: UnityQATest buildsPath: test-app/Build diff --git a/test-app/Assets/iOS/UnityAppControllerDeepLink.mm b/test-app/Assets/iOS/UnityAppControllerDeepLink.mm index da568d57..d5d5c820 100644 --- a/test-app/Assets/iOS/UnityAppControllerDeepLink.mm +++ b/test-app/Assets/iOS/UnityAppControllerDeepLink.mm @@ -54,7 +54,7 @@ - (BOOL)qa_application:(UIApplication *)application __weak typeof(self) weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - __strong typeof(weakSelf) strongSelf = weakSelf; + UnityAppController *strongSelf = weakSelf; if (!strongSelf) return; // Route through the standard iOS URL-open delegate pipeline, // matching Flutter's AppDelegate approach. diff --git a/test-app/ProjectSettings/ProjectVersion.txt b/test-app/ProjectSettings/ProjectVersion.txt index 1d617241..835f4a69 100644 --- a/test-app/ProjectSettings/ProjectVersion.txt +++ b/test-app/ProjectSettings/ProjectVersion.txt @@ -1,2 +1,2 @@ -m_EditorVersion: 2020.3.41f1 -m_EditorVersionWithRevision: 2020.3.41f1 (7c19dc9acfda) +m_EditorVersion: 6000.3.5f1 +m_EditorVersionWithRevision: 6000.3.5f1 (a7a0f52f82fe) From 8b663da29a2f2e45fa7cad1ab119db7d16d7bd3d Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 14:07:03 +0300 Subject: [PATCH 07/51] fix ProjectVersion.txt: correct revision hash for Unity 6000.3.5f1 Co-Authored-By: Claude Sonnet 4.6 --- test-app/ProjectSettings/ProjectVersion.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-app/ProjectSettings/ProjectVersion.txt b/test-app/ProjectSettings/ProjectVersion.txt index 835f4a69..6447acbe 100644 --- a/test-app/ProjectSettings/ProjectVersion.txt +++ b/test-app/ProjectSettings/ProjectVersion.txt @@ -1,2 +1,2 @@ m_EditorVersion: 6000.3.5f1 -m_EditorVersionWithRevision: 6000.3.5f1 (a7a0f52f82fe) +m_EditorVersionWithRevision: 6000.3.5f1 (a1ec4b2f2d19) From 5b24fa2a89713b5bb63d2f5fbdba78d1f6a64994 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 14:24:33 +0300 Subject: [PATCH 08/51] fix CI: Android same-file cp, iOS build for arm64 on M1 runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Android: remove redundant cp in Locate APK — game-ci already writes APK to the expected path, copying to itself caused exit code 1 - iOS: add ARCHS=arm64 VALID_ARCHS=arm64 to xcodebuild — macOS-14 runners are Apple Silicon; building x86_64 hit missing Swift overlay libs in Xcode 15+ - iOS: remove || true from xcodebuild so build failures surface properly Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-android.yml | 4 +--- .github/workflows/rc-e2e-ios.yml | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/rc-e2e-android.yml b/.github/workflows/rc-e2e-android.yml index 8fdde683..e4c91a2e 100644 --- a/.github/workflows/rc-e2e-android.yml +++ b/.github/workflows/rc-e2e-android.yml @@ -67,9 +67,7 @@ jobs: echo "::error::APK not found after Unity build" exit 1 fi - mkdir -p test-app/Build/Android - cp "$APK" test-app/Build/Android/com.appsflyer.engagement.apk - echo "APK_PATH=test-app/Build/Android/com.appsflyer.engagement.apk" >> "$GITHUB_ENV" + echo "APK_PATH=$APK" >> "$GITHUB_ENV" - name: Install jq run: sudo apt-get install -y jq diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index 7f7c3fdb..997a1571 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -111,7 +111,9 @@ jobs: -sdk iphonesimulator \ -configuration Debug \ -derivedDataPath test-app/Build/iOS-Simulator/DerivedData \ - build | xcpretty || true + ARCHS=arm64 \ + VALID_ARCHS=arm64 \ + build | xcpretty APP=$(find test-app/Build/iOS-Simulator/DerivedData -name "UnityQATest.app" -maxdepth 6 | head -1) if [[ -z "$APP" ]]; then From 76c24a37a10e54e495d4569564922d0984b4024b Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 14:44:00 +0300 Subject: [PATCH 09/51] fix iOS xcodebuild: use x86_64 to match Unity Linux-generated libs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unity on Linux (game-ci Docker) compiles UnityRuntime.framework and baselib.a as x86_64 only. Must build for x86_64 (Rosetta on M1) — arm64 causes _UnityAutorotationStatusChanged undefined because the pre-compiled Unity libs are ignored. Previous x86_64 failures were with Unity 2020, not 6000.3.5f1. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-ios.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index 997a1571..06d014ba 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -111,8 +111,8 @@ jobs: -sdk iphonesimulator \ -configuration Debug \ -derivedDataPath test-app/Build/iOS-Simulator/DerivedData \ - ARCHS=arm64 \ - VALID_ARCHS=arm64 \ + ARCHS=x86_64 \ + VALID_ARCHS=x86_64 \ build | xcpretty APP=$(find test-app/Build/iOS-Simulator/DerivedData -name "UnityQATest.app" -maxdepth 6 | head -1) From 50cd78103bec97e6326fe2ffdffb3ea3e83f7cdd Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 14:56:29 +0300 Subject: [PATCH 10/51] fix iOS: set CURRENT_PROJECT_VERSION for valid CFBundleVersion in Info.plist Unity leaves $(CURRENT_PROJECT_VERSION) unresolved in Xcode project; simulator rejects install without a numeric CFBundleVersion value. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-ios.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index 06d014ba..7e2b5303 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -113,6 +113,8 @@ jobs: -derivedDataPath test-app/Build/iOS-Simulator/DerivedData \ ARCHS=x86_64 \ VALID_ARCHS=x86_64 \ + CURRENT_PROJECT_VERSION=1 \ + MARKETING_VERSION=1.0 \ build | xcpretty APP=$(find test-app/Build/iOS-Simulator/DerivedData -name "UnityQATest.app" -maxdepth 6 | head -1) From aaf201340cf4e8a4b1cfb49ec0387bf4b27c2f49 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 14:58:03 +0300 Subject: [PATCH 11/51] fix Android CI: enable KVM access so emulator boots with hardware acceleration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without KVM permissions the emulator runs in TCG software mode and never finishes booting — adb stays offline indefinitely. Standard udev rule fix grants the runner user access to /dev/kvm before launching the emulator. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-android.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/rc-e2e-android.yml b/.github/workflows/rc-e2e-android.yml index e4c91a2e..768c0b2a 100644 --- a/.github/workflows/rc-e2e-android.yml +++ b/.github/workflows/rc-e2e-android.yml @@ -72,6 +72,12 @@ jobs: - name: Install jq run: sudo apt-get install -y jq + - name: Enable KVM for hardware-accelerated emulator + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - name: Run E2E scenario runner inside Android emulator uses: reactivecircus/android-emulator-runner@v2 with: From 792a5f08d5392c9e6737a79c418cff9fadb1aa81 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 16:14:31 +0300 Subject: [PATCH 12/51] fix CI: Android x86_64 ABI for emulator, iOS linker missing Swift overlays - BuildScript: set AndroidArchitecture.X86_64 so APK installs on x86_64 emulator (Unity defaults to ARM64 only) - iOS xcodebuild: add -allow_missing_module_imported_lib to suppress auto-linked Swift overlay errors (swift_Builtin_float etc removed in Xcode 15+) - iOS xcodebuild: add set -o pipefail so build failures surface through xcpretty Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-ios.yml | 2 ++ test-app/Assets/Editor/BuildScript.cs | 1 + 2 files changed, 3 insertions(+) diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index 7e2b5303..81996482 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -104,6 +104,7 @@ jobs: - name: Compile simulator .app from Xcode workspace run: | + set -o pipefail XCODE_WS=test-app/Build/iOS-Simulator/Unity-iPhone.xcworkspace xcodebuild \ -workspace "$XCODE_WS" \ @@ -115,6 +116,7 @@ jobs: VALID_ARCHS=x86_64 \ CURRENT_PROJECT_VERSION=1 \ MARKETING_VERSION=1.0 \ + OTHER_LDFLAGS="-allow_missing_module_imported_lib" \ build | xcpretty APP=$(find test-app/Build/iOS-Simulator/DerivedData -name "UnityQATest.app" -maxdepth 6 | head -1) diff --git a/test-app/Assets/Editor/BuildScript.cs b/test-app/Assets/Editor/BuildScript.cs index 66c5813c..cea0e602 100644 --- a/test-app/Assets/Editor/BuildScript.cs +++ b/test-app/Assets/Editor/BuildScript.cs @@ -20,6 +20,7 @@ public static void BuildAndroid() PlayerSettings.productName = "UnityQATest"; PlayerSettings.Android.bundleVersionCode = 1; PlayerSettings.bundleVersion = "1.0"; + PlayerSettings.Android.targetArchitectures = AndroidArchitecture.X86_64; var options = new BuildPlayerOptions { From ce4399e99a742ae596073fa030559dcc37f06049 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 16:36:12 +0300 Subject: [PATCH 13/51] fix CI: Android adb push line continuation, iOS xcode error capture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Android: android-emulator-runner runs each script line as a separate sh -c, so backslash continuations don't work — flatten adb push and af-scenario-runner.sh onto single lines. iOS: remove unsupported -allow_missing_module_imported_lib flag (unknown on Xcode 15/macOS-14); add ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES=YES, tee raw xcodebuild output so actual linker errors are visible on failure. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-android.yml | 10 ++-------- .github/workflows/rc-e2e-ios.yml | 14 +++++++++++--- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/.github/workflows/rc-e2e-android.yml b/.github/workflows/rc-e2e-android.yml index 768c0b2a..26cc771c 100644 --- a/.github/workflows/rc-e2e-android.yml +++ b/.github/workflows/rc-e2e-android.yml @@ -88,19 +88,13 @@ jobs: emulator-options: -no-snapshot -no-window -no-boot-anim -gpu swiftshader_indirect disable-animations: true script: | - # Push .env to external storage (Unity persistentDataPath on Android) adb wait-for-device adb install -r "${{ env.APK_PATH }}" adb shell mkdir -p /storage/emulated/0/Android/data/com.appsflyer.engagement/files/ - adb push test-app/Assets/StreamingAssets/.env \ - /storage/emulated/0/Android/data/com.appsflyer.engagement/files/.env - # Cold-launch the app and run scenarios + adb push test-app/Assets/StreamingAssets/.env /storage/emulated/0/Android/data/com.appsflyer.engagement/files/.env adb shell am force-stop com.appsflyer.engagement chmod +x scripts/af-scenario-runner.sh - scripts/af-scenario-runner.sh \ - --platform android \ - --plan .af-e2e/test-plan.json \ - --verbose + scripts/af-scenario-runner.sh --platform android --plan .af-e2e/test-plan.json --verbose - name: Upload E2E report if: always() diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index 81996482..163c6d42 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -104,7 +104,6 @@ jobs: - name: Compile simulator .app from Xcode workspace run: | - set -o pipefail XCODE_WS=test-app/Build/iOS-Simulator/Unity-iPhone.xcworkspace xcodebuild \ -workspace "$XCODE_WS" \ @@ -114,10 +113,19 @@ jobs: -derivedDataPath test-app/Build/iOS-Simulator/DerivedData \ ARCHS=x86_64 \ VALID_ARCHS=x86_64 \ + ONLY_ACTIVE_ARCH=NO \ CURRENT_PROJECT_VERSION=1 \ MARKETING_VERSION=1.0 \ - OTHER_LDFLAGS="-allow_missing_module_imported_lib" \ - build | xcpretty + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES=YES \ + build 2>&1 | tee /tmp/xcode-build.log | xcpretty || true + # Detect failure from raw log (xcpretty masks exit code) + if grep -q "\*\* BUILD FAILED \*\*" /tmp/xcode-build.log; then + echo "::group::xcodebuild linker / compiler errors" + grep -A 30 "Undefined symbols" /tmp/xcode-build.log || true + grep -E "^(ld: |clang: error: |error: )" /tmp/xcode-build.log | head -40 || true + echo "::endgroup::" + exit 1 + fi APP=$(find test-app/Build/iOS-Simulator/DerivedData -name "UnityQATest.app" -maxdepth 6 | head -1) if [[ -z "$APP" ]]; then From 8754dfd28bc996d2ebbf9a99cc74327beca47a72 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 16:45:47 +0300 Subject: [PATCH 14/51] fix iOS CI: remove broken PurchaseConnector privacy bundle build phases PurchaseConnector 6.17.9 ships a Pods target 'PurchaseConnector-PurchaseConnector_Privacy' that references a source file 'PurchaseConnector_Privacy' which does not exist on disk, causing simulator xcodebuild to abort with "Build input file cannot be found". Add a post_install hook that strips all build phases from this target, making it a no-op so the rest of the build proceeds normally. Co-Authored-By: Claude Sonnet 4.6 --- scripts/ios-pod-install.sh | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/scripts/ios-pod-install.sh b/scripts/ios-pod-install.sh index 5538ae23..c27ec721 100755 --- a/scripts/ios-pod-install.sh +++ b/scripts/ios-pod-install.sh @@ -35,6 +35,17 @@ target 'UnityFramework' do pod 'AppsFlyerFramework', '6.17.9' pod 'PurchaseConnector', '6.17.9' end + +post_install do |installer| + # PurchaseConnector 6.17.9 ships a privacy bundle target that references a + # file 'PurchaseConnector_Privacy' which does not exist on disk, causing + # simulator builds to fail with "Build input file cannot be found". + # Strip all build phases from the broken target so it becomes a no-op. + installer.pods_project.targets.each do |target| + next unless target.name == 'PurchaseConnector-PurchaseConnector_Privacy' + target.build_phases.to_a.each(&:remove_from_project) + end +end PODFILE echo "[ios-pod-install] Running pod install in $IOS_BUILD_DIR" From 67fcdcb5ecb3aa45414b00d5df97a6a39257688a Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 17:02:56 +0300 Subject: [PATCH 15/51] fix Android E2E: read .env from StreamingAssets, enable file logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs prevented any log collection on Android: 1. QATestScript read .env from Application.persistentDataPath (internal storage) but the workflow was pushing to external storage — wrong path, config never loaded. Fix: use UnityWebRequest to read .env from StreamingAssets (already baked into the APK by the CI 'Write .env' step). Start() becomes a coroutine (InitAsync) to allow the async read. 2. AFQALogger only wrote af_qa_logs.txt on iOS. On Android only Debug.Log was called; after 240s the logcat buffer was overrun by Unity engine output and all [AF_QA] lines were lost. Fix: enable file logging on Android under the same (UNITY_IOS || UNITY_ANDROID) guard. Also remove the now-redundant adb mkdir + adb push .env lines from the workflow — the APK carries .env in its StreamingAssets bundle. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-android.yml | 2 - test-app/Assets/Scripts/AFQALogger.cs | 4 +- test-app/Assets/Scripts/QATestScript.cs | 68 +++++++++++++------------ 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/.github/workflows/rc-e2e-android.yml b/.github/workflows/rc-e2e-android.yml index 26cc771c..98893aad 100644 --- a/.github/workflows/rc-e2e-android.yml +++ b/.github/workflows/rc-e2e-android.yml @@ -90,8 +90,6 @@ jobs: script: | adb wait-for-device adb install -r "${{ env.APK_PATH }}" - adb shell mkdir -p /storage/emulated/0/Android/data/com.appsflyer.engagement/files/ - adb push test-app/Assets/StreamingAssets/.env /storage/emulated/0/Android/data/com.appsflyer.engagement/files/.env adb shell am force-stop com.appsflyer.engagement chmod +x scripts/af-scenario-runner.sh scripts/af-scenario-runner.sh --platform android --plan .af-e2e/test-plan.json --verbose diff --git a/test-app/Assets/Scripts/AFQALogger.cs b/test-app/Assets/Scripts/AFQALogger.cs index 58d87e5d..3fd48f00 100644 --- a/test-app/Assets/Scripts/AFQALogger.cs +++ b/test-app/Assets/Scripts/AFQALogger.cs @@ -7,7 +7,7 @@ public static class AFQALogger static AFQALogger() { -#if UNITY_IOS && !UNITY_EDITOR +#if (UNITY_IOS || UNITY_ANDROID) && !UNITY_EDITOR _logFilePath = Path.Combine(Application.persistentDataPath, "af_qa_logs.txt"); // Truncate on each fresh app launch so phase captures are self-contained. File.WriteAllText(_logFilePath, string.Empty); @@ -18,7 +18,7 @@ public static void Log(string message) { Debug.Log(message); -#if UNITY_IOS && !UNITY_EDITOR +#if (UNITY_IOS || UNITY_ANDROID) && !UNITY_EDITOR if (!string.IsNullOrEmpty(_logFilePath)) { File.AppendAllText(_logFilePath, message + "\n"); diff --git a/test-app/Assets/Scripts/QATestScript.cs b/test-app/Assets/Scripts/QATestScript.cs index ccecea47..cec04901 100644 --- a/test-app/Assets/Scripts/QATestScript.cs +++ b/test-app/Assets/Scripts/QATestScript.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using UnityEngine; +using UnityEngine.Networking; using AppsFlyerSDK; public class QATestScript : MonoBehaviour, IAppsFlyerConversionData @@ -22,8 +23,24 @@ static void AutoInit() void Start() { - if (!LoadConfig()) - return; + StartCoroutine(InitAsync()); + } + + void OnDestroy() + { + AppsFlyer.OnDeepLinkReceived -= OnDeepLinkReceived; + AppsFlyer.OnRequestResponse -= OnRequestResponse; + AppsFlyer.OnInAppResponse -= OnInAppResponse; + } + + // ── Initialisation ──────────────────────────────────────────────────────── + + IEnumerator InitAsync() + { + yield return StartCoroutine(LoadConfig()); + + if (string.IsNullOrEmpty(_devKey)) + yield break; AppsFlyer.OnRequestResponse += OnRequestResponse; AppsFlyer.OnInAppResponse += OnInAppResponse; @@ -40,72 +57,57 @@ void Start() StartCoroutine(RunPostStartApis()); } - void OnDestroy() - { - AppsFlyer.OnDeepLinkReceived -= OnDeepLinkReceived; - AppsFlyer.OnRequestResponse -= OnRequestResponse; - AppsFlyer.OnInAppResponse -= OnInAppResponse; - } - // ── Config loading ──────────────────────────────────────────────────────── - bool LoadConfig() + IEnumerator LoadConfig() { - // Primary path: StreamingAssets (embedded in build, works on all platforms). - // In CI the workflow writes secrets to test-app/Assets/StreamingAssets/.env - // before calling game-ci/unity-builder so they get baked into the binary. - string envPath = Path.Combine(Application.streamingAssetsPath, ".env"); - string content = null; #if UNITY_ANDROID && !UNITY_EDITOR - // On Android, StreamingAssets lives inside the APK; use the persistent - // copy written by the workflow via `adb shell run-as` (fallback path). - string persistentEnv = Path.Combine(Application.persistentDataPath, ".env"); - if (File.Exists(persistentEnv)) - { - content = File.ReadAllText(persistentEnv); - } + // On Android, StreamingAssets are inside the APK — use UnityWebRequest. + // The CI workflow bakes .env into StreamingAssets before calling unity-builder. + string url = Path.Combine(Application.streamingAssetsPath, ".env"); + using var req = UnityWebRequest.Get(url); + yield return req.SendWebRequest(); + if (req.result == UnityWebRequest.Result.Success) + content = req.downloadHandler.text; else - { - AFQALogger.Log("[AF_QA][CONFIG] .env not found at persistentDataPath; " + - "push it with: adb shell run-as com.appsflyer.engagement " + - "sh -c 'cat > /data/data/com.appsflyer.engagement/files/.env'"); - } + AFQALogger.Log("[AF_QA][CONFIG] .env read failed: " + req.error); #else + // iOS / Editor: StreamingAssets are on the regular filesystem. + string envPath = Path.Combine(Application.streamingAssetsPath, ".env"); if (File.Exists(envPath)) content = File.ReadAllText(envPath); else { - // Editor fallback: project root .env string editorEnv = Path.Combine(Application.dataPath, "../.env"); if (File.Exists(editorEnv)) content = File.ReadAllText(editorEnv); } + yield return null; #endif if (string.IsNullOrEmpty(content)) { AFQALogger.Log("[AF_QA][CONFIG] DEV_KEY missing"); - return false; + yield break; } foreach (var line in content.Split('\n')) { string trimmed = line.Trim(); - if (trimmed.StartsWith("DEV_KEY=")) _devKey = trimmed.Substring("DEV_KEY=".Length); - else if (trimmed.StartsWith("IOS_APP_ID=")) _iosAppId = trimmed.Substring("IOS_APP_ID=".Length); + if (trimmed.StartsWith("DEV_KEY=")) _devKey = trimmed.Substring("DEV_KEY=".Length); + else if (trimmed.StartsWith("IOS_APP_ID=")) _iosAppId = trimmed.Substring("IOS_APP_ID=".Length); else if (trimmed.StartsWith("ANDROID_APP_ID=")) _androidAppId = trimmed.Substring("ANDROID_APP_ID=".Length); } if (string.IsNullOrEmpty(_devKey)) { AFQALogger.Log("[AF_QA][CONFIG] DEV_KEY missing"); - return false; + yield break; } AFQALogger.Log("[AF_QA][CONFIG] loaded"); - return true; } // ── Pre-start APIs ──────────────────────────────────────────────────────── From ae87eebeea7f52c3ceaafc66ed4e5b7f8e4c4740 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 17:03:58 +0300 Subject: [PATCH 16/51] fix iOS CI: fully remove privacy bundle target instead of clearing phases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removing build phases from PurchaseConnector-PurchaseConnector_Privacy was not enough — Xcode's implicit bundle machinery still runs and fails looking for the compiled privacy manifest 'PurchaseConnector_Privacy'. The target must be removed entirely: delete all dependency edges pointing to it from every other Pods target, then remove it from the root project target list and the objects map. Co-Authored-By: Claude Sonnet 4.6 --- scripts/ios-pod-install.sh | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/scripts/ios-pod-install.sh b/scripts/ios-pod-install.sh index c27ec721..0f714026 100755 --- a/scripts/ios-pod-install.sh +++ b/scripts/ios-pod-install.sh @@ -37,14 +37,23 @@ target 'UnityFramework' do end post_install do |installer| - # PurchaseConnector 6.17.9 ships a privacy bundle target that references a - # file 'PurchaseConnector_Privacy' which does not exist on disk, causing - # simulator builds to fail with "Build input file cannot be found". - # Strip all build phases from the broken target so it becomes a no-op. - installer.pods_project.targets.each do |target| - next unless target.name == 'PurchaseConnector-PurchaseConnector_Privacy' - target.build_phases.to_a.each(&:remove_from_project) + # PurchaseConnector 6.17.9: the privacy bundle target references a compiled + # file 'PurchaseConnector_Privacy' that never gets created, breaking simulator + # builds with "Build input file cannot be found". Removing only the build + # phases is not enough — Xcode's implicit bundle machinery still runs. + # Fully remove the target and all dependency edges that point to it. + pods_project = installer.pods_project + privacy_target = pods_project.native_targets.find { |t| t.name == 'PurchaseConnector-PurchaseConnector_Privacy' } + next unless privacy_target + + pods_project.targets.each do |t| + t.dependencies.select { |d| + d.target_proxy.remote_global_id_string == privacy_target.uuid rescue false + }.each(&:remove_from_project) end + + pods_project.root_object.targets.delete(privacy_target) + privacy_target.remove_from_project end PODFILE From d2de54f779fb7c568ef73444811442a314898c10 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 17:11:31 +0300 Subject: [PATCH 17/51] fix iOS CI: remove all _Privacy pod targets generically All pod privacy bundle targets (AppsFlyerLib_Privacy, PurchaseConnector_Privacy, etc.) have the same broken compiled-manifest reference. Remove every target whose name ends with _Privacy, along with all dependency edges pointing to them. Co-Authored-By: Claude Sonnet 4.6 --- scripts/ios-pod-install.sh | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/scripts/ios-pod-install.sh b/scripts/ios-pod-install.sh index 0f714026..6601ca7d 100755 --- a/scripts/ios-pod-install.sh +++ b/scripts/ios-pod-install.sh @@ -37,23 +37,24 @@ target 'UnityFramework' do end post_install do |installer| - # PurchaseConnector 6.17.9: the privacy bundle target references a compiled - # file 'PurchaseConnector_Privacy' that never gets created, breaking simulator - # builds with "Build input file cannot be found". Removing only the build - # phases is not enough — Xcode's implicit bundle machinery still runs. - # Fully remove the target and all dependency edges that point to it. + # All pod privacy bundle targets (names ending in _Privacy) reference a + # compiled file that Xcode never produces on simulator builds, causing + # "Build input file cannot be found". Remove every such target and all + # dependency edges pointing to it. pods_project = installer.pods_project - privacy_target = pods_project.native_targets.find { |t| t.name == 'PurchaseConnector-PurchaseConnector_Privacy' } - next unless privacy_target + privacy_targets = pods_project.native_targets.select { |t| t.name.end_with?('_Privacy') } + privacy_uuids = privacy_targets.map(&:uuid).to_set pods_project.targets.each do |t| t.dependencies.select { |d| - d.target_proxy.remote_global_id_string == privacy_target.uuid rescue false + privacy_uuids.include?(d.target_proxy.remote_global_id_string) rescue false }.each(&:remove_from_project) end - pods_project.root_object.targets.delete(privacy_target) - privacy_target.remove_from_project + privacy_targets.each do |pt| + pods_project.root_object.targets.delete(pt) + pt.remove_from_project + end end PODFILE From 41e4014ed983e94cfe3661e7f7d322c48f04e4c8 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 17:34:27 +0300 Subject: [PATCH 18/51] fix Android E2E: root-cat fallback + no logcat tail limit; fix iOS: macos-13 Android log collection broke in two independent ways: 1. run-as fails when adb shell runs as root (Android 10+ CI emulators disable run-as for root callers). Result: platform_peek_qa_log always returned empty, so the marker wait ran the full 240s every phase. Fix: add direct `cat /data/data//files/af_qa_logs.txt` fallback in both platform_peek_qa_log and android_collect_logs. 2. `adb logcat -d -t 2000` only captures the last 2000 raw lines. Unity generates hundreds of lines/sec, so after the 240s wait the [AF_QA] lines (written in the first ~4s) are outside the window. Collected 0 matching lines even when the app ran correctly. Fix: remove the -t limit; the grep filter keeps the output file small. iOS: switch e2e-ios runner from macos-14 to macos-13. Xcode 15 (macos-14) dropped x86_64 simulator Swift overlay libraries (swift_DarwinFoundation*, swift_Builtin_float, SwiftUICore, UIUtilities) that AppsFlyerFramework 6.17.9's x86_64 slice auto-links against. macos-13 uses Xcode 14.3.1 which still ships them. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-ios.yml | 2 +- scripts/af-scenario-runner.sh | 48 ++++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 16 deletions(-) diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index 163c6d42..aa1b0a52 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -74,7 +74,7 @@ jobs: # Job 2: Pod install, compile .app, and run E2E on macOS e2e-ios: name: E2E iOS — ${{ inputs.plugin_version }} - runs-on: macos-14 + runs-on: macos-13 needs: build-ios-xcode timeout-minutes: 60 diff --git a/scripts/af-scenario-runner.sh b/scripts/af-scenario-runner.sh index f7d6be57..64fb64e6 100755 --- a/scripts/af-scenario-runner.sh +++ b/scripts/af-scenario-runner.sh @@ -214,37 +214,47 @@ android_get_pid() { android_collect_logs() { local log_file="$1" - local tail_lines="${ANDROID_LOGCAT_TAIL_LINES:-2000}" # Always start from an empty file so each phase capture is self-contained. : > "$log_file" - # Strategy 1: Read the app's af_qa_logs.txt from internal storage via - # `run-as`. Required because Flutter debug APKs launched standalone (no - # `flutter run` host) do not forward Dart `debugPrint` to logcat, so the - # file is the only reliable source of [AF_QA] markers. The Documents dir - # path on Android is `app_flutter/` for path_provider, but newer versions - # may write directly under `files/`, so try both. `run-as` works because - # `flutter build apk --debug` produces a debuggable APK. + # Strategy 1: Read the app's af_qa_logs.txt from internal storage. + # Try run-as first (works on non-rooted shells with debuggable APK), then + # fall back to a direct cat (works when adb shell runs as root, which + # Android 10+ CI emulators sometimes do, disabling run-as). local found=0 for path in app_flutter/af_qa_logs.txt files/af_qa_logs.txt; do if adb shell "run-as $PACKAGE_NAME cat $path 2>/dev/null" >> "$log_file" 2>/dev/null; then if [[ -s "$log_file" ]]; then - log_debug "Pulled Android QA log from $path" + log_debug "Pulled Android QA log via run-as from $path" found=1 break fi fi done if [[ "$found" -eq 0 ]]; then - log_debug "No af_qa_logs.txt found via run-as; relying on logcat only" + for abs_path in "/data/data/$PACKAGE_NAME/files/af_qa_logs.txt" "/data/user/0/$PACKAGE_NAME/files/af_qa_logs.txt"; do + local file_content + file_content=$(adb shell "cat $abs_path 2>/dev/null" 2>/dev/null) + if [[ -n "$file_content" ]]; then + printf '%s\n' "$file_content" >> "$log_file" + log_debug "Pulled Android QA log via root cat from $abs_path" + found=1 + break + fi + done + fi + if [[ "$found" -eq 0 ]]; then + log_debug "No af_qa_logs.txt found via run-as or root cat; relying on logcat only" fi # Strategy 2: Always also append logcat output. AppsFlyer SDK native logs - # (HTTP response codes, etc.) reach logcat regardless of the Dart-print - # routing, and the count_matches checks need them. Limit to the recent tail - # so CI does not spend a minute dumping the whole emulator buffer every phase. - adb logcat -d -t "$tail_lines" 2>&1 | grep -E "${LOG_TAG}|AppsFlyer|response code:|preparing data:" >> "$log_file" || true + # (HTTP response codes, etc.) reach logcat regardless of Debug.Log routing, + # and the count_matches checks need them. No line-count limit: Unity generates + # hundreds of lines/sec so -t 2000 only covers a few seconds of output, and + # the [AF_QA] lines (written in the first ~4s) would be outside the window + # after a 240s wait. The grep filter keeps the output file small. + adb logcat -d 2>&1 | grep -E "${LOG_TAG}|AppsFlyer|response code:|preparing data:" >> "$log_file" || true } android_background_app() { @@ -430,7 +440,15 @@ platform_trigger_deeplink() { platform_peek_qa_log() { if [[ "$PLATFORM" == "android" ]]; then for path in app_flutter/af_qa_logs.txt files/af_qa_logs.txt; do - adb shell "run-as $PACKAGE_NAME cat $path 2>/dev/null" 2>/dev/null && return 0 + local content + content=$(adb shell "run-as $PACKAGE_NAME cat $path 2>/dev/null" 2>/dev/null) + if [[ -n "$content" ]]; then echo "$content"; return 0; fi + done + # Fallback: direct cat works when adb runs as root (Android 10+ CI emulators) + for abs_path in "/data/data/$PACKAGE_NAME/files/af_qa_logs.txt" "/data/user/0/$PACKAGE_NAME/files/af_qa_logs.txt"; do + local content + content=$(adb shell "cat $abs_path 2>/dev/null" 2>/dev/null) + if [[ -n "$content" ]]; then echo "$content"; return 0; fi done return 0 fi From 15358c50cec93bd0df9bbd125f626548f4a6b2cf Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 22:18:40 +0300 Subject: [PATCH 19/51] fix Android E2E: add || true to prevent set -e exit on failed cat The root-cat fallback introduced in the previous commit used plain variable assignments (not local) for the command substitution result. With `set -euo pipefail`, when `adb shell "cat "` returns non-zero (file absent or inaccessible), the assignment propagates exit code 1 and the script exits immediately after printing "Collecting logs...". Fix: append `|| true` to every fallback `content=$(...)` / `file_content=$(...) assignment in both platform_peek_qa_log and android_collect_logs. Co-Authored-By: Claude Sonnet 4.6 --- scripts/af-scenario-runner.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/af-scenario-runner.sh b/scripts/af-scenario-runner.sh index 64fb64e6..81eb2547 100755 --- a/scripts/af-scenario-runner.sh +++ b/scripts/af-scenario-runner.sh @@ -235,7 +235,7 @@ android_collect_logs() { if [[ "$found" -eq 0 ]]; then for abs_path in "/data/data/$PACKAGE_NAME/files/af_qa_logs.txt" "/data/user/0/$PACKAGE_NAME/files/af_qa_logs.txt"; do local file_content - file_content=$(adb shell "cat $abs_path 2>/dev/null" 2>/dev/null) + file_content=$(adb shell "cat $abs_path 2>/dev/null" 2>/dev/null) || true if [[ -n "$file_content" ]]; then printf '%s\n' "$file_content" >> "$log_file" log_debug "Pulled Android QA log via root cat from $abs_path" @@ -441,13 +441,13 @@ platform_peek_qa_log() { if [[ "$PLATFORM" == "android" ]]; then for path in app_flutter/af_qa_logs.txt files/af_qa_logs.txt; do local content - content=$(adb shell "run-as $PACKAGE_NAME cat $path 2>/dev/null" 2>/dev/null) + content=$(adb shell "run-as $PACKAGE_NAME cat $path 2>/dev/null" 2>/dev/null) || true if [[ -n "$content" ]]; then echo "$content"; return 0; fi done # Fallback: direct cat works when adb runs as root (Android 10+ CI emulators) for abs_path in "/data/data/$PACKAGE_NAME/files/af_qa_logs.txt" "/data/user/0/$PACKAGE_NAME/files/af_qa_logs.txt"; do local content - content=$(adb shell "cat $abs_path 2>/dev/null" 2>/dev/null) + content=$(adb shell "cat $abs_path 2>/dev/null" 2>/dev/null) || true if [[ -n "$content" ]]; then echo "$content"; return 0; fi done return 0 From 24ea58975a75d4e2c6aae088503c611ca26d90c2 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 22:41:16 +0300 Subject: [PATCH 20/51] fix Android E2E: 32M logcat buffer + logcat fallback in marker peek Two related bugs caused 0 log lines even when the app ran correctly: 1. Ring buffer overflow: Unity generates ~100-200 logcat lines/sec; after a 240s wait the 2M buffer (set by android-emulator-runner) holds only the last ~10-20s of output, so [AF_QA] lines written in the first ~4s are gone by collection time. Fix: call `adb logcat -G 32m` in android_launch() before clearing the buffer. 32M holds >5 minutes of typical Unity output. 2. Marker never found early: platform_peek_qa_log only tried run-as and root-cat; when both are unavailable the full 240s timeout always fires, delaying log collection and wasting CI minutes. Fix: add `adb logcat -d | grep AF_QA` as the last-resort peek so the marker is found within the first 10s poll and collection runs while the buffer is definitely still fresh. Co-Authored-By: Claude Sonnet 4.6 --- scripts/af-scenario-runner.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/af-scenario-runner.sh b/scripts/af-scenario-runner.sh index 81eb2547..f2b8a58d 100755 --- a/scripts/af-scenario-runner.sh +++ b/scripts/af-scenario-runner.sh @@ -203,6 +203,10 @@ android_install() { android_launch() { log_info "Launching $PACKAGE_NAME..." + # Increase ring buffer so 240s of Unity output (~100-200 lines/sec) does not + # overflow it before log collection runs. The emulator runner sets it to 2M + # which holds only ~10-20s at typical Unity rates. + adb logcat -G 32m 2>/dev/null || true adb logcat -c adb shell am start -n "${PACKAGE_NAME}/${ACTIVITY}" 2>/dev/null || \ adb shell monkey -p "$PACKAGE_NAME" -c android.intent.category.LAUNCHER 1 2>/dev/null @@ -450,6 +454,10 @@ platform_peek_qa_log() { content=$(adb shell "cat $abs_path 2>/dev/null" 2>/dev/null) || true if [[ -n "$content" ]]; then echo "$content"; return 0; fi done + # Last resort: scan the current logcat ring buffer. With the buffer set to + # 32M in android_launch this is reliable as long as a poll runs within the + # first ~5 minutes of app output. + adb logcat -d 2>/dev/null | grep -E "AF_QA" 2>/dev/null || true return 0 fi ios_ensure_udid From 452fa989fa5d6f5c2226a2e040cac0e6ecc9e719 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 22:48:01 +0300 Subject: [PATCH 21/51] ci(ios): switch to macos-14 arm64 simulator to avoid macos-13 queue macos-13 runners are deprecated and have poor availability, causing 20+ minute queue waits. The reason macos-13 was needed: Xcode 15 dropped x86_64 Swift overlay libs (swift_DarwinFoundation*, SwiftUICore, etc.) that AppsFlyerFramework 6.17.9 auto-links against, causing linker errors. Switch to macos-14 (M1) and build arm64 simulator instead. Unity's iOS simulator framework libraries are fat binaries; the IL2CPP C++ source is compiled by Xcode on the target machine and is arch-agnostic. arm64 avoids the missing Swift overlay issue entirely since those overlays were only dropped for the x86_64 slice. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-ios.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index aa1b0a52..f112324a 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -74,7 +74,7 @@ jobs: # Job 2: Pod install, compile .app, and run E2E on macOS e2e-ios: name: E2E iOS — ${{ inputs.plugin_version }} - runs-on: macos-13 + runs-on: macos-14 needs: build-ios-xcode timeout-minutes: 60 @@ -111,8 +111,8 @@ jobs: -sdk iphonesimulator \ -configuration Debug \ -derivedDataPath test-app/Build/iOS-Simulator/DerivedData \ - ARCHS=x86_64 \ - VALID_ARCHS=x86_64 \ + ARCHS=arm64 \ + VALID_ARCHS=arm64 \ ONLY_ACTIVE_ARCH=NO \ CURRENT_PROJECT_VERSION=1 \ MARKETING_VERSION=1.0 \ From f85e76fcb7f3d074f4778f9c1a13c77f6e060de8 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Sun, 17 May 2026 23:10:36 +0300 Subject: [PATCH 22/51] fix Android E2E: correct activity name + add ARM64 build target Root cause of all Android CI failures: the test plan had the wrong activity name (com.unity3d.player.UnityPlayerActivity). The Unity AppsFlyer plugin generates AppsFlyerUnityActivity as the launcher activity. adb shell am start silently failed on every run, the app never launched, and there were zero logs. Confirmed locally: with the correct activity the app launches and all [AF_QA] logs appear correctly within 5 seconds. Also add ARM64 to the Android build targets alongside x86_64 so the APK installs on both CI x86_64 emulators and local ARM64 (Apple Silicon) emulators. Co-Authored-By: Claude Sonnet 4.6 --- .af-e2e/test-plan.json | 2 +- test-app/Assets/Editor/BuildScript.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.af-e2e/test-plan.json b/.af-e2e/test-plan.json index 6f9309ba..41c948e3 100644 --- a/.af-e2e/test-plan.json +++ b/.af-e2e/test-plan.json @@ -12,7 +12,7 @@ "config": { "android": { "package_name": "com.appsflyer.engagement", - "activity": "com.unity3d.player.UnityPlayerActivity", + "activity": "com.appsflyer.engagement.AppsFlyerUnityActivity", "apk_path": "test-app/Build/Android/com.appsflyer.engagement.apk", "build_cmd": "echo 'Build handled by game-ci/unity-builder in CI'" }, diff --git a/test-app/Assets/Editor/BuildScript.cs b/test-app/Assets/Editor/BuildScript.cs index cea0e602..fba799ff 100644 --- a/test-app/Assets/Editor/BuildScript.cs +++ b/test-app/Assets/Editor/BuildScript.cs @@ -20,7 +20,7 @@ public static void BuildAndroid() PlayerSettings.productName = "UnityQATest"; PlayerSettings.Android.bundleVersionCode = 1; PlayerSettings.bundleVersion = "1.0"; - PlayerSettings.Android.targetArchitectures = AndroidArchitecture.X86_64; + PlayerSettings.Android.targetArchitectures = AndroidArchitecture.X86_64 | AndroidArchitecture.ARM64; var options = new BuildPlayerOptions { From de0c5180ec5399de6057b733bdfe941ee547121d Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 11:12:22 +0300 Subject: [PATCH 23/51] =?UTF-8?q?ci(ios):=20single=20macOS=20job=20?= =?UTF-8?q?=E2=80=94=20Unity=20builds=20on=20macos-14=20producing=20arm64?= =?UTF-8?q?=20simulator=20libs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the Ubuntu Unity build + macOS xcodebuild two-job approach that failed because Linux game-ci ships x86_64-only prebuilt libs (baselib.a, UnityRuntime.framework). Building Unity on macos-14 produces arm64 libs that match the arm64 xcodebuild target, matching the local setup where 38/38 E2E checks pass. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/e2e-manual.yml | 2 +- .github/workflows/rc-e2e-ios-macos.yml | 129 +++++++++++++++++++++++++ 2 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/rc-e2e-ios-macos.yml diff --git a/.github/workflows/e2e-manual.yml b/.github/workflows/e2e-manual.yml index 443e4508..7085781b 100644 --- a/.github/workflows/e2e-manual.yml +++ b/.github/workflows/e2e-manual.yml @@ -31,7 +31,7 @@ jobs: e2e-ios: if: ${{ (inputs.platform || 'both') == 'both' || (inputs.platform || 'both') == 'ios' }} - uses: ./.github/workflows/rc-e2e-ios.yml + uses: ./.github/workflows/rc-e2e-ios-macos.yml with: plugin_version: ${{ inputs.plugin_version || 'push-trigger' }} release_branch: ${{ github.ref_name }} diff --git a/.github/workflows/rc-e2e-ios-macos.yml b/.github/workflows/rc-e2e-ios-macos.yml new file mode 100644 index 00000000..b201703c --- /dev/null +++ b/.github/workflows/rc-e2e-ios-macos.yml @@ -0,0 +1,129 @@ +name: RC E2E — iOS macOS (reusable) + +on: + workflow_call: + inputs: + plugin_version: + description: RC plugin version (e.g. 6.18.0-rc1) + required: true + type: string + release_branch: + description: Release branch to check out + required: true + type: string + secrets: + UNITY_EMAIL: + required: true + UNITY_PASSWORD: + required: true + UNITY_SERIAL: + required: true + ENV_FILE: + required: true + +jobs: + e2e-ios: + name: E2E iOS — ${{ inputs.plugin_version }} + runs-on: macos-14 + timeout-minutes: 90 + + steps: + - name: Checkout release branch + uses: actions/checkout@v4 + with: + ref: ${{ inputs.release_branch }} + + - name: Write .env from secret + run: | + printf '%s' "${{ secrets.ENV_FILE }}" > test-app/Assets/StreamingAssets/.env + + - name: Activate Unity license + uses: game-ci/unity-activate@v2 + env: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} + + - name: Build iOS Simulator Xcode project (macOS — arm64 libs) + uses: game-ci/unity-builder@v4 + env: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} + with: + projectPath: test-app + unityVersion: 6000.3.5f1 + targetPlatform: iOS + buildName: UnityQATest + buildsPath: test-app/Build + buildMethod: BuildScript.BuildIOSSimulator + allowDirtyBuild: true + + - name: Return Unity license + if: always() + uses: game-ci/unity-return-license@v2 + + - name: Install iOS pods + run: | + chmod +x scripts/ios-pod-install.sh + scripts/ios-pod-install.sh test-app/Build/iOS-Simulator + + - name: Compile simulator .app from Xcode workspace + run: | + XCODE_WS=test-app/Build/iOS-Simulator/Unity-iPhone.xcworkspace + xcodebuild \ + -workspace "$XCODE_WS" \ + -scheme Unity-iPhone \ + -sdk iphonesimulator \ + -configuration Debug \ + -derivedDataPath test-app/Build/iOS-Simulator/DerivedData \ + ARCHS=arm64 \ + VALID_ARCHS=arm64 \ + ONLY_ACTIVE_ARCH=NO \ + CURRENT_PROJECT_VERSION=1 \ + MARKETING_VERSION=1.0 \ + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES=YES \ + build 2>&1 | tee /tmp/xcode-build.log | xcpretty || true + if grep -q "\*\* BUILD FAILED \*\*" /tmp/xcode-build.log; then + echo "::group::xcodebuild errors" + grep -A 30 "Undefined symbols" /tmp/xcode-build.log || true + grep -E "^(ld: |clang: error: |error: )" /tmp/xcode-build.log | head -40 || true + echo "::endgroup::" + exit 1 + fi + APP=$(find test-app/Build/iOS-Simulator/DerivedData -name "UnityQATest.app" -maxdepth 6 | head -1) + if [[ -z "$APP" ]]; then + echo "::error::UnityQATest.app not found after xcodebuild" + exit 1 + fi + rm -rf test-app/Build/iOS-Simulator/UnityQATest.app + cp -R "$APP" test-app/Build/iOS-Simulator/UnityQATest.app + + - name: Boot iOS simulator + run: | + UDID=$(xcrun simctl list devices available -j | \ + jq -r '.devices | to_entries[] | select(.key | contains("iOS-18")) | .value[] | select(.isAvailable == true) | .udid' | head -1) + if [[ -z "$UDID" ]]; then + UDID=$(xcrun simctl list devices available -j | \ + jq -r '.devices[][] | select(.isAvailable == true) | .udid' | head -1) + fi + echo "Booting simulator: $UDID" + xcrun simctl boot "$UDID" || true + xcrun simctl bootstatus "$UDID" -b + echo "SIM_UDID=$UDID" >> "$GITHUB_ENV" + + - name: Run E2E scenario runner — iOS + run: | + chmod +x scripts/af-scenario-runner.sh + scripts/af-scenario-runner.sh \ + --platform ios \ + --plan .af-e2e/test-plan.json \ + --verbose + + - name: Upload E2E report + if: always() + uses: actions/upload-artifact@v4 + with: + name: e2e-ios-report-${{ inputs.plugin_version }} + path: .af-e2e/reports/ + retention-days: 30 From 61c272b6ecb2d83479ffec962878360e2b23114d Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 11:17:17 +0300 Subject: [PATCH 24/51] ci(ios): iOS-only push workflow, pin Xcode 15.4 for x86_64 Swift overlays - Add e2e-ios-only.yml: push-triggered, iOS only, no Android - Remove push trigger from e2e-manual.yml (manual dispatch only) - Delete rc-e2e-ios-macos.yml (game-ci is container/Linux only, fails on macOS) - rc-e2e-ios.yml: pin Xcode 15.4 + ARCHS=x86_64 to match Ubuntu Unity libs Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/e2e-ios-only.yml | 15 +++ .github/workflows/e2e-manual.yml | 5 +- .github/workflows/rc-e2e-ios-macos.yml | 129 ------------------------- .github/workflows/rc-e2e-ios.yml | 9 +- 4 files changed, 23 insertions(+), 135 deletions(-) create mode 100644 .github/workflows/e2e-ios-only.yml delete mode 100644 .github/workflows/rc-e2e-ios-macos.yml diff --git a/.github/workflows/e2e-ios-only.yml b/.github/workflows/e2e-ios-only.yml new file mode 100644 index 00000000..376f93a3 --- /dev/null +++ b/.github/workflows/e2e-ios-only.yml @@ -0,0 +1,15 @@ +name: E2E — iOS only + +on: + push: + branches: + - plugins-effort-reduction-workflow1 + workflow_dispatch: + +jobs: + e2e-ios: + uses: ./.github/workflows/rc-e2e-ios.yml + with: + plugin_version: ${{ github.event.inputs.plugin_version || 'push-trigger' }} + release_branch: ${{ github.ref_name }} + secrets: inherit diff --git a/.github/workflows/e2e-manual.yml b/.github/workflows/e2e-manual.yml index 7085781b..5bfe12f4 100644 --- a/.github/workflows/e2e-manual.yml +++ b/.github/workflows/e2e-manual.yml @@ -1,9 +1,6 @@ name: E2E — Manual trigger on: - push: - branches: - - plugins-effort-reduction-workflow1 workflow_dispatch: inputs: plugin_version: @@ -31,7 +28,7 @@ jobs: e2e-ios: if: ${{ (inputs.platform || 'both') == 'both' || (inputs.platform || 'both') == 'ios' }} - uses: ./.github/workflows/rc-e2e-ios-macos.yml + uses: ./.github/workflows/rc-e2e-ios.yml with: plugin_version: ${{ inputs.plugin_version || 'push-trigger' }} release_branch: ${{ github.ref_name }} diff --git a/.github/workflows/rc-e2e-ios-macos.yml b/.github/workflows/rc-e2e-ios-macos.yml deleted file mode 100644 index b201703c..00000000 --- a/.github/workflows/rc-e2e-ios-macos.yml +++ /dev/null @@ -1,129 +0,0 @@ -name: RC E2E — iOS macOS (reusable) - -on: - workflow_call: - inputs: - plugin_version: - description: RC plugin version (e.g. 6.18.0-rc1) - required: true - type: string - release_branch: - description: Release branch to check out - required: true - type: string - secrets: - UNITY_EMAIL: - required: true - UNITY_PASSWORD: - required: true - UNITY_SERIAL: - required: true - ENV_FILE: - required: true - -jobs: - e2e-ios: - name: E2E iOS — ${{ inputs.plugin_version }} - runs-on: macos-14 - timeout-minutes: 90 - - steps: - - name: Checkout release branch - uses: actions/checkout@v4 - with: - ref: ${{ inputs.release_branch }} - - - name: Write .env from secret - run: | - printf '%s' "${{ secrets.ENV_FILE }}" > test-app/Assets/StreamingAssets/.env - - - name: Activate Unity license - uses: game-ci/unity-activate@v2 - env: - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} - UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} - - - name: Build iOS Simulator Xcode project (macOS — arm64 libs) - uses: game-ci/unity-builder@v4 - env: - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} - UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} - with: - projectPath: test-app - unityVersion: 6000.3.5f1 - targetPlatform: iOS - buildName: UnityQATest - buildsPath: test-app/Build - buildMethod: BuildScript.BuildIOSSimulator - allowDirtyBuild: true - - - name: Return Unity license - if: always() - uses: game-ci/unity-return-license@v2 - - - name: Install iOS pods - run: | - chmod +x scripts/ios-pod-install.sh - scripts/ios-pod-install.sh test-app/Build/iOS-Simulator - - - name: Compile simulator .app from Xcode workspace - run: | - XCODE_WS=test-app/Build/iOS-Simulator/Unity-iPhone.xcworkspace - xcodebuild \ - -workspace "$XCODE_WS" \ - -scheme Unity-iPhone \ - -sdk iphonesimulator \ - -configuration Debug \ - -derivedDataPath test-app/Build/iOS-Simulator/DerivedData \ - ARCHS=arm64 \ - VALID_ARCHS=arm64 \ - ONLY_ACTIVE_ARCH=NO \ - CURRENT_PROJECT_VERSION=1 \ - MARKETING_VERSION=1.0 \ - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES=YES \ - build 2>&1 | tee /tmp/xcode-build.log | xcpretty || true - if grep -q "\*\* BUILD FAILED \*\*" /tmp/xcode-build.log; then - echo "::group::xcodebuild errors" - grep -A 30 "Undefined symbols" /tmp/xcode-build.log || true - grep -E "^(ld: |clang: error: |error: )" /tmp/xcode-build.log | head -40 || true - echo "::endgroup::" - exit 1 - fi - APP=$(find test-app/Build/iOS-Simulator/DerivedData -name "UnityQATest.app" -maxdepth 6 | head -1) - if [[ -z "$APP" ]]; then - echo "::error::UnityQATest.app not found after xcodebuild" - exit 1 - fi - rm -rf test-app/Build/iOS-Simulator/UnityQATest.app - cp -R "$APP" test-app/Build/iOS-Simulator/UnityQATest.app - - - name: Boot iOS simulator - run: | - UDID=$(xcrun simctl list devices available -j | \ - jq -r '.devices | to_entries[] | select(.key | contains("iOS-18")) | .value[] | select(.isAvailable == true) | .udid' | head -1) - if [[ -z "$UDID" ]]; then - UDID=$(xcrun simctl list devices available -j | \ - jq -r '.devices[][] | select(.isAvailable == true) | .udid' | head -1) - fi - echo "Booting simulator: $UDID" - xcrun simctl boot "$UDID" || true - xcrun simctl bootstatus "$UDID" -b - echo "SIM_UDID=$UDID" >> "$GITHUB_ENV" - - - name: Run E2E scenario runner — iOS - run: | - chmod +x scripts/af-scenario-runner.sh - scripts/af-scenario-runner.sh \ - --platform ios \ - --plan .af-e2e/test-plan.json \ - --verbose - - - name: Upload E2E report - if: always() - uses: actions/upload-artifact@v4 - with: - name: e2e-ios-report-${{ inputs.plugin_version }} - path: .af-e2e/reports/ - retention-days: 30 diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index f112324a..3c1c15f2 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -88,6 +88,11 @@ jobs: run: | printf '%s' "${{ secrets.ENV_FILE }}" > test-app/Assets/StreamingAssets/.env + - name: Pin Xcode 15 (x86_64 Swift overlays present) + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '15.4' + - name: Install jq run: brew install jq 2>/dev/null || true @@ -111,8 +116,8 @@ jobs: -sdk iphonesimulator \ -configuration Debug \ -derivedDataPath test-app/Build/iOS-Simulator/DerivedData \ - ARCHS=arm64 \ - VALID_ARCHS=arm64 \ + ARCHS=x86_64 \ + VALID_ARCHS=x86_64 \ ONLY_ACTIVE_ARCH=NO \ CURRENT_PROJECT_VERSION=1 \ MARKETING_VERSION=1.0 \ From d1d3c84092ef42af573f218c403741c396eb787e Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 11:25:05 +0300 Subject: [PATCH 25/51] ci(android): Android-only workflow + post_marker_wait_sec for async callbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add e2e-android-only.yml (workflow_dispatch, Android only). Add post_marker_wait_sec: 30 to phases 1/3/5 in test plan so the runner waits 30s after the auto-run-complete marker before collecting logs — giving onInstallConversionData time to arrive from AppsFlyer servers. Co-Authored-By: Claude Sonnet 4.6 --- .af-e2e/test-plan.json | 3 +++ .github/workflows/e2e-android-only.yml | 12 ++++++++++++ scripts/af-scenario-runner.sh | 9 +++++++++ 3 files changed, 24 insertions(+) create mode 100644 .github/workflows/e2e-android-only.yml diff --git a/.af-e2e/test-plan.json b/.af-e2e/test-plan.json index 41c948e3..1da6034b 100644 --- a/.af-e2e/test-plan.json +++ b/.af-e2e/test-plan.json @@ -31,6 +31,7 @@ "description": "Fresh install. Validate SDK startup, pre/post-start APIs, three auto-launched events with HTTP 200, and all standard callbacks.", "requires_fresh_install": true, "wait_after_launch_sec": 240, + "post_marker_wait_sec": 30, "checks": [ { "id": "sdk_started", @@ -169,6 +170,7 @@ "description": "Fresh install. App is in foreground after SDK start. On Android a brief HOME switch triggers onPause and the VIEW intent brings the app back. On iOS the app is terminated and re-launched with -deepLinkURL (same launch-arg path as phase_2); the foreground-vs-killed distinction is Android-only. Both paths fire onDeepLinking with status=FOUND.", "requires_fresh_install": true, "wait_after_launch_sec": 240, + "post_marker_wait_sec": 30, "wait_after_trigger_sec": 90, "deep_link_url": "afqa-unity://deeplink?deep_link_value=qa_deeplink_fg&af_sub1=foreground_test&pid=testmedia&c=deeplink_test", "pre_actions": { @@ -279,6 +281,7 @@ "description": "Fresh install. Sets customer user id (e2e_user_42), currency code (EUR), and additional data (tenant: qa_eu) before start. Verifies readback and propagation. Covers E2E-005.", "requires_fresh_install": true, "wait_after_launch_sec": 240, + "post_marker_wait_sec": 30, "wait_after_trigger_sec": 5, "checks": [ { diff --git a/.github/workflows/e2e-android-only.yml b/.github/workflows/e2e-android-only.yml new file mode 100644 index 00000000..6afe4a8e --- /dev/null +++ b/.github/workflows/e2e-android-only.yml @@ -0,0 +1,12 @@ +name: E2E — Android only + +on: + workflow_dispatch: + +jobs: + e2e-android: + uses: ./.github/workflows/rc-e2e-android.yml + with: + plugin_version: ${{ github.event.inputs.plugin_version || 'push-trigger' }} + release_branch: ${{ github.ref_name }} + secrets: inherit diff --git a/scripts/af-scenario-runner.sh b/scripts/af-scenario-runner.sh index f2b8a58d..fc0aa15c 100755 --- a/scripts/af-scenario-runner.sh +++ b/scripts/af-scenario-runner.sh @@ -717,6 +717,15 @@ run_phase() { # sleeping the full ceiling. Use a slower interval here because each ADB # `run-as cat` is costly on GitHub's emulator. wait_for_qa_marker "[AF_QA][AUTO_APIS] --- Auto run complete ---" "$wait_sec" 10 + + # Allow async network callbacks (e.g. onInstallConversionData) to arrive + # before collecting logs. Phases that need this set post_marker_wait_sec. + local post_wait + post_wait=$(echo "$phase_json" | jq -r '.post_marker_wait_sec // 0') + if [[ "$post_wait" -gt 0 ]]; then + log_info "Waiting ${post_wait}s for async callbacks..." + sleep "$post_wait" + fi fi # Pre-actions (deep link phases: background the app, etc.) From 9d02323b97484724dfe9884013f39366d7c520fc Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 11:29:09 +0300 Subject: [PATCH 26/51] =?UTF-8?q?ci(ios):=20install=20Unity=20natively=20o?= =?UTF-8?q?n=20macos-14=20=E2=80=94=20arm64=20libs=20match=20local=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit game-ci is Docker/container-only so it can't run on macOS. Linux game-ci produces x86_64-only prebuilt libs (baselib.a, UnityRuntime.framework). AppsFlyerFramework 6.17.9 (Swift 6) has no x86_64 simulator support so x86_64 builds fail on any Xcode. arm64 builds fail because Unity libs are x86_64-only. Installing Unity directly on macos-14 produces arm64 libs matching the local setup where 38/38 E2E checks pass. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-ios.yml | 119 +++++++++++++------------------ 1 file changed, 51 insertions(+), 68 deletions(-) diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index 3c1c15f2..9933578e 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -22,61 +22,10 @@ on: required: true jobs: - # Job 1: Build Unity iOS Xcode project on Linux (game-ci Docker requires Linux) - build-ios-xcode: - name: Build iOS Xcode project — ${{ inputs.plugin_version }} - runs-on: ubuntu-latest - timeout-minutes: 60 - - steps: - - name: Checkout release branch - uses: actions/checkout@v4 - with: - ref: ${{ inputs.release_branch }} - - - name: Write .env from secret - run: | - printf '%s' "${{ secrets.ENV_FILE }}" > test-app/Assets/StreamingAssets/.env - - - name: Activate Unity license - uses: game-ci/unity-activate@v2 - env: - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} - UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} - - - name: Build iOS Xcode project (via Unity) - uses: game-ci/unity-builder@v4 - env: - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} - UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} - with: - projectPath: test-app - unityVersion: 6000.3.5f1 - targetPlatform: iOS - buildName: UnityQATest - buildsPath: test-app/Build - buildMethod: BuildScript.BuildIOSSimulator - allowDirtyBuild: true - - - name: Upload Xcode project as artifact - uses: actions/upload-artifact@v4 - with: - name: ios-xcode-project-${{ inputs.plugin_version }} - path: test-app/Build/iOS-Simulator/ - retention-days: 1 - - - name: Return Unity license - if: always() - uses: game-ci/unity-return-license@v2 - - # Job 2: Pod install, compile .app, and run E2E on macOS e2e-ios: name: E2E iOS — ${{ inputs.plugin_version }} runs-on: macos-14 - needs: build-ios-xcode - timeout-minutes: 60 + timeout-minutes: 120 steps: - name: Checkout release branch @@ -88,19 +37,55 @@ jobs: run: | printf '%s' "${{ secrets.ENV_FILE }}" > test-app/Assets/StreamingAssets/.env - - name: Pin Xcode 15 (x86_64 Swift overlays present) - uses: maxim-lobanov/setup-xcode@v1 + - name: Cache Unity Editor installation + id: cache-unity + uses: actions/cache@v4 with: - xcode-version: '15.4' + path: /Applications/Unity/Hub/Editor/6000.3.5f1 + key: unity-6000.3.5f1-ios-macos14 - - name: Install jq - run: brew install jq 2>/dev/null || true + - name: Install Unity Hub + run: brew install --cask unity-hub 2>/dev/null || true - - name: Download Xcode project artifact - uses: actions/download-artifact@v4 - with: - name: ios-xcode-project-${{ inputs.plugin_version }} - path: test-app/Build/iOS-Simulator/ + - name: Install Unity Editor + iOS module + if: steps.cache-unity.outputs.cache-hit != 'true' + run: | + "/Applications/Unity Hub.app/Contents/MacOS/Unity Hub" -- \ + --headless install \ + --version 6000.3.5f1 \ + --module ios + timeout-minutes: 60 + + - name: Activate Unity license + run: | + UNITY="/Applications/Unity/Hub/Editor/6000.3.5f1/Unity.app/Contents/MacOS/Unity" + "$UNITY" -quit -batchmode \ + -serial "${{ secrets.UNITY_SERIAL }}" \ + -username "${{ secrets.UNITY_EMAIL }}" \ + -password "${{ secrets.UNITY_PASSWORD }}" \ + -logFile /tmp/unity-activate.log 2>&1 || true + grep -E "LICENSE|license|error|Error" /tmp/unity-activate.log || true + + - name: Build iOS Simulator Xcode project + run: | + UNITY="/Applications/Unity/Hub/Editor/6000.3.5f1/Unity.app/Contents/MacOS/Unity" + mkdir -p test-app/Build/iOS-Simulator + "$UNITY" -quit -batchmode \ + -logFile /tmp/unity-build.log \ + -projectPath "$(pwd)/test-app" \ + -buildTarget iOS \ + -executeMethod BuildScript.BuildIOSSimulator + BUILD_EXIT=$? + tail -50 /tmp/unity-build.log + exit $BUILD_EXIT + timeout-minutes: 30 + + - name: Return Unity license + if: always() + run: | + UNITY="/Applications/Unity/Hub/Editor/6000.3.5f1/Unity.app/Contents/MacOS/Unity" + "$UNITY" -quit -batchmode -returnlicense \ + -logFile /tmp/unity-return.log 2>&1 || true - name: Install iOS pods run: | @@ -116,22 +101,20 @@ jobs: -sdk iphonesimulator \ -configuration Debug \ -derivedDataPath test-app/Build/iOS-Simulator/DerivedData \ - ARCHS=x86_64 \ - VALID_ARCHS=x86_64 \ + ARCHS=arm64 \ + VALID_ARCHS=arm64 \ ONLY_ACTIVE_ARCH=NO \ CURRENT_PROJECT_VERSION=1 \ MARKETING_VERSION=1.0 \ ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES=YES \ build 2>&1 | tee /tmp/xcode-build.log | xcpretty || true - # Detect failure from raw log (xcpretty masks exit code) if grep -q "\*\* BUILD FAILED \*\*" /tmp/xcode-build.log; then - echo "::group::xcodebuild linker / compiler errors" + echo "::group::xcodebuild errors" grep -A 30 "Undefined symbols" /tmp/xcode-build.log || true grep -E "^(ld: |clang: error: |error: )" /tmp/xcode-build.log | head -40 || true echo "::endgroup::" exit 1 fi - APP=$(find test-app/Build/iOS-Simulator/DerivedData -name "UnityQATest.app" -maxdepth 6 | head -1) if [[ -z "$APP" ]]; then echo "::error::UnityQATest.app not found after xcodebuild" @@ -143,7 +126,7 @@ jobs: - name: Boot iOS simulator run: | UDID=$(xcrun simctl list devices available -j | \ - jq -r '.devices | to_entries[] | select(.key | contains("iOS-17")) | .value[] | select(.isAvailable == true) | .udid' | head -1) + jq -r '.devices | to_entries[] | select(.key | contains("iOS-18")) | .value[] | select(.isAvailable == true) | .udid' | head -1) if [[ -z "$UDID" ]]; then UDID=$(xcrun simctl list devices available -j | \ jq -r '.devices[][] | select(.isAvailable == true) | .udid' | head -1) From 71fb07f92e554bc5e4bb82ddc7237b76c3e4a870 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 11:34:37 +0300 Subject: [PATCH 27/51] ci(android): add push trigger to e2e-android-only workflow Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/e2e-android-only.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/e2e-android-only.yml b/.github/workflows/e2e-android-only.yml index 6afe4a8e..cf9c0325 100644 --- a/.github/workflows/e2e-android-only.yml +++ b/.github/workflows/e2e-android-only.yml @@ -1,6 +1,9 @@ name: E2E — Android only on: + push: + branches: + - plugins-effort-reduction-workflow1 workflow_dispatch: jobs: From 2f7d457a39a995319e38f5566e9e60eeb3f3a2f1 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 11:37:40 +0300 Subject: [PATCH 28/51] ci: make iOS and Android workflows manual-dispatch only Both workflows are now independent and only run when explicitly triggered. No automatic runs on push. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/e2e-android-only.yml | 3 --- .github/workflows/e2e-ios-only.yml | 3 --- 2 files changed, 6 deletions(-) diff --git a/.github/workflows/e2e-android-only.yml b/.github/workflows/e2e-android-only.yml index cf9c0325..6afe4a8e 100644 --- a/.github/workflows/e2e-android-only.yml +++ b/.github/workflows/e2e-android-only.yml @@ -1,9 +1,6 @@ name: E2E — Android only on: - push: - branches: - - plugins-effort-reduction-workflow1 workflow_dispatch: jobs: diff --git a/.github/workflows/e2e-ios-only.yml b/.github/workflows/e2e-ios-only.yml index 376f93a3..c84f7dfd 100644 --- a/.github/workflows/e2e-ios-only.yml +++ b/.github/workflows/e2e-ios-only.yml @@ -1,9 +1,6 @@ name: E2E — iOS only on: - push: - branches: - - plugins-effort-reduction-workflow1 workflow_dispatch: jobs: From 5d89fc6cf3be0a471b5af8128ba88118f3dd734f Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 11:39:01 +0300 Subject: [PATCH 29/51] ci(ios): add Unity 6000.3.5f1 changeset hash for Hub headless install Hub CLI requires --changeset for versions not yet in its release catalog. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-ios.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index 9933578e..3e329a6e 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -53,6 +53,7 @@ jobs: "/Applications/Unity Hub.app/Contents/MacOS/Unity Hub" -- \ --headless install \ --version 6000.3.5f1 \ + --changeset a1ec4b2f2d19 \ --module ios timeout-minutes: 60 From 7782b0eee13fd55ab5028355c970902a7c961d7e Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 11:44:59 +0300 Subject: [PATCH 30/51] ci(ios): pipe newline to Unity Hub to select Apple silicon non-interactively Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-ios.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index 3e329a6e..75ac5351 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -50,7 +50,7 @@ jobs: - name: Install Unity Editor + iOS module if: steps.cache-unity.outputs.cache-hit != 'true' run: | - "/Applications/Unity Hub.app/Contents/MacOS/Unity Hub" -- \ + echo "" | "/Applications/Unity Hub.app/Contents/MacOS/Unity Hub" -- \ --headless install \ --version 6000.3.5f1 \ --changeset a1ec4b2f2d19 \ From a5634d5d005cb56b4775bb30889ee10f54160756 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 11:55:20 +0300 Subject: [PATCH 31/51] =?UTF-8?q?ci(ios):=20install=20Unity=20via=20direct?= =?UTF-8?q?=20pkg=20download=20=E2=80=94=20bypass=20Unity=20Hub=20headless?= =?UTF-8?q?=20prompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unity Hub --headless install uses a TUI architecture prompt that ignores stdin. Download Editor and iOS module pkgs directly from Unity CDN instead. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-ios.yml | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index 75ac5351..439fd6c3 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -41,25 +41,23 @@ jobs: id: cache-unity uses: actions/cache@v4 with: - path: /Applications/Unity/Hub/Editor/6000.3.5f1 + path: /Applications/Unity/Unity.app key: unity-6000.3.5f1-ios-macos14 - - name: Install Unity Hub - run: brew install --cask unity-hub 2>/dev/null || true - - name: Install Unity Editor + iOS module if: steps.cache-unity.outputs.cache-hit != 'true' run: | - echo "" | "/Applications/Unity Hub.app/Contents/MacOS/Unity Hub" -- \ - --headless install \ - --version 6000.3.5f1 \ - --changeset a1ec4b2f2d19 \ - --module ios + curl -fL "https://download.unity3d.com/download_unity/a1ec4b2f2d19/MacEditorInstaller/Unity-6000.3.5f1.pkg" \ + -o /tmp/Unity.pkg + sudo installer -pkg /tmp/Unity.pkg -target / + curl -fL "https://download.unity3d.com/download_unity/a1ec4b2f2d19/MacEditorTargetInstaller/UnitySetup-iOS-Support-for-Editor-6000.3.5f1.pkg" \ + -o /tmp/Unity-iOS.pkg + sudo installer -pkg /tmp/Unity-iOS.pkg -target / timeout-minutes: 60 - name: Activate Unity license run: | - UNITY="/Applications/Unity/Hub/Editor/6000.3.5f1/Unity.app/Contents/MacOS/Unity" + UNITY="/Applications/Unity/Unity.app/Contents/MacOS/Unity" "$UNITY" -quit -batchmode \ -serial "${{ secrets.UNITY_SERIAL }}" \ -username "${{ secrets.UNITY_EMAIL }}" \ @@ -69,7 +67,7 @@ jobs: - name: Build iOS Simulator Xcode project run: | - UNITY="/Applications/Unity/Hub/Editor/6000.3.5f1/Unity.app/Contents/MacOS/Unity" + UNITY="/Applications/Unity/Unity.app/Contents/MacOS/Unity" mkdir -p test-app/Build/iOS-Simulator "$UNITY" -quit -batchmode \ -logFile /tmp/unity-build.log \ @@ -84,7 +82,7 @@ jobs: - name: Return Unity license if: always() run: | - UNITY="/Applications/Unity/Hub/Editor/6000.3.5f1/Unity.app/Contents/MacOS/Unity" + UNITY="/Applications/Unity/Unity.app/Contents/MacOS/Unity" "$UNITY" -quit -batchmode -returnlicense \ -logFile /tmp/unity-return.log 2>&1 || true From 2825c64fff201e3c4fa705332157cb23cafc3d01 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 12:38:14 +0300 Subject: [PATCH 32/51] fix(e2e): force-stop app before Android deep link phases instead of KEYCODE_HOME MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On CI emulators, Unity is killed under memory pressure when backgrounded via KEYCODE_HOME. onNewIntent + performOnDeepLinking then fires before the Unity runtime is ready → no callback. Force-stopping and cold-launching with the VIEW intent gives a clean start: SDK initializes fully before processing the deep link. Verified 38/38 locally. Co-Authored-By: Claude Sonnet 4.6 --- .af-e2e/test-plan.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.af-e2e/test-plan.json b/.af-e2e/test-plan.json index 1da6034b..24231b94 100644 --- a/.af-e2e/test-plan.json +++ b/.af-e2e/test-plan.json @@ -131,7 +131,7 @@ "wait_after_trigger_sec": 90, "deep_link_url": "afqa-unity://deeplink?deep_link_value=qa_deeplink_bg&af_sub1=background_test&pid=testmedia&c=deeplink_test", "pre_actions": { - "android": ["adb shell input keyevent KEYCODE_HOME", "sleep 2"], + "android": ["adb shell am force-stop com.appsflyer.engagement", "sleep 1"], "ios": ["xcrun simctl terminate {{UDID}} {{BUNDLE_ID}}", "sleep 1"] }, "trigger": { @@ -174,7 +174,7 @@ "wait_after_trigger_sec": 90, "deep_link_url": "afqa-unity://deeplink?deep_link_value=qa_deeplink_fg&af_sub1=foreground_test&pid=testmedia&c=deeplink_test", "pre_actions": { - "android": ["adb shell am start -a android.intent.action.MAIN -c android.intent.category.HOME", "sleep 1"], + "android": ["adb shell am force-stop com.appsflyer.engagement", "sleep 1"], "ios": ["xcrun simctl terminate {{UDID}} {{BUNDLE_ID}}", "sleep 1"] }, "trigger": { From eeaed2bb8601c93610c7504532d042ff17e12f62 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 14:14:12 +0300 Subject: [PATCH 33/51] fix(ios-ci): patch arm64 simulator libs + stub _Privacy.bundle in resources scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unity on ARM64 Mac generates x86_64 simulator baselib.a and UnityRuntime.framework by default. Copy the arm64 variants from Unity's PlaybackEngines after the build. CocoaPods resources scripts reference _Privacy.bundle targets that don't exist in simulator builds — replace those install_resource calls with : (no-op) to prevent "Build input file cannot be found" failures without breaking bash 3.2 if/fi blocks. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-ios.yml | 23 ++++++++++++++++++----- scripts/ios-pod-install.sh | 3 +++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index 439fd6c3..ae56352a 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -42,22 +42,25 @@ jobs: uses: actions/cache@v4 with: path: /Applications/Unity/Unity.app - key: unity-6000.3.5f1-ios-macos14 + key: unity-6000.3.5f1-ios-macos14-arm64 - name: Install Unity Editor + iOS module if: steps.cache-unity.outputs.cache-hit != 'true' run: | - curl -fL "https://download.unity3d.com/download_unity/a1ec4b2f2d19/MacEditorInstaller/Unity-6000.3.5f1.pkg" \ + curl -fL "https://download.unity3d.com/download_unity/a1ec4b2f2d19/MacEditorInstallerArm64/Unity-6000.3.5f1.pkg" \ -o /tmp/Unity.pkg sudo installer -pkg /tmp/Unity.pkg -target / curl -fL "https://download.unity3d.com/download_unity/a1ec4b2f2d19/MacEditorTargetInstaller/UnitySetup-iOS-Support-for-Editor-6000.3.5f1.pkg" \ -o /tmp/Unity-iOS.pkg sudo installer -pkg /tmp/Unity-iOS.pkg -target / + UNITY=$(find /Applications -name "Unity" -type f -path "*/MacOS/Unity" 2>/dev/null | head -1) + echo "Unity installed at: $UNITY" + echo "UNITY_PATH=$UNITY" >> "$GITHUB_ENV" timeout-minutes: 60 - name: Activate Unity license run: | - UNITY="/Applications/Unity/Unity.app/Contents/MacOS/Unity" + UNITY="${UNITY_PATH:-/Applications/Unity/Unity.app/Contents/MacOS/Unity}" "$UNITY" -quit -batchmode \ -serial "${{ secrets.UNITY_SERIAL }}" \ -username "${{ secrets.UNITY_EMAIL }}" \ @@ -67,7 +70,7 @@ jobs: - name: Build iOS Simulator Xcode project run: | - UNITY="/Applications/Unity/Unity.app/Contents/MacOS/Unity" + UNITY="${UNITY_PATH:-/Applications/Unity/Unity.app/Contents/MacOS/Unity}" mkdir -p test-app/Build/iOS-Simulator "$UNITY" -quit -batchmode \ -logFile /tmp/unity-build.log \ @@ -79,10 +82,20 @@ jobs: exit $BUILD_EXIT timeout-minutes: 30 + - name: Patch Unity libs to arm64 simulator + run: | + UNITY_APP="${UNITY_PATH%/Contents/MacOS/Unity}" + TRAMPOLINE="$UNITY_APP/Contents/PlaybackEngines/iOSSupport/Trampoline" + cp "$TRAMPOLINE/Libraries/baselib-sim-arm64.a" \ + test-app/Build/iOS-Simulator/Libraries/baselib.a + cp "$TRAMPOLINE/Frameworks/libiPhone-lib-sim-arm64/UnityRuntime.framework/UnityRuntime" \ + test-app/Build/iOS-Simulator/Frameworks/UnityRuntime.framework/UnityRuntime + echo "Patched to arm64 simulator" + - name: Return Unity license if: always() run: | - UNITY="/Applications/Unity/Unity.app/Contents/MacOS/Unity" + UNITY="${UNITY_PATH:-/Applications/Unity/Unity.app/Contents/MacOS/Unity}" "$UNITY" -quit -batchmode -returnlicense \ -logFile /tmp/unity-return.log 2>&1 || true diff --git a/scripts/ios-pod-install.sh b/scripts/ios-pod-install.sh index 6601ca7d..1818381f 100755 --- a/scripts/ios-pod-install.sh +++ b/scripts/ios-pod-install.sh @@ -62,6 +62,9 @@ echo "[ios-pod-install] Running pod install in $IOS_BUILD_DIR" cd "$IOS_BUILD_DIR" pod install +echo "[ios-pod-install] Stubbing out _Privacy.bundle install_resource calls (simulator builds only)" +find . -name "*-resources.sh" -exec sed -i '' 's/install_resource.*_Privacy\.bundle.*/:/' {} \; + echo "[ios-pod-install] Patching project.pbxproj — removing hardcoded SDKROOT so -sdk flag wins" # Unity hardcodes SDKROOT = iphoneos at the target level which overrides xcodebuild's -sdk flag. # Must run after pod install since pod install rewrites parts of the project. From f76ec493a12e6e5608ef2feced0a0d97270c1916 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 14:33:34 +0300 Subject: [PATCH 34/51] =?UTF-8?q?fix(ios-ci):=20use=20find=20to=20locate?= =?UTF-8?q?=20arm64=20Trampoline=20libs=20=E2=80=94=20works=20for=20both?= =?UTF-8?q?=20Hub=20and=20direct=20pkg=20install?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The direct pkg install puts PlaybackEngines alongside Unity.app, not inside Unity.app/Contents/. Using find resolves the path correctly in both cases. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-ios.yml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index ae56352a..aa1cef91 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -84,8 +84,13 @@ jobs: - name: Patch Unity libs to arm64 simulator run: | - UNITY_APP="${UNITY_PATH%/Contents/MacOS/Unity}" - TRAMPOLINE="$UNITY_APP/Contents/PlaybackEngines/iOSSupport/Trampoline" + TRAMPOLINE=$(find /Applications/Unity -name "baselib-sim-arm64.a" 2>/dev/null | head -1 | sed 's|/Libraries/baselib-sim-arm64.a||') + if [[ -z "$TRAMPOLINE" ]]; then + echo "::error::baselib-sim-arm64.a not found under /Applications/Unity" + find /Applications/Unity -name "PlaybackEngines" -type d 2>/dev/null | head -5 + exit 1 + fi + echo "Trampoline: $TRAMPOLINE" cp "$TRAMPOLINE/Libraries/baselib-sim-arm64.a" \ test-app/Build/iOS-Simulator/Libraries/baselib.a cp "$TRAMPOLINE/Frameworks/libiPhone-lib-sim-arm64/UnityRuntime.framework/UnityRuntime" \ From 7f95acd8cc717efb6270bea5e5af7f6a3880b8e7 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 15:24:23 +0300 Subject: [PATCH 35/51] fix(ios): remove PurchaseConnector from E2E Podfile; guard PC code with __has_include PurchaseConnector 6.17.9 was compiled with SDK 26.0 which is incompatible with CI runners on Xcode 16.x. The E2E test app does not exercise purchase connector functionality, so remove it from the test Podfile. Also fix AppsFlyeriOSWrapper to compile without PurchaseConnector: - Move PurchaseConnector-Swift.h import inside the __has_include guard in the header - Make @interface protocol conformances conditional on PC availability - Wrap all PC function bodies in #if __has_include guards with no-op stubs - Wrap PC @implementation methods in the same guard - Add explicit AppsFlyerLib-Swift.h import so AppsFlyerConsent is fully defined Co-Authored-By: Claude Sonnet 4.6 --- .../Plugins/iOS/AppsFlyeriOSWrapper.h | 11 +++++--- .../Plugins/iOS/AppsFlyeriOSWrapper.mm | 26 ++++++++++++++++--- scripts/ios-pod-install.sh | 2 -- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.h b/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.h index cf73afe5..6795d8c4 100644 --- a/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.h +++ b/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.h @@ -15,24 +15,29 @@ #endif #if __has_include() #import -#else +#import +#elif __has_include("PurchaseConnector.h") #import "PurchaseConnector.h" #endif -#import // Add StoreKit 2 support #if __has_include() #import #endif +#if __has_include() || __has_include("PurchaseConnector.h") @interface AppsFlyeriOSWarpper : NSObject +#else +@interface AppsFlyeriOSWarpper : NSObject +#endif + (BOOL) didCallStart; + (void) setDidCallStart:(BOOL)val; -// Add StoreKit 2 methods +#if __has_include() || __has_include("PurchaseConnector.h") - (void)setStoreKitVersion:(int)storeKitVersion; - (void)logConsumableTransaction:(id)transaction; +#endif @end diff --git a/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.mm b/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.mm index 34153d2b..57c8ffb1 100644 --- a/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.mm +++ b/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.mm @@ -11,6 +11,12 @@ #import #import "UnityFramework/UnityFramework-Swift.h" +#if __has_include() +#import +#elif __has_include("AppsFlyerLib-Swift.h") +#import "AppsFlyerLib-Swift.h" +#endif + #if __has_include() #import #elif __has_include("PurchaseConnector-Swift.h") @@ -351,6 +357,7 @@ const void _disableIDFVCollection(bool isDisabled) { } // Purchase connector +#if __has_include() || __has_include("PurchaseConnector-Swift.h") const void _startObservingTransactions() { [[PurchaseConnector shared] startObservingTransactions]; } @@ -388,16 +395,16 @@ const void _setPurchaseRevenueDataSource(const char* objectName) { } if (strstr(objectName, "StoreKit2") != NULL) { - + // Force protocol conformance Protocol *sk2Protocol = @protocol(AppsFlyerPurchaseRevenueDataSourceStoreKit2); class_addProtocol([_AppsFlyerdelegate class], sk2Protocol); - + if (![_AppsFlyerdelegate conformsToProtocol:@protocol(AppsFlyerPurchaseRevenueDataSourceStoreKit2)]) { NSLog(@"[AppsFlyer] Warning: SK2 protocol not conformed!"); } } - + [PurchaseConnector shared].purchaseRevenueDataSource = _AppsFlyerdelegate; } @@ -417,6 +424,17 @@ const void _logConsumableTransaction(const char* transactionId) { }]; } } +#else + const void _startObservingTransactions() {} + const void _stopObservingTransactions() {} + const void _setIsSandbox(bool isSandBox) {} + const void _setPurchaseRevenueDelegate() {} + const void _setAutoLogPurchaseRevenue(int option) {} + const void _initPurchaseConnector(const char* objectName) {} + const void _setPurchaseRevenueDataSource(const char* objectName) {} + const void _setStoreKitVersion(int storeKitVersion) {} + const void _logConsumableTransaction(const char* transactionId) {} +#endif #ifdef __cplusplus extern "C" { @@ -483,6 +501,7 @@ - (void)didResolveDeepLink:(AppsFlyerDeepLinkResult *)result{ } // Purchase Connector +#if __has_include() || __has_include("PurchaseConnector-Swift.h") - (void)didReceivePurchaseRevenueValidationInfo:(NSDictionary *)validationInfo error:(NSError *)error { if (error != nil) { unityCallBack(onPurchaseValidationObjectName, PURCHASE_REVENUE_ERROR_CALLBACK, [[error localizedDescription] UTF8String]); @@ -595,6 +614,7 @@ - (NSDictionary *)purchaseRevenueAdditionalParametersStoreKit2ForProducts:(NSSet } return @{}; } +#endif // PurchaseConnector @end diff --git a/scripts/ios-pod-install.sh b/scripts/ios-pod-install.sh index 1818381f..fa2bb098 100755 --- a/scripts/ios-pod-install.sh +++ b/scripts/ios-pod-install.sh @@ -24,7 +24,6 @@ use_frameworks! :linkage => :static target 'Unity-iPhone' do pod 'AppsFlyerFramework', '6.17.9' - pod 'PurchaseConnector', '6.17.9' target 'Unity-iPhone Tests' do inherit! :search_paths @@ -33,7 +32,6 @@ end target 'UnityFramework' do pod 'AppsFlyerFramework', '6.17.9' - pod 'PurchaseConnector', '6.17.9' end post_install do |installer| From 1f6869e9a0f62fd5ec8f16d6765cc7f25993745f Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 15:50:49 +0300 Subject: [PATCH 36/51] =?UTF-8?q?ci(ios):=20run=20E2E=203=C3=97=20sequenti?= =?UTF-8?q?ally=20for=20stability=20validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit matrix: [1,2,3] with max-parallel:1 avoids Unity license conflicts (serial can only be active on one machine at a time). fail-fast:false so all 3 runs complete even if one fails. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/e2e-ios-only.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/e2e-ios-only.yml b/.github/workflows/e2e-ios-only.yml index c84f7dfd..5175cb39 100644 --- a/.github/workflows/e2e-ios-only.yml +++ b/.github/workflows/e2e-ios-only.yml @@ -5,8 +5,13 @@ on: jobs: e2e-ios: + strategy: + matrix: + run: [1, 2, 3] + fail-fast: false + max-parallel: 1 uses: ./.github/workflows/rc-e2e-ios.yml with: - plugin_version: ${{ github.event.inputs.plugin_version || 'push-trigger' }} + plugin_version: stability-run-${{ matrix.run }} release_branch: ${{ github.ref_name }} secrets: inherit From c804c694a545b0a11e82164fbc666855a2995c25 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 16:19:14 +0300 Subject: [PATCH 37/51] ci(ios): switch to macos-15 + Xcode 26, restore PurchaseConnector MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit macos-15 runner ships Xcode 26.x. PurchaseConnector 6.17.9 was compiled with SDK 26.0 and requires it. No conditional compile workarounds needed. Changes: - runs-on: macos-14 → macos-15 - Add "Select Xcode 26" step (xcode-select -s Xcode_26.3.app) - Update cache key to macos15-arm64 - Simulator boot targets iOS-19 (Xcode 26 default runtime) - Restore PurchaseConnector 6.17.9 in Podfile - Revert AppsFlyeriOSWrapper.h/.mm to original (no conditional PC guards) Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-ios.yml | 9 ++++++--- .../Plugins/iOS/AppsFlyeriOSWrapper.h | 11 +++------- .../Plugins/iOS/AppsFlyeriOSWrapper.mm | 20 ------------------- scripts/ios-pod-install.sh | 2 ++ 4 files changed, 11 insertions(+), 31 deletions(-) diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index aa1cef91..9b7e4891 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -24,7 +24,7 @@ on: jobs: e2e-ios: name: E2E iOS — ${{ inputs.plugin_version }} - runs-on: macos-14 + runs-on: macos-15 timeout-minutes: 120 steps: @@ -33,6 +33,9 @@ jobs: with: ref: ${{ inputs.release_branch }} + - name: Select Xcode 26 + run: sudo xcode-select -s /Applications/Xcode_26.3.app + - name: Write .env from secret run: | printf '%s' "${{ secrets.ENV_FILE }}" > test-app/Assets/StreamingAssets/.env @@ -42,7 +45,7 @@ jobs: uses: actions/cache@v4 with: path: /Applications/Unity/Unity.app - key: unity-6000.3.5f1-ios-macos14-arm64 + key: unity-6000.3.5f1-ios-macos15-arm64 - name: Install Unity Editor + iOS module if: steps.cache-unity.outputs.cache-hit != 'true' @@ -143,7 +146,7 @@ jobs: - name: Boot iOS simulator run: | UDID=$(xcrun simctl list devices available -j | \ - jq -r '.devices | to_entries[] | select(.key | contains("iOS-18")) | .value[] | select(.isAvailable == true) | .udid' | head -1) + jq -r '.devices | to_entries[] | select(.key | contains("iOS-19")) | .value[] | select(.isAvailable == true) | .udid' | head -1) if [[ -z "$UDID" ]]; then UDID=$(xcrun simctl list devices available -j | \ jq -r '.devices[][] | select(.isAvailable == true) | .udid' | head -1) diff --git a/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.h b/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.h index 6795d8c4..cf73afe5 100644 --- a/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.h +++ b/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.h @@ -15,29 +15,24 @@ #endif #if __has_include() #import -#import -#elif __has_include("PurchaseConnector.h") +#else #import "PurchaseConnector.h" #endif +#import // Add StoreKit 2 support #if __has_include() #import #endif -#if __has_include() || __has_include("PurchaseConnector.h") @interface AppsFlyeriOSWarpper : NSObject -#else -@interface AppsFlyeriOSWarpper : NSObject -#endif + (BOOL) didCallStart; + (void) setDidCallStart:(BOOL)val; -#if __has_include() || __has_include("PurchaseConnector.h") +// Add StoreKit 2 methods - (void)setStoreKitVersion:(int)storeKitVersion; - (void)logConsumableTransaction:(id)transaction; -#endif @end diff --git a/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.mm b/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.mm index 57c8ffb1..7af1d225 100644 --- a/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.mm +++ b/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.mm @@ -11,12 +11,6 @@ #import #import "UnityFramework/UnityFramework-Swift.h" -#if __has_include() -#import -#elif __has_include("AppsFlyerLib-Swift.h") -#import "AppsFlyerLib-Swift.h" -#endif - #if __has_include() #import #elif __has_include("PurchaseConnector-Swift.h") @@ -357,7 +351,6 @@ const void _disableIDFVCollection(bool isDisabled) { } // Purchase connector -#if __has_include() || __has_include("PurchaseConnector-Swift.h") const void _startObservingTransactions() { [[PurchaseConnector shared] startObservingTransactions]; } @@ -424,17 +417,6 @@ const void _logConsumableTransaction(const char* transactionId) { }]; } } -#else - const void _startObservingTransactions() {} - const void _stopObservingTransactions() {} - const void _setIsSandbox(bool isSandBox) {} - const void _setPurchaseRevenueDelegate() {} - const void _setAutoLogPurchaseRevenue(int option) {} - const void _initPurchaseConnector(const char* objectName) {} - const void _setPurchaseRevenueDataSource(const char* objectName) {} - const void _setStoreKitVersion(int storeKitVersion) {} - const void _logConsumableTransaction(const char* transactionId) {} -#endif #ifdef __cplusplus extern "C" { @@ -501,7 +483,6 @@ - (void)didResolveDeepLink:(AppsFlyerDeepLinkResult *)result{ } // Purchase Connector -#if __has_include() || __has_include("PurchaseConnector-Swift.h") - (void)didReceivePurchaseRevenueValidationInfo:(NSDictionary *)validationInfo error:(NSError *)error { if (error != nil) { unityCallBack(onPurchaseValidationObjectName, PURCHASE_REVENUE_ERROR_CALLBACK, [[error localizedDescription] UTF8String]); @@ -614,7 +595,6 @@ - (NSDictionary *)purchaseRevenueAdditionalParametersStoreKit2ForProducts:(NSSet } return @{}; } -#endif // PurchaseConnector @end diff --git a/scripts/ios-pod-install.sh b/scripts/ios-pod-install.sh index fa2bb098..1818381f 100755 --- a/scripts/ios-pod-install.sh +++ b/scripts/ios-pod-install.sh @@ -24,6 +24,7 @@ use_frameworks! :linkage => :static target 'Unity-iPhone' do pod 'AppsFlyerFramework', '6.17.9' + pod 'PurchaseConnector', '6.17.9' target 'Unity-iPhone Tests' do inherit! :search_paths @@ -32,6 +33,7 @@ end target 'UnityFramework' do pod 'AppsFlyerFramework', '6.17.9' + pod 'PurchaseConnector', '6.17.9' end post_install do |installer| From d6fc343f450eff0f8b2a4c974358301412cf7587 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 16:42:37 +0300 Subject: [PATCH 38/51] test: try PurchaseConnector 6.18.1 in E2E iOS Podfile Co-Authored-By: Claude Sonnet 4.6 --- scripts/ios-pod-install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/ios-pod-install.sh b/scripts/ios-pod-install.sh index 1818381f..fe19786c 100755 --- a/scripts/ios-pod-install.sh +++ b/scripts/ios-pod-install.sh @@ -24,7 +24,7 @@ use_frameworks! :linkage => :static target 'Unity-iPhone' do pod 'AppsFlyerFramework', '6.17.9' - pod 'PurchaseConnector', '6.17.9' + pod 'PurchaseConnector', '6.18.1' target 'Unity-iPhone Tests' do inherit! :search_paths @@ -33,7 +33,7 @@ end target 'UnityFramework' do pod 'AppsFlyerFramework', '6.17.9' - pod 'PurchaseConnector', '6.17.9' + pod 'PurchaseConnector', '6.18.1' end post_install do |installer| From df8a32b640341ce98fc30c0ac29581118a11d41d Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 16:47:22 +0300 Subject: [PATCH 39/51] test: use AppsFlyerFramework 6.18.0 + PurchaseConnector 6.18.1 in E2E Podfile Both binaries compiled with iOS 17.x SDK (no iOS 26 restricted frameworks). PC 6.18.1 requires AFLib 6.18.0 per podspec dependency. Co-Authored-By: Claude Sonnet 4.6 --- scripts/ios-pod-install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/ios-pod-install.sh b/scripts/ios-pod-install.sh index fe19786c..1571a006 100755 --- a/scripts/ios-pod-install.sh +++ b/scripts/ios-pod-install.sh @@ -23,7 +23,7 @@ platform :ios, '15.0' use_frameworks! :linkage => :static target 'Unity-iPhone' do - pod 'AppsFlyerFramework', '6.17.9' + pod 'AppsFlyerFramework', '6.18.0' pod 'PurchaseConnector', '6.18.1' target 'Unity-iPhone Tests' do @@ -32,7 +32,7 @@ target 'Unity-iPhone' do end target 'UnityFramework' do - pod 'AppsFlyerFramework', '6.17.9' + pod 'AppsFlyerFramework', '6.18.0' pod 'PurchaseConnector', '6.18.1' end From ce8a11cc0a232048dd3ddc1e7f69e7c14db2c393 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 17:04:23 +0300 Subject: [PATCH 40/51] test: revert to macos-14/Xcode16, remove PC, restore __has_include guards Reverts to the state of the successful run 26033290223: - macos-14 runner (no Xcode 26 step) - AppsFlyerFramework 6.17.9 only in E2E Podfile (no PurchaseConnector) - __has_include guards in wrapper .h/.mm so PC absence compiles cleanly - Explicit AppsFlyerLib-Swift.h import for AppsFlyerConsent Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-ios.yml | 7 ++----- Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.h | 12 +++++++++--- Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.mm | 10 ++++++++++ scripts/ios-pod-install.sh | 6 ++---- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index 9b7e4891..96e47993 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -24,7 +24,7 @@ on: jobs: e2e-ios: name: E2E iOS — ${{ inputs.plugin_version }} - runs-on: macos-15 + runs-on: macos-14 timeout-minutes: 120 steps: @@ -33,9 +33,6 @@ jobs: with: ref: ${{ inputs.release_branch }} - - name: Select Xcode 26 - run: sudo xcode-select -s /Applications/Xcode_26.3.app - - name: Write .env from secret run: | printf '%s' "${{ secrets.ENV_FILE }}" > test-app/Assets/StreamingAssets/.env @@ -45,7 +42,7 @@ jobs: uses: actions/cache@v4 with: path: /Applications/Unity/Unity.app - key: unity-6000.3.5f1-ios-macos15-arm64 + key: unity-6000.3.5f1-ios-macos14-arm64 - name: Install Unity Editor + iOS module if: steps.cache-unity.outputs.cache-hit != 'true' diff --git a/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.h b/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.h index cf73afe5..2196ca92 100644 --- a/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.h +++ b/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.h @@ -15,24 +15,30 @@ #endif #if __has_include() #import -#else -#import "PurchaseConnector.h" #endif +#if __has_include() #import +#endif // Add StoreKit 2 support #if __has_include() #import #endif +#if __has_include() @interface AppsFlyeriOSWarpper : NSObject +#else +@interface AppsFlyeriOSWarpper : NSObject +#endif + (BOOL) didCallStart; + (void) setDidCallStart:(BOOL)val; -// Add StoreKit 2 methods +#if __has_include() +// StoreKit 2 methods - (void)setStoreKitVersion:(int)storeKitVersion; - (void)logConsumableTransaction:(id)transaction; +#endif @end diff --git a/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.mm b/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.mm index 7af1d225..672be257 100644 --- a/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.mm +++ b/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.mm @@ -23,6 +23,12 @@ #import "UnityFramework-Swift.h" #endif +#if __has_include() +#import +#elif __has_include("AppsFlyerLib-Swift.h") +#import "AppsFlyerLib-Swift.h" +#endif + static void unityCallBack(NSString* objectName, const char* method, const char* msg) { if(objectName){ UnitySendMessage([objectName UTF8String], method, msg); @@ -350,6 +356,7 @@ const void _disableIDFVCollection(bool isDisabled) { [AppsFlyerLib shared].disableIDFVCollection = isDisabled; } +#if __has_include() // Purchase connector const void _startObservingTransactions() { [[PurchaseConnector shared] startObservingTransactions]; @@ -441,6 +448,7 @@ void RegisterUnityPurchaseRevenueParamsCallbackSK2(UnityPurchaseCallback callbac #ifdef __cplusplus } #endif +#endif // __has_include PurchaseConnector } @implementation AppsFlyeriOSWarpper @@ -482,6 +490,7 @@ - (void)didResolveDeepLink:(AppsFlyerDeepLinkResult *)result{ unityCallBack(onDeeplinkingObjectName, ON_DEEPLINKING, stringFromdictionary(dict)); } +#if __has_include() // Purchase Connector - (void)didReceivePurchaseRevenueValidationInfo:(NSDictionary *)validationInfo error:(NSError *)error { if (error != nil) { @@ -595,6 +604,7 @@ - (NSDictionary *)purchaseRevenueAdditionalParametersStoreKit2ForProducts:(NSSet } return @{}; } +#endif // __has_include PurchaseConnector @end diff --git a/scripts/ios-pod-install.sh b/scripts/ios-pod-install.sh index 1571a006..fa2bb098 100755 --- a/scripts/ios-pod-install.sh +++ b/scripts/ios-pod-install.sh @@ -23,8 +23,7 @@ platform :ios, '15.0' use_frameworks! :linkage => :static target 'Unity-iPhone' do - pod 'AppsFlyerFramework', '6.18.0' - pod 'PurchaseConnector', '6.18.1' + pod 'AppsFlyerFramework', '6.17.9' target 'Unity-iPhone Tests' do inherit! :search_paths @@ -32,8 +31,7 @@ target 'Unity-iPhone' do end target 'UnityFramework' do - pod 'AppsFlyerFramework', '6.18.0' - pod 'PurchaseConnector', '6.18.1' + pod 'AppsFlyerFramework', '6.17.9' end post_install do |installer| From fd2f21cf0e7f9ba9144e51148d263f118fb45766 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 17:17:30 +0300 Subject: [PATCH 41/51] revert: restore exact state of successful run 26033290223 Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-ios.yml | 2 +- .../Plugins/iOS/AppsFlyeriOSWrapper.h | 9 +++--- .../Plugins/iOS/AppsFlyeriOSWrapper.mm | 30 ++++++++++++------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index 96e47993..aa1cef91 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -143,7 +143,7 @@ jobs: - name: Boot iOS simulator run: | UDID=$(xcrun simctl list devices available -j | \ - jq -r '.devices | to_entries[] | select(.key | contains("iOS-19")) | .value[] | select(.isAvailable == true) | .udid' | head -1) + jq -r '.devices | to_entries[] | select(.key | contains("iOS-18")) | .value[] | select(.isAvailable == true) | .udid' | head -1) if [[ -z "$UDID" ]]; then UDID=$(xcrun simctl list devices available -j | \ jq -r '.devices[][] | select(.isAvailable == true) | .udid' | head -1) diff --git a/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.h b/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.h index 2196ca92..6795d8c4 100644 --- a/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.h +++ b/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.h @@ -15,9 +15,9 @@ #endif #if __has_include() #import -#endif -#if __has_include() #import +#elif __has_include("PurchaseConnector.h") +#import "PurchaseConnector.h" #endif // Add StoreKit 2 support @@ -25,7 +25,7 @@ #import #endif -#if __has_include() +#if __has_include() || __has_include("PurchaseConnector.h") @interface AppsFlyeriOSWarpper : NSObject #else @interface AppsFlyeriOSWarpper : NSObject @@ -34,8 +34,7 @@ + (BOOL) didCallStart; + (void) setDidCallStart:(BOOL)val; -#if __has_include() -// StoreKit 2 methods +#if __has_include() || __has_include("PurchaseConnector.h") - (void)setStoreKitVersion:(int)storeKitVersion; - (void)logConsumableTransaction:(id)transaction; #endif diff --git a/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.mm b/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.mm index 672be257..57c8ffb1 100644 --- a/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.mm +++ b/Assets/AppsFlyer/Plugins/iOS/AppsFlyeriOSWrapper.mm @@ -11,6 +11,12 @@ #import #import "UnityFramework/UnityFramework-Swift.h" +#if __has_include() +#import +#elif __has_include("AppsFlyerLib-Swift.h") +#import "AppsFlyerLib-Swift.h" +#endif + #if __has_include() #import #elif __has_include("PurchaseConnector-Swift.h") @@ -23,12 +29,6 @@ #import "UnityFramework-Swift.h" #endif -#if __has_include() -#import -#elif __has_include("AppsFlyerLib-Swift.h") -#import "AppsFlyerLib-Swift.h" -#endif - static void unityCallBack(NSString* objectName, const char* method, const char* msg) { if(objectName){ UnitySendMessage([objectName UTF8String], method, msg); @@ -356,8 +356,8 @@ const void _disableIDFVCollection(bool isDisabled) { [AppsFlyerLib shared].disableIDFVCollection = isDisabled; } -#if __has_include() // Purchase connector +#if __has_include() || __has_include("PurchaseConnector-Swift.h") const void _startObservingTransactions() { [[PurchaseConnector shared] startObservingTransactions]; } @@ -424,6 +424,17 @@ const void _logConsumableTransaction(const char* transactionId) { }]; } } +#else + const void _startObservingTransactions() {} + const void _stopObservingTransactions() {} + const void _setIsSandbox(bool isSandBox) {} + const void _setPurchaseRevenueDelegate() {} + const void _setAutoLogPurchaseRevenue(int option) {} + const void _initPurchaseConnector(const char* objectName) {} + const void _setPurchaseRevenueDataSource(const char* objectName) {} + const void _setStoreKitVersion(int storeKitVersion) {} + const void _logConsumableTransaction(const char* transactionId) {} +#endif #ifdef __cplusplus extern "C" { @@ -448,7 +459,6 @@ void RegisterUnityPurchaseRevenueParamsCallbackSK2(UnityPurchaseCallback callbac #ifdef __cplusplus } #endif -#endif // __has_include PurchaseConnector } @implementation AppsFlyeriOSWarpper @@ -490,8 +500,8 @@ - (void)didResolveDeepLink:(AppsFlyerDeepLinkResult *)result{ unityCallBack(onDeeplinkingObjectName, ON_DEEPLINKING, stringFromdictionary(dict)); } -#if __has_include() // Purchase Connector +#if __has_include() || __has_include("PurchaseConnector-Swift.h") - (void)didReceivePurchaseRevenueValidationInfo:(NSDictionary *)validationInfo error:(NSError *)error { if (error != nil) { unityCallBack(onPurchaseValidationObjectName, PURCHASE_REVENUE_ERROR_CALLBACK, [[error localizedDescription] UTF8String]); @@ -604,7 +614,7 @@ - (NSDictionary *)purchaseRevenueAdditionalParametersStoreKit2ForProducts:(NSSet } return @{}; } -#endif // __has_include PurchaseConnector +#endif // PurchaseConnector @end From 6386eb36d44f9ef81c33f151898a257d629c9ccc Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 17:50:37 +0300 Subject: [PATCH 42/51] ci: complete RC pipeline aligned with appsflyer-mobile-plugin-tooling contract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - rc-release.yml: add RC-PUBLISH job (GitHub pre-release + Jira check + Slack notify) after the pre-publish gate; fires only when dry_run=false and both E2E gates pass - promote-release.yml: new RC-PROMOTE workflow triggered by 'pass QA ready for deploy' label on the release PR; strips -rcN from all version surfaces and pushes - .af-release/rc-plan.json: Unity plugin machine-readable RC plan mapping all RC-* stages to workflow files per the tooling contract RC-RELEASE is handled by the existing release_production_workflow.yml (PR merge → master). RC-SMOKE deferred (no published-artifact smoke app yet). Co-Authored-By: Claude Sonnet 4.6 --- .af-release/rc-plan.json | 44 +++++++++++++++++ .github/workflows/promote-release.yml | 69 +++++++++++++++++++++++++++ .github/workflows/rc-release.yml | 47 ++++++++++++++++++ 3 files changed, 160 insertions(+) create mode 100644 .af-release/rc-plan.json create mode 100644 .github/workflows/promote-release.yml diff --git a/.af-release/rc-plan.json b/.af-release/rc-plan.json new file mode 100644 index 00000000..75156a42 --- /dev/null +++ b/.af-release/rc-plan.json @@ -0,0 +1,44 @@ +{ + "_meta": { + "plugin": "unity", + "version": "1.0.0", + "description": "RC release configuration for the AppsFlyer Unity plugin. Maps the RC-* stages to GitHub Actions workflows.", + "tooling_contract_ref": "RC-PREP, RC-E2E, RC-PUBLISH, RC-PROMOTE, RC-RELEASE", + "schema_version": "1.0.0" + }, + + "branch_pattern": "^releases/\\d+\\.x\\.x/\\d+\\.\\d+\\.x/\\d+\\.\\d+\\.\\d+-rc\\d+$", + "version_regex": "^\\d+\\.\\d+\\.\\d+-rc\\d+$", + "rc_suffix": "-rc", + + "registry": { + "kind": "github-release", + "repo": "AppsFlyerSDK/appsflyer-unity-plugin" + }, + + "workflows": { + "RC-PREP": { "path": ".github/workflows/rc-release.yml", "job": "prepare-branch" }, + "RC-E2E": { "path": ".github/workflows/rc-release.yml", "job": "run-e2e-ios" }, + "RC-PUBLISH": { "path": ".github/workflows/rc-release.yml", "job": "rc-publish" }, + "RC-PROMOTE": { "path": ".github/workflows/promote-release.yml", "job": "promote" }, + "RC-RELEASE": { "path": ".github/workflows/release_production_workflow.yml" } + }, + + "gates": { + "RC-E2E": { + "check_run_name": "E2E — iOS", + "required_conclusion": "success" + }, + "RC-E2E-ANDROID": { + "check_run_name": "E2E — Android", + "required_conclusion": "success" + } + }, + + "plans": { + "e2e_plan": ".af-e2e/test-plan.json" + }, + + "promote_label": "pass QA ready for deploy", + "dry_run_default": true +} diff --git a/.github/workflows/promote-release.yml b/.github/workflows/promote-release.yml new file mode 100644 index 00000000..4ea8a988 --- /dev/null +++ b/.github/workflows/promote-release.yml @@ -0,0 +1,69 @@ +name: RC-PROMOTE — Strip -rcN and prepare for merge + +# Triggered when a maintainer applies the "pass QA ready for deploy" label +# to the release PR targeting master. +on: + pull_request: + types: [labeled] + branches: [master] + +jobs: + promote: + name: RC-PROMOTE + if: | + github.event.label.name == 'pass QA ready for deploy' && + startsWith(github.event.pull_request.head.ref, 'releases/') + runs-on: ubuntu-latest + + steps: + - name: Checkout release branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure git + run: | + git config user.name "${{ secrets.CI_USERNAME }}" + git config user.email "${{ secrets.CI_EMAIL }}" + + - name: Extract base version (strip -rcN) + id: version + run: | + BRANCH="${{ github.event.pull_request.head.ref }}" + # Branch pattern: releases/6.x.x/18.x/6.18.0-rc1 + BASE=$(echo "$BRANCH" | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+(-rc[0-9]+)?' | tail -1 | sed 's/-rc[0-9]*$//') + echo "base_version=$BASE" >> "$GITHUB_OUTPUT" + echo "Promoting to: $BASE" + + - name: Strip -rcN from all version surfaces + run: | + BASE="${{ steps.version.outputs.base_version }}" + for f in \ + Assets/AppsFlyer/package.json \ + Assets/AppsFlyer/AppsFlyer.cs \ + deploy/build_unity_package.sh \ + deploy/strict_mode_build_package.sh; do + [[ -f "$f" ]] && sed -i "s/-rc[0-9]*//g" "$f" && echo "Stripped: $f" + done + + - name: Commit and push + run: | + BASE="${{ steps.version.outputs.base_version }}" + git add \ + Assets/AppsFlyer/package.json \ + Assets/AppsFlyer/AppsFlyer.cs \ + deploy/build_unity_package.sh \ + deploy/strict_mode_build_package.sh || true + git diff --cached --quiet && echo "Nothing to commit" && exit 0 + git commit -m "chore: promote to $BASE — strip -rcN" + git push origin "${{ github.event.pull_request.head.ref }}" + + - name: Comment on PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BASE="${{ steps.version.outputs.base_version }}" + gh pr comment "${{ github.event.pull_request.number }}" --body \ + "✅ **RC-PROMOTE complete** — version surfaces updated to \`$BASE\`. Branch is ready to merge." diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml index a51354f5..c6074830 100644 --- a/.github/workflows/rc-release.yml +++ b/.github/workflows/rc-release.yml @@ -260,3 +260,50 @@ jobs: fi echo "All checks passed. RC ${{ github.event.inputs.plugin_version }} is ready for manual publish." + + # ── 7. RC-PUBLISH — GitHub pre-release + Jira/Slack notify ───────────────── + rc-publish: + name: RC-PUBLISH + needs: [prepare-branch, pre-publish-gate] + if: needs.pre-publish-gate.result == 'success' + runs-on: ubuntu-latest + steps: + - name: Checkout release branch + uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare-branch.outputs.release_branch }} + + - name: Check Jira fixed version + env: + JIRA_TOKEN: ${{ secrets.CI_JIRA_TOKEN }} + run: | + BASE=$(echo "${{ github.event.inputs.plugin_version }}" | sed 's/-rc[0-9]*$//') + chmod +x .github/workflows/Scripts/checkJira.sh + .github/workflows/Scripts/checkJira.sh "$JIRA_TOKEN" "Unity SDK v$BASE" || true + + - name: Create GitHub pre-release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ github.event.inputs.plugin_version }}" + BRANCH="${{ needs.prepare-branch.outputs.release_branch }}" + gh release create "$VERSION" \ + --prerelease \ + --target "$BRANCH" \ + --title "v$VERSION (RC)" \ + --notes "$(printf '## RC %s\n\n| Field | Value |\n|---|---|\n| Plugin | `%s` |\n| iOS SDK | `%s` |\n| Android SDK | `%s` |\n| Branch | `%s` |\n\n### Pipeline\n- [x] RC-PREP\n- [x] RC-E2E (iOS + Android)\n- [x] RC-PUBLISH\n- [ ] RC-PROMOTE — apply `pass QA ready for deploy` label to the PR\n' \ + "$VERSION" "$VERSION" \ + "${{ github.event.inputs.ios_sdk_version }}" \ + "${{ github.event.inputs.android_sdk_version }}" \ + "$BRANCH")" + + - name: Notify Slack — RC published + env: + SLACK_TOKEN: ${{ secrets.CI_SLACK_TOKEN }} + run: | + VERSION="${{ github.event.inputs.plugin_version }}" + IOS_SDK="${{ github.event.inputs.ios_sdk_version }}" + AND_SDK="${{ github.event.inputs.android_sdk_version }}" + curl -X POST -H 'Content-type: application/json' --data \ + '{"plugin_version":"'"$VERSION"'","git_branch":"'"${{ needs.prepare-branch.outputs.release_branch }}"'","ios_sdk_dependency":"iOS AppsFlyer SDK v'"$IOS_SDK"'","android_sdk_dependency":"Android AppsFlyer SDK v'"$AND_SDK"'","version_changes":"RC PUBLISHED: Unity Plugin '"$VERSION"'","deploy_type":"RC"}' \ + "$SLACK_TOKEN" From 699796b8cd21acf0c144178e44cc0be12f9b8e9c Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 17:56:11 +0300 Subject: [PATCH 43/51] =?UTF-8?q?ci:=20simplify=20rc-release.yml=20?= =?UTF-8?q?=E2=80=94=20inputs,=20branch=20prep,=20iOS+Android=20E2E=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strips the workflow down to the three core steps: 1. Take plugin/android/ios version inputs 2. Cut release branch and apply version bumps 3. Run iOS E2E then Android E2E on that branch Publish, promote, and release stages come later as separate workflows. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-release.yml | 225 +++---------------------------- 1 file changed, 18 insertions(+), 207 deletions(-) diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml index c6074830..4093b8e2 100644 --- a/.github/workflows/rc-release.yml +++ b/.github/workflows/rc-release.yml @@ -4,75 +4,45 @@ on: workflow_dispatch: inputs: plugin_version: - description: "RC plugin version — must match ^\\d+\\.\\d+\\.\\d+-rc\\d+$ (e.g. 6.18.0-rc1)" + description: "Plugin version — e.g. 6.18.0-rc1" required: true type: string android_sdk_version: - description: "Android SDK version to pin (e.g. 6.18.0)" + description: "Android SDK version — e.g. 6.18.0" required: true type: string ios_sdk_version: - description: "iOS SDK version to pin (e.g. 6.18.0)" + description: "iOS SDK version — e.g. 6.18.0" required: true type: string base_branch: - description: "Branch to cut the release from" + description: "Branch to cut the release from (default: development)" required: false default: development type: string - skip_tests: - description: "Skip unit test job (emergency / hotfix use only)" - required: false - default: "false" - type: string - skip_e2e: - description: "Skip E2E jobs (emergency / hotfix use only)" - required: false - default: "false" - type: string - dry_run: - description: "Dry run — validate inputs and print plan without pushing or opening a PR" - required: false - default: "true" - type: string jobs: - # ── 1. Validate inputs ────────────────────────────────────────────────────── - validate-release: - name: Validate inputs + # ── 1. Prepare release branch ─────────────────────────────────────────────── + prepare-branch: + name: Prepare release branch runs-on: ubuntu-latest outputs: - base_version: ${{ steps.parse.outputs.base_version }} + release_branch: ${{ steps.branch.outputs.release_branch }} + steps: - - name: Check plugin_version format + - name: Validate plugin_version format run: | if ! echo "${{ github.event.inputs.plugin_version }}" | grep -qE '^\d+\.\d+\.\d+-rc\d+$'; then - echo "::error::plugin_version '${{ github.event.inputs.plugin_version }}' does not match ^\d+\.\d+\.\d+-rc\d+$" + echo "::error::plugin_version must match X.Y.Z-rcN (e.g. 6.18.0-rc1)" exit 1 fi - echo "plugin_version OK: ${{ github.event.inputs.plugin_version }}" - - - name: Parse base_version (strip -rcN suffix) - id: parse - run: | - BASE=$(echo "${{ github.event.inputs.plugin_version }}" | sed 's/-rc[0-9]*$//') - echo "base_version=$BASE" >> "$GITHUB_OUTPUT" - echo "base_version=$BASE" - # ── 2. Prepare release branch ─────────────────────────────────────────────── - prepare-branch: - name: Prepare release branch - needs: validate-release - runs-on: ubuntu-latest - outputs: - release_branch: ${{ steps.branch.outputs.release_branch }} - steps: - name: Checkout base branch uses: actions/checkout@v4 with: - ref: ${{ github.event.inputs.base_branch }} + ref: ${{ github.event.inputs.base_branch }} fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} - name: Configure git run: | @@ -96,7 +66,7 @@ jobs: echo "Branch exists — resetting to ${{ github.event.inputs.base_branch }}" git checkout -B "$BRANCH" "origin/${{ github.event.inputs.base_branch }}" else - echo "Creating new branch: $BRANCH" + echo "Creating branch: $BRANCH" git checkout -b "$BRANCH" fi @@ -108,7 +78,7 @@ jobs: --android-sdk-version "${{ github.event.inputs.android_sdk_version }}" \ --ios-sdk-version "${{ github.event.inputs.ios_sdk_version }}" - - name: Commit version bump + - name: Commit and push run: | git add \ Assets/AppsFlyer/package.json \ @@ -119,87 +89,12 @@ jobs: CHANGELOG.md \ README.md git commit -m "chore: bump to ${{ github.event.inputs.plugin_version }}" || echo "Nothing to commit" - - - name: Push release branch - if: github.event.inputs.dry_run == 'false' - run: | git push origin "${{ steps.branch.outputs.release_branch }}" --force - - name: Write PR body to file - if: github.event.inputs.dry_run == 'false' - run: | - VERSION="${{ github.event.inputs.plugin_version }}" - BASE_VERSION="${{ needs.validate-release.outputs.base_version }}" - { - printf '## Release %s\n\n' "$VERSION" - printf 'Field | Value\n' - printf '--- | ---\n' - printf 'Plugin version | `%s`\n' "$VERSION" - printf 'Base version | `%s`\n' "$BASE_VERSION" - printf 'Android SDK | `%s`\n' "${{ github.event.inputs.android_sdk_version }}" - printf 'iOS SDK | `%s`\n' "${{ github.event.inputs.ios_sdk_version }}" - printf 'Base branch | `%s`\n\n' "${{ github.event.inputs.base_branch }}" - printf '### RC Pipeline status\n' - printf -- '- [x] RC-PREP (this PR)\n' - printf -- '- [ ] RC-E2E (triggered automatically)\n' - } > /tmp/pr_body.md - - - name: Open / update PR to master - if: github.event.inputs.dry_run == 'false' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - BRANCH="${{ steps.branch.outputs.release_branch }}" - VERSION="${{ github.event.inputs.plugin_version }}" - - PR_URL=$(gh pr list \ - --head "$BRANCH" \ - --base master \ - --state open \ - --json url \ - --jq '.[0].url' 2>/dev/null || true) - - if [[ -n "$PR_URL" ]]; then - echo "PR already exists: $PR_URL" - gh pr edit "$PR_URL" \ - --title "Release $VERSION" \ - --body-file /tmp/pr_body.md - else - gh pr create \ - --head "$BRANCH" \ - --base master \ - --title "Release $VERSION" \ - --body-file /tmp/pr_body.md - fi - - - name: Dry run summary - if: github.event.inputs.dry_run == 'true' - run: | - echo "=== DRY RUN — nothing was pushed ===" - echo "release_branch : ${{ steps.branch.outputs.release_branch }}" - echo "plugin_version : ${{ github.event.inputs.plugin_version }}" - echo "android_sdk : ${{ github.event.inputs.android_sdk_version }}" - echo "ios_sdk : ${{ github.event.inputs.ios_sdk_version }}" - git diff HEAD~1 HEAD --stat || git status - - # ── 3. Unit tests (parallel with prepare-branch) ──────────────────────────── - # Runs the existing PlayMode test suite (iOS, Android, Shared matrix) against - # the plugin source on the dispatched-from branch. Branch prep uses no Unity - # seat, so these two jobs run concurrently without license contention. - # E2E waits for BOTH prepare-branch and run-tests before starting, ensuring - # the seat is free when E2E kicks off. - run-tests: - name: Unit tests - needs: validate-release - if: github.event.inputs.skip_tests == 'false' - uses: ./.github/workflows/main.yml - secrets: inherit - - # ── 4. E2E — iOS ──────────────────────────────────────────────────────────── + # ── 2. E2E — iOS ──────────────────────────────────────────────────────────── run-e2e-ios: name: E2E — iOS - needs: [prepare-branch, run-tests] - if: github.event.inputs.skip_e2e == 'false' && github.event.inputs.dry_run == 'false' + needs: prepare-branch uses: ./.github/workflows/rc-e2e-ios.yml with: plugin_version: ${{ github.event.inputs.plugin_version }} @@ -210,11 +105,10 @@ jobs: UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} ENV_FILE: ${{ secrets.ENV_FILE }} - # ── 5. E2E — Android (after iOS to avoid Unity license contention) ────────── + # ── 3. E2E — Android (after iOS to avoid Unity license contention) ────────── run-e2e-android: name: E2E — Android needs: [prepare-branch, run-e2e-ios] - if: github.event.inputs.skip_e2e == 'false' && github.event.inputs.dry_run == 'false' uses: ./.github/workflows/rc-e2e-android.yml with: plugin_version: ${{ github.event.inputs.plugin_version }} @@ -224,86 +118,3 @@ jobs: UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} ENV_FILE: ${{ secrets.ENV_FILE }} - - # ── 6. Pre-publish gate ────────────────────────────────────────────────────── - pre-publish-gate: - name: Pre-publish gate - needs: [prepare-branch, run-tests, run-e2e-ios, run-e2e-android] - if: always() && github.event.inputs.dry_run == 'false' - runs-on: ubuntu-latest - steps: - - name: Check unit test and E2E results - run: | - TEST_RESULT="${{ needs.run-tests.result }}" - IOS_RESULT="${{ needs.run-e2e-ios.result }}" - AND_RESULT="${{ needs.run-e2e-android.result }}" - SKIP_TESTS="${{ github.event.inputs.skip_tests }}" - SKIP_E2E="${{ github.event.inputs.skip_e2e }}" - - if [[ "$SKIP_TESTS" != "true" && "$TEST_RESULT" != "success" ]]; then - echo "::error::Unit tests result: $TEST_RESULT" - exit 1 - fi - - if [[ "$SKIP_E2E" == "true" ]]; then - echo "E2E skipped by operator — gate passes automatically" - exit 0 - fi - - if [[ "$IOS_RESULT" != "success" ]]; then - echo "::error::iOS E2E result: $IOS_RESULT" - exit 1 - fi - if [[ "$AND_RESULT" != "success" ]]; then - echo "::error::Android E2E result: $AND_RESULT" - exit 1 - fi - - echo "All checks passed. RC ${{ github.event.inputs.plugin_version }} is ready for manual publish." - - # ── 7. RC-PUBLISH — GitHub pre-release + Jira/Slack notify ───────────────── - rc-publish: - name: RC-PUBLISH - needs: [prepare-branch, pre-publish-gate] - if: needs.pre-publish-gate.result == 'success' - runs-on: ubuntu-latest - steps: - - name: Checkout release branch - uses: actions/checkout@v4 - with: - ref: ${{ needs.prepare-branch.outputs.release_branch }} - - - name: Check Jira fixed version - env: - JIRA_TOKEN: ${{ secrets.CI_JIRA_TOKEN }} - run: | - BASE=$(echo "${{ github.event.inputs.plugin_version }}" | sed 's/-rc[0-9]*$//') - chmod +x .github/workflows/Scripts/checkJira.sh - .github/workflows/Scripts/checkJira.sh "$JIRA_TOKEN" "Unity SDK v$BASE" || true - - - name: Create GitHub pre-release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - VERSION="${{ github.event.inputs.plugin_version }}" - BRANCH="${{ needs.prepare-branch.outputs.release_branch }}" - gh release create "$VERSION" \ - --prerelease \ - --target "$BRANCH" \ - --title "v$VERSION (RC)" \ - --notes "$(printf '## RC %s\n\n| Field | Value |\n|---|---|\n| Plugin | `%s` |\n| iOS SDK | `%s` |\n| Android SDK | `%s` |\n| Branch | `%s` |\n\n### Pipeline\n- [x] RC-PREP\n- [x] RC-E2E (iOS + Android)\n- [x] RC-PUBLISH\n- [ ] RC-PROMOTE — apply `pass QA ready for deploy` label to the PR\n' \ - "$VERSION" "$VERSION" \ - "${{ github.event.inputs.ios_sdk_version }}" \ - "${{ github.event.inputs.android_sdk_version }}" \ - "$BRANCH")" - - - name: Notify Slack — RC published - env: - SLACK_TOKEN: ${{ secrets.CI_SLACK_TOKEN }} - run: | - VERSION="${{ github.event.inputs.plugin_version }}" - IOS_SDK="${{ github.event.inputs.ios_sdk_version }}" - AND_SDK="${{ github.event.inputs.android_sdk_version }}" - curl -X POST -H 'Content-type: application/json' --data \ - '{"plugin_version":"'"$VERSION"'","git_branch":"'"${{ needs.prepare-branch.outputs.release_branch }}"'","ios_sdk_dependency":"iOS AppsFlyer SDK v'"$IOS_SDK"'","android_sdk_dependency":"Android AppsFlyer SDK v'"$AND_SDK"'","version_changes":"RC PUBLISHED: Unity Plugin '"$VERSION"'","deploy_type":"RC"}' \ - "$SLACK_TOKEN" From 70d2f1f6e12f83cfe54247f37aad7cda7cc8e952 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 20:07:11 +0300 Subject: [PATCH 44/51] test: add UNITY_SERIAL to return license step Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-android.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/rc-e2e-android.yml b/.github/workflows/rc-e2e-android.yml index 98893aad..e01869a8 100644 --- a/.github/workflows/rc-e2e-android.yml +++ b/.github/workflows/rc-e2e-android.yml @@ -105,3 +105,5 @@ jobs: - name: Return Unity license if: always() uses: game-ci/unity-return-license@v2 + env: + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} From 551b51150ed2d334ae07b5376ff4945e5512f1d1 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 20:12:23 +0300 Subject: [PATCH 45/51] refactor: replace game-ci Docker with direct Unity 6000.3.5f1 install on Ubuntu Removes legacy Unity 2019 licensing (game-ci) which uses a deprecated license server that times out on return. Mirrors the iOS approach: direct install, entitlement-based activate/return. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-android.yml | 80 +++++++++++++++++++--------- 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/.github/workflows/rc-e2e-android.yml b/.github/workflows/rc-e2e-android.yml index e01869a8..709ad049 100644 --- a/.github/workflows/rc-e2e-android.yml +++ b/.github/workflows/rc-e2e-android.yml @@ -37,28 +37,62 @@ jobs: run: | printf '%s' "${{ secrets.ENV_FILE }}" > test-app/Assets/StreamingAssets/.env + - name: Cache Unity Editor installation + id: cache-unity + uses: actions/cache@v4 + with: + path: /opt/unity + key: unity-6000.3.5f1-android-ubuntu + + - name: Install Unity Editor + Android module + if: steps.cache-unity.outputs.cache-hit != 'true' + run: | + curl -fL "https://download.unity3d.com/download_unity/a1ec4b2f2d19/LinuxEditorInstaller/Unity-6000.3.5f1.tar.xz" \ + -o /tmp/Unity.tar.xz + sudo mkdir -p /opt/unity + sudo tar -xf /tmp/Unity.tar.xz -C /opt/unity + curl -fL "https://download.unity3d.com/download_unity/a1ec4b2f2d19/LinuxEditorTargetInstaller/UnitySetup-Android-Support-for-Editor-6000.3.5f1.tar.xz" \ + -o /tmp/Unity-Android.tar.xz + sudo tar -xf /tmp/Unity-Android.tar.xz -C /opt/unity + UNITY=$(find /opt/unity -name "Unity" -type f -path "*/Editor/Unity" 2>/dev/null | head -1) + echo "Unity installed at: $UNITY" + echo "UNITY_PATH=$UNITY" >> "$GITHUB_ENV" + timeout-minutes: 60 + - name: Activate Unity license - uses: game-ci/unity-activate@v2 - env: - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} - UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} + run: | + UNITY="${UNITY_PATH:-/opt/unity/Editor/Unity}" + "$UNITY" -quit -batchmode \ + -serial "${{ secrets.UNITY_SERIAL }}" \ + -username "${{ secrets.UNITY_EMAIL }}" \ + -password "${{ secrets.UNITY_PASSWORD }}" \ + -logFile /tmp/unity-activate.log 2>&1 || true + grep -E "LICENSE|license|error|Error" /tmp/unity-activate.log || true - - name: Build Android APK (via Unity) - uses: game-ci/unity-builder@v4 - env: - UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} - UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} - UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} - with: - projectPath: test-app - unityVersion: 6000.3.5f1 - targetPlatform: Android - buildName: com.appsflyer.engagement - buildsPath: test-app/Build - buildMethod: BuildScript.BuildAndroid - androidExportType: androidPackage - allowDirtyBuild: true + - name: Build Android APK + run: | + UNITY="${UNITY_PATH:-/opt/unity/Editor/Unity}" + mkdir -p test-app/Build/Android + "$UNITY" -quit -batchmode \ + -logFile /tmp/unity-build.log \ + -projectPath "$(pwd)/test-app" \ + -buildTarget Android \ + -executeMethod BuildScript.BuildAndroid + BUILD_EXIT=$? + tail -50 /tmp/unity-build.log + exit $BUILD_EXIT + timeout-minutes: 30 + + - name: Return Unity license + if: always() + run: | + UNITY="${UNITY_PATH:-/opt/unity/Editor/Unity}" + "$UNITY" -quit -batchmode -returnlicense \ + -username "${{ secrets.UNITY_EMAIL }}" \ + -password "${{ secrets.UNITY_PASSWORD }}" \ + -logFile /tmp/unity-return.log 2>&1 || true + echo "=== unity-return.log ===" + cat /tmp/unity-return.log || true - name: Locate APK run: | @@ -101,9 +135,3 @@ jobs: name: e2e-android-report-${{ inputs.plugin_version }} path: .af-e2e/reports/ retention-days: 30 - - - name: Return Unity license - if: always() - uses: game-ci/unity-return-license@v2 - env: - UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} From 6a5f8638fd6c4717c29316326fee9db25c72c7bb Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 21:06:30 +0300 Subject: [PATCH 46/51] fix: remove non-existent Linux Android module download Android support is bundled in the base Unity Linux installer. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-android.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/rc-e2e-android.yml b/.github/workflows/rc-e2e-android.yml index 709ad049..7bc86236 100644 --- a/.github/workflows/rc-e2e-android.yml +++ b/.github/workflows/rc-e2e-android.yml @@ -51,9 +51,6 @@ jobs: -o /tmp/Unity.tar.xz sudo mkdir -p /opt/unity sudo tar -xf /tmp/Unity.tar.xz -C /opt/unity - curl -fL "https://download.unity3d.com/download_unity/a1ec4b2f2d19/LinuxEditorTargetInstaller/UnitySetup-Android-Support-for-Editor-6000.3.5f1.tar.xz" \ - -o /tmp/Unity-Android.tar.xz - sudo tar -xf /tmp/Unity-Android.tar.xz -C /opt/unity UNITY=$(find /opt/unity -name "Unity" -type f -path "*/Editor/Unity" 2>/dev/null | head -1) echo "Unity installed at: $UNITY" echo "UNITY_PATH=$UNITY" >> "$GITHUB_ENV" From 6961e32f3e95dd3445e46437cda2f965ff8c585f Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 21:24:04 +0300 Subject: [PATCH 47/51] test: remove unity-activate and unity-return-license, let builder handle it Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-android.yml | 66 ++++++---------------------- 1 file changed, 14 insertions(+), 52 deletions(-) diff --git a/.github/workflows/rc-e2e-android.yml b/.github/workflows/rc-e2e-android.yml index 7bc86236..4e1bca5b 100644 --- a/.github/workflows/rc-e2e-android.yml +++ b/.github/workflows/rc-e2e-android.yml @@ -37,59 +37,21 @@ jobs: run: | printf '%s' "${{ secrets.ENV_FILE }}" > test-app/Assets/StreamingAssets/.env - - name: Cache Unity Editor installation - id: cache-unity - uses: actions/cache@v4 + - name: Build Android APK (via Unity) + uses: game-ci/unity-builder@v4 + env: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} with: - path: /opt/unity - key: unity-6000.3.5f1-android-ubuntu - - - name: Install Unity Editor + Android module - if: steps.cache-unity.outputs.cache-hit != 'true' - run: | - curl -fL "https://download.unity3d.com/download_unity/a1ec4b2f2d19/LinuxEditorInstaller/Unity-6000.3.5f1.tar.xz" \ - -o /tmp/Unity.tar.xz - sudo mkdir -p /opt/unity - sudo tar -xf /tmp/Unity.tar.xz -C /opt/unity - UNITY=$(find /opt/unity -name "Unity" -type f -path "*/Editor/Unity" 2>/dev/null | head -1) - echo "Unity installed at: $UNITY" - echo "UNITY_PATH=$UNITY" >> "$GITHUB_ENV" - timeout-minutes: 60 - - - name: Activate Unity license - run: | - UNITY="${UNITY_PATH:-/opt/unity/Editor/Unity}" - "$UNITY" -quit -batchmode \ - -serial "${{ secrets.UNITY_SERIAL }}" \ - -username "${{ secrets.UNITY_EMAIL }}" \ - -password "${{ secrets.UNITY_PASSWORD }}" \ - -logFile /tmp/unity-activate.log 2>&1 || true - grep -E "LICENSE|license|error|Error" /tmp/unity-activate.log || true - - - name: Build Android APK - run: | - UNITY="${UNITY_PATH:-/opt/unity/Editor/Unity}" - mkdir -p test-app/Build/Android - "$UNITY" -quit -batchmode \ - -logFile /tmp/unity-build.log \ - -projectPath "$(pwd)/test-app" \ - -buildTarget Android \ - -executeMethod BuildScript.BuildAndroid - BUILD_EXIT=$? - tail -50 /tmp/unity-build.log - exit $BUILD_EXIT - timeout-minutes: 30 - - - name: Return Unity license - if: always() - run: | - UNITY="${UNITY_PATH:-/opt/unity/Editor/Unity}" - "$UNITY" -quit -batchmode -returnlicense \ - -username "${{ secrets.UNITY_EMAIL }}" \ - -password "${{ secrets.UNITY_PASSWORD }}" \ - -logFile /tmp/unity-return.log 2>&1 || true - echo "=== unity-return.log ===" - cat /tmp/unity-return.log || true + projectPath: test-app + unityVersion: 6000.3.5f1 + targetPlatform: Android + buildName: com.appsflyer.engagement + buildsPath: test-app/Build + buildMethod: BuildScript.BuildAndroid + androidExportType: androidPackage + allowDirtyBuild: true - name: Locate APK run: | From 0261ceb01055a3dd72d9cead219dbc8fa6e93c20 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Mon, 18 May 2026 22:16:04 +0300 Subject: [PATCH 48/51] fix: return Unity license via Docker after builder completes game-ci/unity-builder does not return the license on exit. Run returnlicense explicitly in the same Docker image. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-android.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/.github/workflows/rc-e2e-android.yml b/.github/workflows/rc-e2e-android.yml index 4e1bca5b..597b7b9b 100644 --- a/.github/workflows/rc-e2e-android.yml +++ b/.github/workflows/rc-e2e-android.yml @@ -53,6 +53,28 @@ jobs: androidExportType: androidPackage allowDirtyBuild: true + - name: Return Unity license + if: always() + run: | + docker run --rm \ + --env UNITY_EMAIL \ + --env UNITY_PASSWORD \ + --env UNITY_SERIAL \ + --env HOME=/root \ + unityci/editor:ubuntu-6000.3.5f1-android-3 \ + bash -c " + unity-editor -quit -batchmode -returnlicense \ + -username \"\$UNITY_EMAIL\" \ + -password \"\$UNITY_PASSWORD\" \ + -logFile /tmp/unity-return.log 2>&1 || true + echo '=== unity-return.log ===' + cat /tmp/unity-return.log || true + " || true + env: + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} + - name: Locate APK run: | APK=$(find test-app/Build/Android -name "*.apk" | head -1) From ecc9c9d2c39dae543b8090ba183e58e3a8d6042b Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Tue, 19 May 2026 10:47:49 +0300 Subject: [PATCH 49/51] fix: bump-version.sh must also patch test-app gradle and ios-pod-install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit mainTemplate.gradle and ios-pod-install.sh had native SDK versions hardcoded — the bump script only updated AppsFlyerDependencies.xml which is not what the E2E builds actually use. E2E was running against the old SDK versions regardless of what was passed to the pipeline. Co-Authored-By: Claude Sonnet 4.6 --- scripts/bump-version.sh | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh index f876770a..3bfe5ea0 100755 --- a/scripts/bump-version.sh +++ b/scripts/bump-version.sh @@ -78,6 +78,22 @@ echo "[8/8] $STRICT_SH" sed -i.bak "s|PACKAGE_NAME=\"appsflyer-unity-plugin-strict-mode-[^\"]*\.unitypackage\"|PACKAGE_NAME=\"appsflyer-unity-plugin-strict-mode-${PLUGIN_VERSION}.unitypackage\"|" "$STRICT_SH" rm -f "${STRICT_SH}.bak" +# ── 9. test-app/Assets/Plugins/Android/mainTemplate.gradle ─────────────────── +MAIN_GRADLE="test-app/Assets/Plugins/Android/mainTemplate.gradle" +if [[ -f "$MAIN_GRADLE" ]]; then + echo "[9/10] $MAIN_GRADLE — af-android-sdk" + sed -i.bak "s|com.appsflyer:af-android-sdk:[^']*|com.appsflyer:af-android-sdk:$ANDROID_SDK_VERSION|" "$MAIN_GRADLE" + rm -f "${MAIN_GRADLE}.bak" +fi + +# ── 10. scripts/ios-pod-install.sh ─────────────────────────────────────────── +IOS_POD_SH="scripts/ios-pod-install.sh" +if [[ -f "$IOS_POD_SH" ]]; then + echo "[10/10] $IOS_POD_SH — AppsFlyerFramework" + sed -i.bak "s|pod 'AppsFlyerFramework', '[^']*'|pod 'AppsFlyerFramework', '$IOS_SDK_VERSION'|g" "$IOS_POD_SH" + rm -f "${IOS_POD_SH}.bak" +fi + # ── CHANGELOG.md — prepend new version header if not already present ────────── CHANGELOG="CHANGELOG.md" if [[ -f "$CHANGELOG" ]]; then From 2352cc1c3f9099cd5bf723061ea3c0e8e52a8489 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Tue, 19 May 2026 10:57:31 +0300 Subject: [PATCH 50/51] ci: add version bump verification step to rc-release pipeline Fails fast before Unity builds if bump-version.sh did not correctly update mainTemplate.gradle, ios-pod-install.sh, or AppsFlyer.cs. Also stages the two newly-bumped files in the release commit. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-release.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml index 4093b8e2..b130e181 100644 --- a/.github/workflows/rc-release.yml +++ b/.github/workflows/rc-release.yml @@ -78,6 +78,23 @@ jobs: --android-sdk-version "${{ github.event.inputs.android_sdk_version }}" \ --ios-sdk-version "${{ github.event.inputs.ios_sdk_version }}" + - name: Verify version bump + run: | + ANDROID_SDK="${{ github.event.inputs.android_sdk_version }}" + IOS_SDK="${{ github.event.inputs.ios_sdk_version }}" + PLUGIN="${{ github.event.inputs.plugin_version }}" + + grep -q "af-android-sdk:$ANDROID_SDK" test-app/Assets/Plugins/Android/mainTemplate.gradle \ + || { echo "::error::mainTemplate.gradle does not contain af-android-sdk:$ANDROID_SDK"; exit 1; } + + grep -q "AppsFlyerFramework', '$IOS_SDK'" scripts/ios-pod-install.sh \ + || { echo "::error::ios-pod-install.sh does not contain AppsFlyerFramework $IOS_SDK"; exit 1; } + + grep -q "kAppsFlyerPluginVersion = \"$PLUGIN\"" Assets/AppsFlyer/AppsFlyer.cs \ + || { echo "::error::AppsFlyer.cs does not contain plugin version $PLUGIN"; exit 1; } + + echo "Version bump verified: plugin=$PLUGIN android-sdk=$ANDROID_SDK ios-sdk=$IOS_SDK" + - name: Commit and push run: | git add \ @@ -86,6 +103,8 @@ jobs: Assets/AppsFlyer/Editor/AppsFlyerDependencies.xml \ deploy/build_unity_package.sh \ deploy/strict_mode_build_package.sh \ + test-app/Assets/Plugins/Android/mainTemplate.gradle \ + scripts/ios-pod-install.sh \ CHANGELOG.md \ README.md git commit -m "chore: bump to ${{ github.event.inputs.plugin_version }}" || echo "Nothing to commit" From 2e3a793e6fb0a56fa581af070a51eaec14b509b8 Mon Sep 17 00:00:00 2001 From: "kobi.kagan" Date: Tue, 19 May 2026 11:02:35 +0300 Subject: [PATCH 51/51] ci: align workflow files with master fixes - rc-e2e-ios.yml: bring in retry logic + credentials on license return - rc-release.yml: fix \d regex to [0-9]+ for Linux grep compatibility Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/rc-e2e-ios.yml | 33 +++++++++++++++++++++++++++----- .github/workflows/rc-release.yml | 2 +- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/.github/workflows/rc-e2e-ios.yml b/.github/workflows/rc-e2e-ios.yml index aa1cef91..0fe67865 100644 --- a/.github/workflows/rc-e2e-ios.yml +++ b/.github/workflows/rc-e2e-ios.yml @@ -61,11 +61,30 @@ jobs: - name: Activate Unity license run: | UNITY="${UNITY_PATH:-/Applications/Unity/Unity.app/Contents/MacOS/Unity}" - "$UNITY" -quit -batchmode \ - -serial "${{ secrets.UNITY_SERIAL }}" \ - -username "${{ secrets.UNITY_EMAIL }}" \ - -password "${{ secrets.UNITY_PASSWORD }}" \ - -logFile /tmp/unity-activate.log 2>&1 || true + + activate() { + rm -f /tmp/unity-activate.log + "$UNITY" -quit -batchmode \ + -serial "${{ secrets.UNITY_SERIAL }}" \ + -username "${{ secrets.UNITY_EMAIL }}" \ + -password "${{ secrets.UNITY_PASSWORD }}" \ + -logFile /tmp/unity-activate.log 2>&1 || true + } + + activate + + if grep -q "serial has reached the maximum number of activations" /tmp/unity-activate.log 2>/dev/null; then + echo "::warning::Serial at max activations — returning existing license and retrying in 60s..." + "$UNITY" -quit -batchmode -returnlicense \ + -username "${{ secrets.UNITY_EMAIL }}" \ + -password "${{ secrets.UNITY_PASSWORD }}" \ + -logFile /tmp/unity-return-preflight.log 2>&1 || true + echo "=== preflight return log ===" + cat /tmp/unity-return-preflight.log || true + sleep 60 + activate + fi + grep -E "LICENSE|license|error|Error" /tmp/unity-activate.log || true - name: Build iOS Simulator Xcode project @@ -102,7 +121,11 @@ jobs: run: | UNITY="${UNITY_PATH:-/Applications/Unity/Unity.app/Contents/MacOS/Unity}" "$UNITY" -quit -batchmode -returnlicense \ + -username "${{ secrets.UNITY_EMAIL }}" \ + -password "${{ secrets.UNITY_PASSWORD }}" \ -logFile /tmp/unity-return.log 2>&1 || true + echo "=== unity-return.log ===" + cat /tmp/unity-return.log || true - name: Install iOS pods run: | diff --git a/.github/workflows/rc-release.yml b/.github/workflows/rc-release.yml index b130e181..fadf171d 100644 --- a/.github/workflows/rc-release.yml +++ b/.github/workflows/rc-release.yml @@ -32,7 +32,7 @@ jobs: steps: - name: Validate plugin_version format run: | - if ! echo "${{ github.event.inputs.plugin_version }}" | grep -qE '^\d+\.\d+\.\d+-rc\d+$'; then + if ! echo "${{ github.event.inputs.plugin_version }}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+-rc[0-9]+$'; then echo "::error::plugin_version must match X.Y.Z-rcN (e.g. 6.18.0-rc1)" exit 1 fi