Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e700f24
ci(SDK-5040): parallelize CI jobs with KMP cache
fadi-george Aug 19, 2026
69df65f
ci(SDK-5040): use standard macOS runners
fadi-george Aug 19, 2026
cbb7460
ci(SDK-5040): remove explicit Xcode version selection
fadi-george Aug 19, 2026
811b724
ci(SDK-5040): update workflow actions
fadi-george Aug 19, 2026
5ff61fa
ci(SDK-5040): use swiftlint-action
fadi-george Aug 19, 2026
8516e78
ci(SDK-5040): add concurrency cancellation
fadi-george Aug 19, 2026
0e84427
ci(SDK-5040): use simulator UDID for xcodebuild
fadi-george Aug 19, 2026
65d81cd
ci(SDK-5040): update codeql actions
fadi-george Aug 19, 2026
a48d67b
fix(tests): reset resilient storage in mocks
fadi-george Aug 19, 2026
4a794fc
fix(tests): remove async setup races
fadi-george Aug 19, 2026
d937a30
fix(tests): replace sleeps with predicate expectations
fadi-george Aug 19, 2026
d5f4afe
ci(SDK-5040): remove PR trigger from CodeQL
fadi-george Aug 19, 2026
acd0cb5
ci(SDK-5040): optimize simulator build and stabilize tests
fadi-george Aug 19, 2026
185de16
ci(SDK-5040): boot simulator and parallelize tests
fadi-george Aug 19, 2026
f33da0a
test(consistency): refactor concurrent token update test
fadi-george Aug 19, 2026
a47fe65
ci(SDK-5040): parallelize simulator boot and build
fadi-george Aug 19, 2026
70ed8cb
test(SDK-5040): replace fixed waits with condition-based polling
fadi-george Aug 19, 2026
2f57fc0
test(SDK-5040): refactor waitForCondition and ObjC API
fadi-george Aug 19, 2026
09a1b52
ci(SDK-5040): use generic simulator destination
fadi-george Aug 19, 2026
9282f71
test(SDK-5040): improve switch user integration test reliability
fadi-george Aug 19, 2026
606bfd0
ci(SDK-5040): boot simulator after build step
fadi-george Aug 19, 2026
cc1ef29
ci(SDK-5040): add -quiet flag to xcodebuild
fadi-george Aug 19, 2026
fe58a39
test(SDK-5040): fix flaky switch user test
fadi-george Aug 19, 2026
2ac9929
temp test with 1 worker
fadi-george Aug 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 155 additions & 40 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,172 @@ on:
- '.github/**'
- 'README.md'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
KMP_XCFRAMEWORK_PATH: OneSignal-KMP-SDK/kmp/build/XCFrameworks/release/OneSignalKMP.xcframework

jobs:
build:
name: Build and Test using any available iPhone simulator
runs-on: macos-15-large
lint:
name: Swift Lint
runs-on: macos-latest

steps:
- name: Select Xcode Version
run: |
sudo xcode-select -s /Applications/Xcode_16.4.app/Contents/Developer
- name: Checkout OneSignal-iOS-SDK
uses: actions/checkout@v4
uses: actions/checkout@v7
- name: Run Swift Lint
uses: cirruslabs/swiftlint-action@v1
with:
version: latest

kmp-xcframework:
name: Build OneSignalKMP XCFramework
runs-on: macos-latest

steps:
- name: Checkout OneSignal-iOS-SDK
uses: actions/checkout@v7
with:
submodules: recursive
- name: Create KMP cache key
id: kmp
run: |
kmp_sha="$(git rev-parse HEAD:OneSignal-KMP-SDK)"
xcode_hash="$(xcodebuild -version | shasum -a 256 | awk '{print $1}')"
echo "cache-key=kmp-xcframework-${{ runner.os }}-${{ runner.arch }}-${xcode_hash}-${kmp_sha}" >> "$GITHUB_OUTPUT"
- name: Restore OneSignalKMP XCFramework
id: kmp-cache
uses: actions/cache/restore@v6
with:
path: ${{ env.KMP_XCFRAMEWORK_PATH }}
key: ${{ steps.kmp.outputs.cache-key }}
- name: Setup JDK 17
uses: actions/setup-java@v4
if: steps.kmp-cache.outputs.cache-hit != 'true'
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Build OneSignalKMP XCFramework
run: iOS_SDK/OneSignalSDK/build_kmp_xcframework.sh
if: steps.kmp-cache.outputs.cache-hit != 'true'
uses: gradle/actions/setup-gradle@v6
- name: Assemble OneSignalKMP XCFramework
if: steps.kmp-cache.outputs.cache-hit != 'true'
run: |
iOS_SDK/OneSignalSDK/build_kmp_xcframework.sh \
:kmp:assembleOneSignalKMPReleaseXCFramework
- name: Save OneSignalKMP XCFramework
if: steps.kmp-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v6
with:
path: ${{ env.KMP_XCFRAMEWORK_PATH }}
key: ${{ steps.kmp.outputs.cache-key }}
- name: Package OneSignalKMP XCFramework
run: |
tar -czf "$RUNNER_TEMP/OneSignalKMP.xcframework.tar.gz" \
-C "$(dirname "$KMP_XCFRAMEWORK_PATH")" \
"$(basename "$KMP_XCFRAMEWORK_PATH")"
- name: Upload OneSignalKMP XCFramework
uses: actions/upload-artifact@v7
with:
name: OneSignalKMP-XCFramework
path: ${{ runner.temp }}/OneSignalKMP.xcframework.tar.gz
retention-days: 1

ios-simulator:
name: Build and Test using any available iPhone simulator
runs-on: macos-latest
needs: kmp-xcframework

steps:
- name: Checkout OneSignal-iOS-SDK
uses: actions/checkout@v7
- name: Download OneSignalKMP XCFramework
uses: actions/download-artifact@v8
with:
name: OneSignalKMP-XCFramework
path: ${{ runner.temp }}
- name: Extract OneSignalKMP XCFramework
run: |
mkdir -p "$(dirname "$KMP_XCFRAMEWORK_PATH")"
tar -xzf "$RUNNER_TEMP/OneSignalKMP.xcframework.tar.gz" \
-C "$(dirname "$KMP_XCFRAMEWORK_PATH")"
- name: Select iPhone simulator
id: simulator
run: |
runtime_id="$(xcrun simctl list runtimes available --json | jq -r \
'[.runtimes[] | select(.platform == "iOS")] | sort_by(.version | split(".") | map(tonumber)) | last.identifier')"
device_id="$(xcrun simctl list devices available --json | jq -r --arg runtime "$runtime_id" \
'.devices[$runtime] | map(select(.name | startswith("iPhone"))) | first.udid')"
if [[ -z "$device_id" || "$device_id" == "null" ]]; then
echo "No available iPhone simulator found" >&2
exit 1
fi
echo "device-id=$device_id" >> "$GITHUB_OUTPUT"
- name: Build
env:
scheme: ${{ 'UnitTestApp' }}
file_to_build: ${{ 'iOS_SDK/OneSignalSDK/OneSignal.xcodeproj' }}
filetype_parameter: ${{ 'project' }}
run: |
xcodebuild build-for-testing \
-quiet \
-scheme "$scheme" \
-"$filetype_parameter" "$file_to_build" \
-destination "generic/platform=iOS Simulator" \
-enableCodeCoverage NO \
ARCHS=arm64 \
ONLY_ACTIVE_ARCH=YES
- name: Start simulator boot
env:
device_id: ${{ steps.simulator.outputs.device-id }}
run: |
device_state="$(xcrun simctl list devices --json | jq -r --arg device "$device_id" \
'[.devices[][] | select(.udid == $device)] | first.state')"
if [[ "$device_state" != "Booted" ]]; then
xcrun simctl boot "$device_id"
fi
- name: Wait for simulator boot
env:
device_id: ${{ steps.simulator.outputs.device-id }}
run: xcrun simctl bootstatus "$device_id" -b
- name: Test
env:
scheme: ${{ 'UnitTestApp' }}
test_plan: ${{ 'UnitTestApp_TestPlan_Reduced' }}
platform: ${{ 'iOS Simulator' }}
file_to_build: ${{ 'iOS_SDK/OneSignalSDK/OneSignal.xcodeproj' }}
filetype_parameter: ${{ 'project' }}
device_id: ${{ steps.simulator.outputs.device-id }}
run: |
xcodebuild test-without-building \
-quiet \
-scheme "$scheme" \
-testPlan "$test_plan" \
-"$filetype_parameter" "$file_to_build" \
-destination "platform=$platform,id=$device_id,arch=arm64" \
-maximum-parallel-testing-workers 1 \
-enableCodeCoverage NO

catalyst:
name: KMP logger Mac Catalyst integration
runs-on: macos-latest
needs: kmp-xcframework

steps:
- name: Checkout OneSignal-iOS-SDK
uses: actions/checkout@v7
- name: Download OneSignalKMP XCFramework
uses: actions/download-artifact@v8
with:
name: OneSignalKMP-XCFramework
path: ${{ runner.temp }}
- name: Extract OneSignalKMP XCFramework
run: |
mkdir -p "$(dirname "$KMP_XCFRAMEWORK_PATH")"
tar -xzf "$RUNNER_TEMP/OneSignalKMP.xcframework.tar.gz" \
-C "$(dirname "$KMP_XCFRAMEWORK_PATH")"
- name: Archive OneSignalFramework for Catalyst
run: |
xcodebuild archive \
Expand Down Expand Up @@ -57,32 +201,3 @@ jobs:
iOS_SDK/OneSignalSDK/CatalystLoggerHost/main.swift \
-o "$host"
DYLD_FRAMEWORK_PATH="$frameworks" "$host"
- name: Set Default Scheme
run: |
default="UnitTestApp"
echo $default | cat >default
echo Using default scheme: $default
- name: Run Swift Lint
run: |
swiftlint
- name: Build
env:
scheme: ${{ 'UnitTestApp' }}
platform: ${{ 'iOS Simulator' }}
file_to_build: ${{ 'iOS_SDK/OneSignalSDK/OneSignal.xcodeproj' }}
filetype_parameter: ${{ 'project' }}
run: |
# xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959)
device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"`
xcodebuild build-for-testing -scheme "$scheme" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device"
- name: Test
env:
scheme: ${{ 'UnitTestApp' }}
test_plan: ${{ 'UnitTestApp_TestPlan_Reduced' }}
platform: ${{ 'iOS Simulator' }}
file_to_build: ${{ 'iOS_SDK/OneSignalSDK/OneSignal.xcodeproj' }}
filetype_parameter: ${{ 'project' }}
run: |
# xcrun xctrace returns via stderr, not the expected stdout (see https://developer.apple.com/forums/thread/663959)
device=`xcrun xctrace list devices 2>&1 | grep -oE 'iPhone.*?[^\(]+' | head -1 | awk '{$1=$1;print}' | sed -e "s/ Simulator$//"`
xcodebuild test-without-building -scheme "$scheme" -testPlan "$test_plan" -"$filetype_parameter" "$file_to_build" -destination "platform=$platform,name=$device"
15 changes: 8 additions & 7 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,13 @@ name: "CodeQL"
on:
push:
branches: [ "main" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "main" ]
schedule:
- cron: '35 6 * * 2'

concurrency:
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true

jobs:
analyze:
name: Analyze
Expand All @@ -46,11 +47,11 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v7

# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
Expand All @@ -64,7 +65,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v4

# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
Expand All @@ -77,6 +78,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{matrix.language}}"
22 changes: 16 additions & 6 deletions iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOSDispatchQueue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,43 @@
THE SOFTWARE.
*/

import Foundation
import OneSignalOSCore

public class MockDispatchQueue: OSDispatchQueue {
let requestDispatch = DispatchQueue(label: "MockDispatchQueue")
var numDispatches = 0
private let dispatchCondition = NSCondition()
private var numDispatches = 0

public init() {}

public func async(execute work: @escaping @convention(block) () -> Void) {
requestDispatch.async {
work()
self.numDispatches += 1
self.recordDispatch()
}
}

public func asyncAfterTime(deadline: DispatchTime, execute work: @escaping @Sendable @convention(block) () -> Void) {
requestDispatch.asyncAfterTime(deadline: deadline) {
work()
self.numDispatches += 1
self.recordDispatch()
}
}

public func waitForDispatches(_ numDispatches: Int) {
dispatchCondition.lock()
defer { dispatchCondition.unlock() }

while self.numDispatches < numDispatches {
requestDispatch.sync {
Thread.sleep(forTimeInterval: TimeInterval(1))
}
dispatchCondition.wait()
}
}

private func recordDispatch() {
dispatchCondition.lock()
numDispatches += 1
dispatchCondition.broadcast()
dispatchCondition.unlock()
}
}
35 changes: 33 additions & 2 deletions iOS_SDK/OneSignalSDK/OneSignalCoreMocks/MockOneSignalClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ public class MockOneSignalClient: NSObject, IOneSignalClient {
public var executedRequests: [OneSignalRequest] = []
/// Requests that have entered `execute` (including those still held / delayed).
public private(set) var startedRequests: [OneSignalRequest] = []
public private(set) var completedRequests: [OneSignalRequest] = []
public var executeInstantaneously = false
/// Set to true to make it unnecessary to setup mock responses for every request possible
public var fireSuccessForAllRequests = false
Expand Down Expand Up @@ -91,6 +92,7 @@ public class MockOneSignalClient: NSObject, IOneSignalClient {
networkRequestCount = 0
executedRequests.removeAll()
startedRequests.removeAll()
completedRequests.removeAll()
heldExecutions.removeAll()
holdResponses = false
executeInstantaneously = true
Expand Down Expand Up @@ -181,6 +183,10 @@ public class MockOneSignalClient: NSObject, IOneSignalClient {
allRequestsHandled = false
print("🧪 cannot find a mock response for request: \(stringifiedRequest)")
}

lock.withLock {
completedRequests.append(request)
}
}

func didCompleteRequest(_ request: OneSignalRequest) {
Expand Down Expand Up @@ -214,9 +220,10 @@ extension MockOneSignalClient {
*/
@objc
public func onlyOneRequest(contains path: String, contains payload: [String: Any]) -> Bool {
let requests = lock.withLock { executedRequests }
var found = false

for request in executedRequests {
for request in requests {
guard let params = request.parameters as? NSDictionary else {
continue
}
Expand All @@ -238,7 +245,8 @@ extension MockOneSignalClient {
}

public func hasExecutedRequestOfType(_ type: AnyClass, expectedCount: Int? = nil) -> Bool {
let matchingCount = executedRequests.filter { request in
let requests = lock.withLock { executedRequests }
let matchingCount = requests.filter { request in
request.isKind(of: type)
}.count

Expand All @@ -248,4 +256,27 @@ extension MockOneSignalClient {
return matchingCount > 0
}
}

public func hasCompletedRequestOfType(_ type: AnyClass, expectedCount: Int? = nil) -> Bool {
let matchingCount = completedRequestCount(ofType: type)

if let expectedCount {
return matchingCount == expectedCount
}
return matchingCount > 0
}

public func completedRequestCount(ofType type: AnyClass) -> Int {
let requests = lock.withLock { completedRequests }
return requests.filter { request in
request.isKind(of: type)
}.count
}

public func startedRequestCount(ofType type: AnyClass) -> Int {
let requests = lock.withLock { startedRequests }
return requests.filter { request in
request.isKind(of: type)
}.count
}
}
Loading
Loading