From 1008d4b14c9e7d8e064492fe7d027df1ab1cea03 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 18 May 2026 12:15:04 -0700 Subject: [PATCH 1/4] ci: add E2E workflow and iOS device build --- .github/actions/create-demo-env/action.yml | 25 ++ .github/workflows/e2e.yml | 235 ++++++++++++++++++ .../demo/Assets/Scripts/Editor/BuildScript.cs | 43 ++++ 3 files changed, 303 insertions(+) create mode 100644 .github/actions/create-demo-env/action.yml create mode 100644 .github/workflows/e2e.yml diff --git a/.github/actions/create-demo-env/action.yml b/.github/actions/create-demo-env/action.yml new file mode 100644 index 00000000..d6d8abe5 --- /dev/null +++ b/.github/actions/create-demo-env/action.yml @@ -0,0 +1,25 @@ +name: "Create demo .env" +description: "Writes the Unity demo app's .env file used by E2E builds" +inputs: + onesignal-app-id: + description: "OneSignal App ID for the demo app" + required: true + onesignal-api-key: + description: "OneSignal REST API key for the demo app" + required: true + e2e-mode: + description: "Whether to enable E2E_MODE in the demo app" + required: false + default: "true" +runs: + using: "composite" + steps: + - name: Write .env + working-directory: examples/demo + shell: bash + run: | + { + echo "ONESIGNAL_APP_ID=${{ inputs.onesignal-app-id }}" + echo "ONESIGNAL_API_KEY=${{ inputs.onesignal-api-key }}" + echo "E2E_MODE=${{ inputs.e2e-mode }}" + } > .env diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 00000000..e8dd461d --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,235 @@ +name: E2E Tests + +on: + push: + branches: + - rel/** + workflow_dispatch: + inputs: + platform: + description: "Platform to test" + required: true + default: "both" + type: choice + options: + - android + - ios + - both + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-android: + if: >- + github.event_name == 'push' || + github.event.inputs.platform == 'android' || + github.event.inputs.platform == 'both' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + lfs: true + + - name: Create demo .env + uses: ./.github/actions/create-demo-env + with: + onesignal-app-id: ${{ vars.APPIUM_ONESIGNAL_APP_ID }} + onesignal-api-key: ${{ secrets.APPIUM_ONESIGNAL_API_KEY }} + + - name: Resolve OneSignal Android SDK version + id: android-sdk-version + run: | + VERSION=$(grep -oE 'com\.onesignal:OneSignal:[^"]+' com.onesignal.unity.android/Editor/OneSignalAndroidDependencies.xml | head -n1 | cut -d: -f3) + if [ -z "$VERSION" ]; then + echo "::error::Could not parse OneSignal Android SDK version from OneSignalAndroidDependencies.xml" + exit 1 + fi + echo "version=${VERSION}" >> "$GITHUB_OUTPUT" + + - name: Wait for OneSignal Android SDK on Maven Central + uses: OneSignal/sdk-shared/.github/actions/wait-for-maven-artifact@main + with: + version: ${{ steps.android-sdk-version.outputs.version }} + + - name: Cache Unity Library + uses: actions/cache@v5 + with: + path: examples/demo/Library + key: unity-library-android-${{ hashFiles('examples/demo/Packages/manifest.json', 'examples/demo/ProjectSettings/ProjectVersion.txt') }} + restore-keys: unity-library-android- + + - name: Build APK + uses: game-ci/unity-builder@v4 + env: + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} + UNITY_EMAIL: ${{ secrets.UNITY_USERNAME }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + with: + projectPath: examples/demo + unityVersion: auto + targetPlatform: Android + buildMethod: BuildScript.BuildAndroidEmulator + allowDirtyBuild: true + + - name: Upload APK + uses: actions/upload-artifact@v7 + with: + name: demo-apk + path: examples/demo/Build/Android/onesignal-demo.apk + retention-days: 1 + compression-level: 0 + + build-ios: + if: >- + github.event_name == 'push' || + github.event.inputs.platform == 'ios' || + github.event.inputs.platform == 'both' + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + lfs: true + + - name: Create demo .env + uses: ./.github/actions/create-demo-env + with: + onesignal-app-id: ${{ vars.APPIUM_ONESIGNAL_APP_ID }} + onesignal-api-key: ${{ secrets.APPIUM_ONESIGNAL_API_KEY }} + + - name: Cache Unity Library + uses: actions/cache@v5 + with: + path: examples/demo/Library + key: unity-library-ios-${{ hashFiles('examples/demo/Packages/manifest.json', 'examples/demo/ProjectSettings/ProjectVersion.txt') }} + restore-keys: unity-library-ios- + + - name: Cache Xcode DerivedData + uses: actions/cache@v5 + with: + path: examples/demo/Build/iOS-DerivedData + key: deriveddata-${{ runner.os }}-${{ hashFiles('examples/demo/Build/iOS/Unity-iPhone.xcodeproj/project.pbxproj', 'examples/demo/Build/iOS/Podfile') }} + restore-keys: deriveddata-${{ runner.os }}- + + - name: Export Xcode project from Unity + uses: game-ci/unity-builder@v4 + env: + UNITY_SERIAL: ${{ secrets.UNITY_SERIAL }} + UNITY_EMAIL: ${{ secrets.UNITY_USERNAME }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + with: + projectPath: examples/demo + unityVersion: auto + targetPlatform: iOS + buildMethod: BuildScript.BuildiOSDevice + allowDirtyBuild: true + cacheUnityInstallationOnMac: true + + - name: Pod install + working-directory: examples/demo/Build/iOS + run: | + if [ -f Podfile ]; then + pod install --repo-update + else + echo "No Podfile in Unity-exported Xcode project; skipping pod install" + fi + + - name: Set up iOS codesigning + uses: OneSignal/sdk-shared/.github/actions/setup-ios-demo-codesigning@main + with: + p12-base64: ${{ secrets.APPIUM_IOS_DEV_CERT_P12_BASE64 }} + p12-password: ${{ secrets.APPIUM_IOS_DEV_CERT_PASSWORD }} + asc-key-id: ${{ secrets.APPIUM_APP_STORE_CONNECT_KEY_ID }} + asc-issuer-id: ${{ secrets.APPIUM_APP_STORE_CONNECT_ISSUER_ID }} + asc-private-key: ${{ secrets.APPIUM_APP_STORE_CONNECT_PRIVATE_KEY }} + + - name: Build signed IPA + working-directory: examples/demo/Build/iOS + # Unity always names the generated Xcode project Unity-iPhone, even + # though the demo's product name is "OneSignal Demo". After + # `pod install` an Unity-iPhone.xcworkspace exists and must be used; + # without CocoaPods we fall back to the .xcodeproj. The + # SigningPostProcessor already pinned DEVELOPMENT_TEAM and the NSE / + # Live Activity bundle IDs to match ExportOptions.plist. + run: | + if [ -d Unity-iPhone.xcworkspace ]; then + CONTAINER_ARGS=(-workspace Unity-iPhone.xcworkspace) + else + CONTAINER_ARGS=(-project Unity-iPhone.xcodeproj) + fi + xcodebuild archive \ + "${CONTAINER_ARGS[@]}" \ + -scheme Unity-iPhone \ + -configuration Release \ + -sdk iphoneos \ + -destination 'generic/platform=iOS' \ + -archivePath build/App.xcarchive \ + -derivedDataPath ../iOS-DerivedData \ + -quiet \ + -hideShellScriptEnvironment \ + CODE_SIGN_STYLE=Manual \ + COMPILER_INDEX_STORE_ENABLE=NO + xcodebuild -exportArchive \ + -archivePath build/App.xcarchive \ + -exportOptionsPlist "$GITHUB_WORKSPACE/examples/demo/iOS/ExportOptions.plist" \ + -exportPath build/ipa \ + -quiet + + - name: Locate and stage IPA + id: ipa + working-directory: examples/demo + run: | + IPA=$(ls Build/iOS/build/ipa/*.ipa | head -n1) + if [ -z "$IPA" ]; then + echo "::error::No IPA produced by xcodebuild -exportArchive" + exit 1 + fi + cp "$IPA" demo.ipa + echo "path=examples/demo/demo.ipa" >> "$GITHUB_OUTPUT" + + - name: Verify aps-environment in IPA + working-directory: examples/demo + run: | + unzip -oq demo.ipa -d /tmp/ipa + APP=$(ls -d /tmp/ipa/Payload/*.app | head -n1) + codesign -d --entitlements - "$APP" 2>&1 | tee /tmp/entitlements.txt + if ! grep -q 'aps-environment' /tmp/entitlements.txt; then + echo "::error::Built IPA is missing aps-environment entitlement; push subscription will not work" + exit 1 + fi + + - name: Upload IPA + uses: actions/upload-artifact@v7 + with: + name: demo-ipa + path: ${{ steps.ipa.outputs.path }} + retention-days: 1 + compression-level: 0 + + e2e-android: + needs: build-android + uses: OneSignal/sdk-shared/.github/workflows/appium-e2e.yml@main + secrets: inherit + with: + platform: android + app-artifact: demo-apk + app-filename: onesignal-demo.apk + sdk-type: unity + build-name: unity-android-${{ github.ref_name }}-${{ github.run_number }} + + e2e-ios: + needs: build-ios + uses: OneSignal/sdk-shared/.github/workflows/appium-e2e.yml@main + secrets: inherit + with: + platform: ios + app-artifact: demo-ipa + app-filename: demo.ipa + sdk-type: unity + build-name: unity-ios-${{ github.ref_name }}-${{ github.run_number }} diff --git a/examples/demo/Assets/Scripts/Editor/BuildScript.cs b/examples/demo/Assets/Scripts/Editor/BuildScript.cs index 623a932e..697ad6c2 100644 --- a/examples/demo/Assets/Scripts/Editor/BuildScript.cs +++ b/examples/demo/Assets/Scripts/Editor/BuildScript.cs @@ -110,6 +110,49 @@ public static void BuildiOSSimulator() HandleReport(report, IOSOutputDir); } + /// + /// Builds an Xcode project targeting physical iOS devices, used by CI to + /// produce a signed IPA for BrowserStack. The OneSignal SDK and demo + /// post-processors add push capabilities, the NSE / Live Activity widget + /// targets, and pin DEVELOPMENT_TEAM + aps-environment=development so the + /// archive step can sign with the Appium provisioning profiles in + /// ExportOptions.plist. + /// + public static void BuildiOSDevice() + { + Directory.CreateDirectory(IOSOutputDir); + + PlayerSettings.SetScriptingBackend(NamedBuildTarget.iOS, ScriptingImplementation.IL2CPP); + PlayerSettings.iOS.sdkVersion = iOSSdkVersion.DeviceSDK; + + Debug.Log( + $"[BuildScript] iOS sdk={PlayerSettings.iOS.sdkVersion} backend={PlayerSettings.GetScriptingBackend(NamedBuildTarget.iOS)}" + ); + + PlayerSettings.SetIl2CppCodeGeneration( + NamedBuildTarget.iOS, + Il2CppCodeGeneration.OptimizeSize + ); + PlayerSettings.SetManagedStrippingLevel(NamedBuildTarget.iOS, ManagedStrippingLevel.High); + PlayerSettings.stripEngineCode = true; + + PlayerSettings.SetIl2CppCompilerConfiguration( + NamedBuildTarget.iOS, + Il2CppCompilerConfiguration.Release + ); + + var options = new BuildPlayerOptions + { + scenes = GetScenes(), + locationPathName = IOSOutputDir, + target = BuildTarget.iOS, + options = BuildOptions.None, + }; + + var report = BuildPipeline.BuildPlayer(options); + HandleReport(report, IOSOutputDir); + } + private static string[] GetScenes() { var scenes = EditorBuildSettings.scenes; From 2da09ce26ae51b79143fcebc4dede07610f350d4 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 18 May 2026 13:31:41 -0700 Subject: [PATCH 2/4] ci: address PR review nits - create-demo-env: route inputs through step env to avoid the documented Actions script-injection pattern. Same behavior; values arrive as environment variables instead of being textually substituted into the shell program. - e2e.yml: rebase the iOS DerivedData cache key on inputs that exist at checkout time (manifest.json, ProjectVersion.txt, ProjectSettings.asset, com.onesignal.unity.{ios,core}/Editor/**, ExportOptions.plist) instead of the Unity-exported pbxproj/Podfile, which don't exist when the cache step runs and caused the key to collapse to a single global slot. Co-authored-by: Cursor --- .github/actions/create-demo-env/action.yml | 10 +++++++--- .github/workflows/e2e.yml | 6 +++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/actions/create-demo-env/action.yml b/.github/actions/create-demo-env/action.yml index d6d8abe5..60424773 100644 --- a/.github/actions/create-demo-env/action.yml +++ b/.github/actions/create-demo-env/action.yml @@ -17,9 +17,13 @@ runs: - name: Write .env working-directory: examples/demo shell: bash + env: + APP_ID: ${{ inputs.onesignal-app-id }} + API_KEY: ${{ inputs.onesignal-api-key }} + E2E_MODE: ${{ inputs.e2e-mode }} run: | { - echo "ONESIGNAL_APP_ID=${{ inputs.onesignal-app-id }}" - echo "ONESIGNAL_API_KEY=${{ inputs.onesignal-api-key }}" - echo "E2E_MODE=${{ inputs.e2e-mode }}" + echo "ONESIGNAL_APP_ID=$APP_ID" + echo "ONESIGNAL_API_KEY=$API_KEY" + echo "E2E_MODE=$E2E_MODE" } > .env diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index e8dd461d..e214e465 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -111,10 +111,14 @@ jobs: restore-keys: unity-library-ios- - name: Cache Xcode DerivedData + # Hash inputs to the Unity iOS export (files that exist at checkout + # time) rather than the exported Xcode project / Podfile, which + # don't exist until later steps run. Mirrors the Android Unity + # Library cache key shape above. uses: actions/cache@v5 with: path: examples/demo/Build/iOS-DerivedData - key: deriveddata-${{ runner.os }}-${{ hashFiles('examples/demo/Build/iOS/Unity-iPhone.xcodeproj/project.pbxproj', 'examples/demo/Build/iOS/Podfile') }} + key: deriveddata-${{ runner.os }}-${{ hashFiles('examples/demo/Packages/manifest.json', 'examples/demo/ProjectSettings/ProjectVersion.txt', 'examples/demo/ProjectSettings/ProjectSettings.asset', 'com.onesignal.unity.ios/Editor/**', 'com.onesignal.unity.core/Editor/**', 'examples/demo/iOS/ExportOptions.plist') }} restore-keys: deriveddata-${{ runner.os }}- - name: Export Xcode project from Unity From ca8a03fcd89e2b897495c1f4edf51d5ee4cf4a86 Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 18 May 2026 14:17:38 -0700 Subject: [PATCH 3/4] ci: write ONESIGNAL_ANDROID_CHANNEL_ID to demo .env Matches the Capacitor demo .env pattern (channel `7ec2ece9-...`). Required for the Appium "send WITH SOUND notification" spec on Android, which posts a notification using this channel ID; without it the channel falls back to default and the sound assertion fails. Exposed as an action input so a future caller can override. Co-authored-by: Cursor --- .github/actions/create-demo-env/action.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/actions/create-demo-env/action.yml b/.github/actions/create-demo-env/action.yml index 60424773..bbd29dfc 100644 --- a/.github/actions/create-demo-env/action.yml +++ b/.github/actions/create-demo-env/action.yml @@ -7,6 +7,10 @@ inputs: onesignal-api-key: description: "OneSignal REST API key for the demo app" required: true + onesignal-android-channel-id: + description: "Android Notification Channel ID used by the WITH SOUND test notification" + required: false + default: "7ec2ece9-c538-4656-9516-1316f48a005c" e2e-mode: description: "Whether to enable E2E_MODE in the demo app" required: false @@ -20,10 +24,12 @@ runs: env: APP_ID: ${{ inputs.onesignal-app-id }} API_KEY: ${{ inputs.onesignal-api-key }} + ANDROID_CHANNEL_ID: ${{ inputs.onesignal-android-channel-id }} E2E_MODE: ${{ inputs.e2e-mode }} run: | { echo "ONESIGNAL_APP_ID=$APP_ID" echo "ONESIGNAL_API_KEY=$API_KEY" + echo "ONESIGNAL_ANDROID_CHANNEL_ID=$ANDROID_CHANNEL_ID" echo "E2E_MODE=$E2E_MODE" } > .env From ab21e053ac9e76787864b2264d7701184a36411e Mon Sep 17 00:00:00 2001 From: Fadi George Date: Mon, 18 May 2026 15:29:16 -0700 Subject: [PATCH 4/4] ci: fail fast when Unity iOS export has no Podfile Drop the Podfile presence check; `pod install` already errors out with "[!] No \`Podfile' found in the project directory." when it's missing, which is enough to fail the step and surface a broken EDM4U / post-processor run. Addresses Sherwin's review comment on #870. Co-authored-by: Cursor --- .github/workflows/e2e.yml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index e214e465..5861aa67 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -137,12 +137,7 @@ jobs: - name: Pod install working-directory: examples/demo/Build/iOS - run: | - if [ -f Podfile ]; then - pod install --repo-update - else - echo "No Podfile in Unity-exported Xcode project; skipping pod install" - fi + run: pod install --repo-update - name: Set up iOS codesigning uses: OneSignal/sdk-shared/.github/actions/setup-ios-demo-codesigning@main