Skip to content

fix: merge main into workstation/swift-migration and port pending concurrency fixes - #983

Merged
nickolas-dimitrakas merged 25 commits into
workstation/swift-migrationfrom
fix/merge-main-into-workstation-swift-migration
Sep 10, 2026
Merged

nickolas-dimitrakas merged 25 commits into
workstation/swift-migrationfrom
fix/merge-main-into-workstation-swift-migration

Conversation

@nickolas-dimitrakas

Copy link
Copy Markdown
Contributor

Summary

  • Real git merge of main's tip (23 commits since fix: merge main into workstation/swift-migration and apply pending test fixes #915) into workstation/swift-migration, resolving 42 conflicted files.
  • Ported the substance of main's fix: close the kitsRegistry flush race and stabilise the ObjC test suite #878/fix: eliminate the kitsRegistry thread-safety crash #916 kitsRegistry concurrency fixes into the Swift-migrated container (MPKitContainerExecutionAdapter.m + MPKitContainer.swift, which don't exist in the same shape as main's now-deleted MPKitContainer.m): a separate bracketsSemaphore, deferred wrapperInstance teardown (freeKitRegister:/teardownDetachedWrapperInstance:), and a kitTeardownGroup (needed for mParticle.m's already-auto-merged calls to compile at all).
  • Caught and fixed a regression that same port introduced: removeAllSideloadedKits identified sideloaded kits via wrapperInstance respondsToSelector:, which stopped matching once the ported fix started nil'ing wrapperInstance earlier — testSwitchWorkspaceSideloadedKits caught it. Fixed the same way main's fix: eliminate the kitsRegistry thread-safety crash #916 did (identify by code).
  • This same sync also lands the fix for the "weak wait" test gap fix: merge main into workstation/swift-migration and apply pending test fixes #915 had flagged as a known follow-up (testSwitchWorkspaceOptions/testSwitchWorkspaceSideloadedKits now wait on .initialized, not just a signal that fires before startup finishes).
  • Mechanical conflicts (VERSION, CHANGELOG, podspecs) took main's 9.4.3. MPKitRokt.m/MPConnector.m/their ObjC tests: took this branch's Swift-migrated versions where main's side tested now-removed APIs; ported forward one real regression test (GoDaddy cert-pinning fingerprint check) into the Swift suite.

Test plan

  • xcodebuild build — clean
  • ObjC suite (mParticle-Apple-SDK scheme) — 970/970 passing
  • Swift suite (mParticle-Apple-SDK-Swift scheme) — 893/894 passing; the one failure (MPPersistenceStoreTests.testMigratesVersion30RowsAndPurgesExpiredRecords) reproduces identically on workstation/swift-migration's tip with none of this PR's changes — pre-existing, not touched here
  • trunk check — clean, no new issues

🤖 Generated with Claude Code

thomson-t and others added 25 commits August 28, 2026 16:44
feat(network): accept native GoDaddy R1 certificate chains

Add GoDaddy TLS Root CA - R1 to the default certificate set while retaining all existing trust anchors.

Verify the embedded certificate against GoDaddy's published SHA-256 fingerprint to prevent accidental pin changes.

#agentic
chore: (release) 9.4.1

Updates version to 9.4.1 across the mParticle ecosystem.
release-ecosystem-from-main.yml is registered in Actions as
disabled_manually but its file is not on main at all - only on unmerged
branches - so describing it as a workflow file in the tree that never
runs was wrong. The real trap is that the workflow list and the tree
disagree in both directions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Observed on this branch: the build-kits Pod Lint jobs fail as a batch on
a CocoaPods CDN error, and a cancelled job is a timeout-minutes expiry or
a superseded push under pull-request.yml's cancel-in-progress concurrency
group - not a test result.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 5 to 6.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](actions/setup-java@v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* ci: stop simulator cloning and bound network/test hangs

Two unrelated hangs were holding up CI. Both were unbounded, so they
consumed a job's full budget instead of failing.

native-tests: the iOS x mParticle-Apple-SDK-Swift cell repeatedly hit its
15-minute timeout. A single test, MPDeviceTests.testDictionaryDescription,
took 156-459s across recent runs while every other test finished in under
1.6s; job duration tracked that test exactly. The same test takes 0.069s on
tvOS in the same run, so the cause is simulator state, not the assertions.
MPDevice reads UIDevice.current for name and identifierForVendor, which are
XPC calls into simulator daemons, and mParticle-Apple-SDK-Swift.xcscheme is
the only scheme in the repo with parallelizable="YES" - so xcodebuild clones
and boots extra iOS simulators on a runner that just erased one, starving the
daemons that test then blocks on.

Disable parallel testing for the run. The suite is ~2s of tests; locally
this is 2x faster end to end (33s vs 74s) because it no longer boots 8
simulator clones. Also wait for a real boot via simctl bootstatus - the
action's wait_for_boot only waits for state=Booted, not for first-boot work
to finish - and add a per-test timeout so a hung test fails in 2 minutes
with an explicit diagnostic instead of silently eating the job timeout. The
slowest genuine test on CI is ~30s, leaving 4x headroom.

build-kits: Build rokt-sdk-plus-ios stalled 30 minutes inside a git fetch of
stripe-ios (2.7 GB, reached transitively via rokt-payment-extension-ios),
leaving orphaned git and git-remote-http processes. The job has no
timeout-minutes, so only an unrelated concurrency cancel stopped it;
otherwise it would have held a macOS runner for the 6-hour default.

Make git abort a transfer stalled below 1 KB/s for 3 minutes, retry the
resolve three times (matching the existing pod-lint-kits pattern), and add
job timeouts so no hang here can run unbounded again. Healthy resolves take
2-5 minutes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ci: bound each SPM resolve attempt with a kill-and-retry watchdog

The first version of this fix relied on git's own low-speed abort to break
a stalled fetch. It does not fire: on run 33010942238 the stripe-ios fetch
sat for 24.5 minutes with http.lowSpeedLimit/lowSpeedTime set, produced no
error, and never returned - so the retry loop never got a turn and the step
burned its whole 25-minute budget before failing.

Bound each attempt explicitly instead. A watchdog kills the resolve and its
surviving git children once an attempt passes ATTEMPT_TIMEOUT_SECONDS (480s,
against a healthy 2-5 minute resolve), so a stalled attempt is retried rather
than consuming the step. Drop to 2 attempts and a 20-minute step timeout,
since an attempt can no longer run unbounded.

Keep the git low-speed config: it is not sufficient on its own but still
aborts genuinely slow transfers where it does apply.

Verified against the extracted step body with a stubbed resolver: a healthy
resolve exits 0 in 1s with no kills; a permanent hang is killed twice and
fails bounded; a hang followed by a healthy retry is killed once and exits 0.
Defaulted the timeout in-place so an unset variable cannot make the watchdog
fire immediately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ci: run the resolve watchdog cleanup in the parent so it actually executes

Bugbot caught that the watchdog's cleanup was dead code. The watchdog TERMed
the resolve subshell and then slept 5s before escalating, but TERMing the
resolve unblocks the parent's wait immediately, and the parent then kills the
watchdog mid-sleep - so the kill -KILL and the pkill of leftover xcodebuild
and git-remote-http children never ran. The retry could therefore start while
the previous stalled fetch was still alive and race it over SwiftPM's cache
locks, which is the exact failure the watchdog was added to prevent.

The watchdog now only records that it fired (via a marker file) and sends
TERM. Escalation and child cleanup run in the parent after wait returns, where
they cannot be pre-empted, followed by a short grace period before the retry.

Verified by extracting the step body, stubbing the resolver and replacing the
pkill calls with probes: on the old code the cleanup probes fired 0 times out
of 4 expected; they now fire 4/4 on a permanent hang and 2/2 when one attempt
hangs and the retry succeeds. Cleanup still does not run on a non-timeout
failure (0 probes), so a fast failure does not trigger a spurious pkill, and
the healthy path is unchanged at exit 0 with no kills.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…inistic (#870)

testSwitchWorkspaceKitsWithStop waited on two nested 10-second dispatch_after
blocks, so it always burned at least 20 seconds of wall clock and depended on
fixed timing rather than on the SDK actually being ready. Wait on the SDK's own
readiness signals instead - MParticle.initialized after startWithOptions:, and
the replacement shared instance after switchWorkspaceWithOptions: - by spinning
the main run loop until the condition holds. The assertions are unchanged, so
the test still checks the same behaviour; it now takes 0.11s instead of 20.2s.

testActiveKitsRegistryThreadSafety tracked failure in a plain __block BOOL
written from four concurrent blocks, which is itself an unsynchronized data
race, and called XCTFail from those background queues. Guard the shared state
with an NSLock, record only the first exception, and report it from the
dispatch_group_notify block on the main queue.

This commit does not change the flakiness of testActiveKitsRegistryThreadSafety
itself: that crash is a real race in MPKitContainer (flushSerializedKits
enumerates and mutates kitsRegistry without kitsSemaphore, reached via the
early return in configureKits: before the lock is taken). Reproduced at 2
crashes in 10 runs of that test alone. Fixing it changes core SDK locking, so
it is deliberately left for a separate, explicitly-reviewed change.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
docs: trim AGENTS.md to the non-derivable core
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.1.2 to 3.1.5.
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](isaacs/minimatch@v3.1.2...v3.1.5)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.21 to 4.18.1.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](lodash/lodash@4.17.21...4.18.1)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
….29.4 in /RNExample (#767)

chore: bump @babel/plugin-transform-modules-systemjs in /RNExample

Bumps [@babel/plugin-transform-modules-systemjs](https://github.com/babel/babel/tree/HEAD/packages/babel-plugin-transform-modules-systemjs) from 7.25.9 to 7.29.4.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.29.4/packages/babel-plugin-transform-modules-systemjs)

---
updated-dependencies:
- dependency-name: "@babel/plugin-transform-modules-systemjs"
  dependency-version: 7.29.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [shell-quote](https://github.com/ljharb/shell-quote) from 1.8.1 to 1.10.0.
- [Changelog](https://github.com/ljharb/shell-quote/blob/main/CHANGELOG.md)
- [Commits](ljharb/shell-quote@v1.8.1...v1.10.0)

---
updated-dependencies:
- dependency-name: shell-quote
  dependency-version: 1.10.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…ork (#948)

* fix: code-sign the nested Swift framework inside the release XCFramework

mParticle_Apple_SDK.framework embeds mParticle_Apple_SDK_Swift.framework
as a nested framework, but make_artifacts.sh only signed the top-level
xcframework wrapper. That only seals the wrapper's file manifest - it
does not add a code signature to the Mach-O binaries inside. The outer
framework gets re-signed by a consuming app's own build when it embeds
it, but the nested Swift framework is just copied verbatim and is never
re-signed by the consumer, so it reached apps with no code signature at
all. iOS enforces signatures at launch on a physical device, which is
why Simulator and archive/export builds (which do their own recursive
re-sign pass) were unaffected while a Debug build on a device crashed
with "Library not loaded ... missing code signature".

Sign inside-out for every platform slice: nested framework first, then
the framework that embeds it, before sealing the xcframework wrapper.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: force-replace existing signatures when signing xcframework slices

Simulator platform slices come off the archive step already carrying an
automatic ad-hoc signature (device slices do not), so plain `codesign
-s` was silently failing with "is already signed" on those and leaving
the old ad-hoc signature in place instead of the real one. Add --force
so every slice actually gets signed with the intended identity.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
chore: (release) 9.4.2

Updates version to 9.4.2 across the mParticle ecosystem.
…ite (#878)

* fix: close the kitsRegistry race and stabilise the Rokt test suite

Three separate defects were behind the remaining native-tests failures. Taken
together they move MPRoktTests from 6/10 to 15/15 locally, with no crashes, no
async timeouts and no aborted runners.

kitsRegistry data race (shipped code)
-------------------------------------
kitsRegistry is guarded by kitsSemaphore in activeKitsRegistry and in
configureKits:, but flushSerializedKits enumerated and mutated it with no lock
held - reached through the early return in configureKits: that fires before the
lock is taken. Concurrent locked mutation plus unlocked enumeration crashed
testActiveKitsRegistryThreadSafety at 2 runs in 10 in isolation; that test was
not flaky, it was catching this.

flushSerializedKits now snapshots the set under kitsSemaphore and iterates the
snapshot on the main queue without holding the lock - waiting on the semaphore
from the main queue would let a long background critical section stall the main
thread. Because the snapshot already carries the registers, the flush path calls
a new freeKitRegister:integrationId: directly instead of freeKit:, so it no
longer reads kitsRegistry unguarded either. freeKit: keeps the lookup for
configureKits:, which calls it while already holding the semaphore, and stays
lock-free because dispatch_semaphore is not recursive. freeKit: is private to
the implementation with two callers, so there is no kit-facing surface here.

Async budgets of 200ms (largest failure cluster)
------------------------------------------------
Every test in MPRoktTests gave the SDK 200ms to dispatch through
[MParticle messageQueue] and reach the kit container. A slow shared runner
misses that, and because 25 tests shared the budget and the path they failed
together - the "16 failing tests" seen on CI were 16 identical
"Exceeded timeout of 0.2 seconds" errors, not 16 distinct problems. Raised to a
named 5s constant; XCTest and OCMock both return as soon as the expectation is
satisfied, so this costs nothing when the machine is fast. The single rejection
keeps a tighter 1s window, since a rejection must wait out its whole window.

Mock lifetime (aborted the test runner)
--------------------------------------
testConfirmUserNilUserWithEmailCallsIdentifyAndBlocksUntilCompletion captures
the identify completion and deliberately never invokes it, so confirmUser's
completion block stayed alive holding a class mock that tearDown then disposed.
The next test to touch it aborted the process with "Attempt to use unknown
class", taking the runner down and losing the rest of the suite.

tearDown now drains work already queued on the SDK's message queue before the
mocks are stopped, spinning the main run loop so blocks that queue back onto
main can also finish, bounded at 2s so a wedged queue slows teardown rather than
hanging the suite. The partial mock is also stopped before the object it wraps
is released, rather than after. This fixed the residual async timeouts too: a
stale mock meant forwards reached the real container, so those expectations
never fired.

Rejected along the way, both measured: moving assertions out of all 21 OCMArg
matchers (10/15, no change) and draining the stuck completion itself (5/15,
worse - invoking it mutates shared SDK state).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: replace the remaining workspace-switch fixed delays with condition waits

The four workspace-switch tests still built on nested 10-second dispatch_after
chains were the only failures left in the full ObjC suite once the Rokt fixes
landed - they failed 4 runs out of 4 in full-suite context while passing alone,
because a fixed delay is a bet on machine speed rather than on the SDK being
ready.

Converted all four to MPWaitForCondition, the helper added for
testSwitchWorkspaceKitsWithStop. Where a test asserts an outcome that arrives
via the flush on the main queue (registeredKits.count == 0), the wait condition
includes that outcome rather than just "the switch completed", so it cannot race
the flush. Assertions are otherwise unchanged.

testSwitchWorkspaceOptions had a 10-second delay before it even called
startWithOptions:, with nothing async pending; that one is removed rather than
converted. WORKSPACE_SWITCHING_DELAY is now unused and gone, and MParticleTests
has no dispatch_after calls left.

Full ObjC suite before: 0 of 4 runs clean, 2-6 failures each, 538-4379s.
After: 954 tests with 0 failures on 2 of 3 runs, ~75s.

The remaining run failed in testActiveKitsRegistryThreadSafety, which is the
kitsRegistry race and is not fully fixed - see the previous commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix: guard brackets with its own lock, defer kit teardown off kitsSemaphore

PR #878's own new stress tests crashed this branch's CI 2/2 times (once
on iOS, once on tvOS, both process restarts mid-test) while passing
every local run before this fix - isolated x60, full local suite x3.
Reproducing at the whole-class level (MPKitContainerTests, not just the
single test in isolation) eventually caught it locally too, at roughly
the same low rate CI saw it, and pinned the culprit down to two separate
issues:

1. brackets (an NSMutableDictionary ivar) was read by bracketForKit:
   and mutated by updateBracketsWithConfiguration:integrationId: with
   no synchronization at all - not even kitsSemaphore. Concurrent
   mutation of an NSMutableDictionary during a read is exactly the kind
   of crash "Restarting after unexpected exit" describes, and
   testBracketForKitThreadSafety hammers exactly this: 3 reader threads
   calling bracketForKit: while a 4th concurrently calls
   updateBracketsWithConfiguration:. Added a dedicated bracketsSemaphore
   (not kitsSemaphore itself) scoped to just this state, since
   updateBracketsWithConfiguration: is also called from inside
   configureKits:'s kitsSemaphore-locked region - dispatch_semaphore is
   not reentrant, so reusing kitsSemaphore here would deadlock there.

2. freeKitRegister: - reached from configureKits:'s deactivateKits
   cleanup while it holds kitsSemaphore - called stop(), did disk
   cleanup, and posted mParticleKitDidBecomeInactiveNotification
   synchronously, still holding that lock. All of that runs arbitrary
   kit and observer code, which stalls every other thread waiting on
   the lock for as long as it takes, or deadlocks outright if an
   observer calls back into a kitsSemaphore-guarded method. Deferred it
   to dispatch_async(main) the same way flushSerializedKits already
   does; the wrapperInstance detach itself stays inline, since that is
   what synchronizes with isActiveAndNotDisabled:'s reads on other
   threads.

Validated after both fixes: MPKitContainerTests class x60, full local
suite x3, 0 crashes, 0 failures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* test: guard testBracketForKitThreadSafety's shared error state

Same issue testActiveKitsRegistryThreadSafety had before it was fixed:
a plain __block BOOL encounteredError shared across 4 concurrent blocks
is itself an unsynchronized data race, and XCTFail is not documented as
safe to call off the main thread. Applied the same NSLock-guarded
firstException pattern, deferring the actual XCTFail to
dispatch_group_notify's main-queue block.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* fix: wait for deferred kit teardown before starting the next workspace

Bugbot (Cursor) flagged this on the previous commit: freeKitRegister:
defers stop(), disk cleanup, and posting mParticleKitDidBecomeInactive-
Notification to dispatch_async(main), matching flushSerializedKits's
existing pattern - deliberately, so that work never runs while
kitsSemaphore is held. But resetForSwitchingWorkspaces: and reset: both
proceed to their completion (which starts the next workspace's kits)
right after flushSerializedKits/removeAllSideloadedKits return, with no
guarantee the deferred teardown they just scheduled has actually run
yet by then.

For most call paths this is masked by GCD's FIFO ordering on a serial
queue - the teardown block and the "start next workspace" block both
end up on the main queue, in the order they were enqueued. But
startWithKeyCallback: calls identifyNoDispatch:completion:, whose
completion can fire on whatever thread the network layer calls back on
- not necessarily main, and with no ordering relationship to the
already-scheduled teardown at all. A kit whose stop() has process-wide
effects (their example: something like Rokt's stop() calling a shared
close()) can start its new instance and then have the old instance's
deferred stop() run after, tearing down what was just started.

Added a dispatch_group (kitTeardownGroup) that flushSerializedKits and
freeKitRegister: enter synchronously - before returning, not inside the
dispatched block itself, which would leave the same race one level in -
and leave once their deferred work actually completes.
notifyWhenKitTeardownComplete:block: lets a caller wait on it;
resetForSwitchingWorkspaces:/reset: now use it in place of the raw
executeOnMain: call, so completion() (and whatever it starts) only runs
once every kit scheduled for teardown by this reset has actually
finished stopping.

Validated: MPKitContainerTests + MParticleTests together x55 (30 + 25,
after ruling out an intermittent batch of failures as this machine's
own load - no code changes between batches, and two subsequent
batches came back clean), full local suite x3, 0 crashes, 0 failures.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix: scope the kit registry lock to the state it guards

kitsRegistry is class-level state, created once in +initialize, but kitsSemaphore
was a per-instance ivar created in -init. A lock scoped per-instance cannot
protect state scoped per-class: two containers could mutate the same set while
each held its own semaphore, so the lock could not do the job it was there for.

Move kitsSemaphore to a static created alongside kitsRegistry in +initialize, and
update the four sites that reached it through strongSelf-> - a static is not
reachable through the instance, so those would not have compiled otherwise.

Also guard removeAllSideloadedKits and removeKitsFromRegistryInvalidForWorkspaceSwitch,
which mutated the static set with no lock at all. The [kitsRegistry copy] in each
protects the enumeration but not the removeObject: that follows. Both are called
only from mParticle.m, outside any locked region, so taking the lock cannot
deadlock against an existing holder.

testActiveKitsRegistryThreadSafety over 20 runs: 3 crashes, down from roughly 1
in 3. Reduced, not eliminated - something in this path is still unguarded, so
this is a step rather than a fix.

Note the tradeoff: a shared static lock means two containers now block each other
where they previously raced. That is correct, but dispatch_semaphore is not
recursive, so a nested cross-instance call taken under the lock would deadlock
rather than race. No such path was found, but it is worth a reviewer's eye.

#agentic

* fix: synchronize the wrapperInstance handoff in flushSerializedKits

Scoping kitsSemaphore to the class and closing the unlocked mutators (previous
commit) cut the crash rate in testActiveKitsRegistryThreadSafety from ~1 in 3
to 3 in 20, but did not close it. Real crash reports (.ips, not just the
generic "unexpected exit" xcodebuild prints) show all three remaining crashes
faulting in objc_retain/objc_release inside isActiveAndNotDisabled:, called
from activeKitsRegistryWhenLocked - a use-after-free on a single element, not
a set-structure error.

kitRegister.wrapperInstance is declared nonatomic, so it has no synchronization
of its own; kitsSemaphore only protects code that remembers to acquire it.
activeKitsRegistry does, reading wrapperInstance under the lock. But
flushSerializedKits's dispatch_async(main) block called freeKitRegister:,
which set wrapperInstance = nil, without the lock - deliberately, per the
previous commit's comment, to avoid stalling the main thread across file I/O
and a notification post. That left the one write this teardown needed to
synchronize outside the one section that was supposed to cover it, and a
background reader's objc_retain could land mid-write on the object being
released.

Split the detach from the teardown: flushSerializedKits now nils out
wrapperInstance for every snapshotted kit while still holding kitsSemaphore -
a pointer swap, not file I/O - then runs stop(), file cleanup and the
notification afterward, unlocked, via a new teardownDetachedWrapperInstance:
forIntegrationId: that no longer touches kitRegister.wrapperInstance at all.
freeKit:/freeKitRegister: (called from configureKits: while it already holds
the lock) are unchanged.

testActiveKitsRegistryThreadSafety: 0 crashes in 60 runs (was 3/20, was ~1/3).
Full ObjC suite x4: 0 crashes in any run. One assertion failure recurred in
testSwitchWorkspaceSideloadedKits across those runs; bisected against the
parent commit (this fix reverted) under the same load and it fails there too,
with a different assertion failing on a different run - pre-existing
timing-sensitive flakiness in that test's MPWaitForCondition wait under a
heavily loaded machine, unrelated to this change.

startKit: and registerSideloadedKits still read/write this registry unlocked.
Nothing in the current suite calls them concurrently, so they are not
implicated in anything observed, but they are the same category of bug and
remain open.

#agentic

* fix: silence analyzer warning on the NSNull sentinel ternary

CI's run-analyzer job flagged the previous commit: 'incompatible operand types
(id<MPKitProtocol> _Nullable and NSNull * _Nonnull)' at the ?: that boxes a
possibly-nil wrapperInstance for storage in detachedWrapperInstances (NSArray
cannot hold nil directly). The idiom is correct at runtime - this is the
standard way to carry an optional through an NSArray - clang's ternary
type-unification just doesn't like the two branch types. Any new warning
fails this job per its filter, so an explicit (id) cast on the protocol side
unifies the branches without changing behavior.

Verified locally: xcodebuild ... analyze under the same warning filters CI
uses exits 0, and no warning is reported at this line (previously present).

#agentic

* fix: identify sideloaded kits by code, not by a soon-to-be-nil wrapperInstance

testSwitchWorkspaceSideloadedKits went from passing to failing 4/4 on this
PR's CI, on both platforms, always the same way: after switching workspaces,
registeredKits.count stayed at 2 instead of dropping to 1, and anyOTAobject's
wrapperInstance was nil instead of the new kit instance. That is a regression
from the previous commit, not the pre-existing flakiness this branch already
carries elsewhere.

removeAllSideloadedKits identified sideloaded registers by asking
[kitRegister.wrapperInstance respondsToSelector:@selector(sideloadedKitCode)].
Both call sites that matter - resetForSwitchingWorkspaces: and reset:, in
mParticle.m - call flushSerializedKits immediately before removeAllSideloadedKits,
every time. The previous commit made flushSerializedKits detach wrapperInstance
to nil synchronously, before it returns, specifically so the detach happens
under kitsSemaphore and closes the race with activeKitsRegistry. That also
means wrapperInstance is already nil by the time removeAllSideloadedKits runs
right after it - [nil respondsToSelector:] is NO, so the check stopped matching
anything, and stale sideloaded kits from the previous workspace were never
removed from kitsRegistry.

initWithInstance:kitCode: assigns every sideloaded kit a code starting at
sideloadedKitCodeStartValue (1e9), on the register itself, independent of
wrapperInstance and unaffected by the detach. Switched the check to that.
removeKitsFromRegistryInvalidForWorkspaceSwitch has the same
wrapperInstance-dependent shape, but it runs before flushSerializedKits at
both call sites, so it is not affected by this ordering and is left alone.

testSwitchWorkspaceSideloadedKits: 19/20 (the one failure was a different,
earlier assertion - "Sideloaded kit was not registered" on the very first
wait, before any switch happens - not this regression's signature).
testActiveKitsRegistryThreadSafety: 0 crashes in 20, unaffected by this change.
MParticleTests + MPKitContainerTests together x4: 140/140, 0 failures each run.

#agentic

* fix: defer freeKitRegister's kit teardown off kitsSemaphore

PR #878/#916's own new stress test, testActiveKitsRegistryThreadSafety,
crashed CI 2/2 times (once on iOS, once on tvOS, both restarts mid-test)
while passing 60/60 isolated local runs plus 3/3 full-suite local runs,
both before and after this fix - consistent with a rare, CI-hardware-
specific timing window rather than something reproducible on demand.

Auditing the locked paths this test exercises turned up a real
inconsistency with the architecture the rest of this stack established.
flushSerializedKits (895696a, a40a516) deliberately detaches
wrapperInstance to nil under kitsSemaphore, then defers stop(), disk
cleanup and the mParticleKitDidBecomeInactiveNotification post to
dispatch_async(main) - specifically so that arbitrary kit/observer code
never runs while every other thread is blocked on the lock.

freeKitRegister:integrationId: - reached via configureKits:'s
deactivateKits cleanup, which this exact stress test's writer thread
exercises on every iteration - detaches wrapperInstance the same way,
but then called teardownDetachedWrapperInstance: synchronously, still
holding kitsSemaphore. That both stalls every reader thread for however
long stop()/disk I/O takes, and risks an outright deadlock if any
observer of the notification calls back into a kitsSemaphore-guarded
method, since dispatch_semaphore_t is not reentrant.

Deferred the same way flushSerializedKits does. The detach itself stays
inline under the lock, since that is what synchronizes with
isActiveAndNotDisabled:'s reads on other threads.

Validated: MPKitContainerTests class x20 and full local suite x3 (both
modes previously used to validate this stack), 0 failures, 0 crash
restarts. This does not reproduce the CI-only crash locally to confirm
root cause directly - filed as the most concrete, evidence-backed lead
found by auditing every locked path the failing test touches.

#agentic
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* chore: (release) 9.4.3

Updates version to 9.4.3 across the mParticle ecosystem.

* Update CHANGELOG for Rokt kit changes

Updated CHANGELOG to reflect recent fixes and additions for the Rokt kit.

---------

Co-authored-by: Thomson Thomas <125323226+thomson-t@users.noreply.github.com>
fix(release): publish correct notes and report signing status

The publish workflow treated unreleased changelog entries as release
notes, causing stale changes to appear while omitting the current
release.

Use the latest versioned changelog section for core and kit releases,
remove the stale unreleased entries, and print deep code-signing
verification without changing the script’s successful exit behavior.

#agentic
…kstation-swift-ebb64b

# Conflicts:
#	CHANGELOG.md
#	Framework/Info.plist
#	Kits/adjust/adjust-5/mParticle-Adjust-5.podspec
#	Kits/adobe/adobe-5/mParticle-Adobe-5.podspec
#	Kits/appsflyer/appsflyer-6/mParticle-AppsFlyer-6.podspec
#	Kits/apptentive/apptentive-6/mParticle-Apptentive-6.podspec
#	Kits/apptentive/apptentive-7/mParticle-Apptentive-7.podspec
#	Kits/apptimize/apptimize-3/mParticle-Apptimize-3.podspec
#	Kits/branchmetrics/branchmetrics-3/Sources/mParticle-BranchMetrics/MPKitBranchMetrics.m
#	Kits/branchmetrics/branchmetrics-3/mParticle-BranchMetrics-3.podspec
#	Kits/braze/braze-12/mParticle-Braze-12.podspec
#	Kits/braze/braze-13/mParticle-Braze-13.podspec
#	Kits/braze/braze-14/mParticle-Braze-14.podspec
#	Kits/braze/braze-17/mParticle-Braze-17.podspec
#	Kits/clevertap/clevertap-7/Sources/mParticle-CleverTap/MPKitCleverTap.m
#	Kits/clevertap/clevertap-7/mParticle-CleverTap-7.podspec
#	Kits/comscore/comscore-6/mParticle-ComScore-6.podspec
#	Kits/google-analytics-firebase-ga4/firebase-ga4-11/mParticle-FirebaseGA4-11.podspec
#	Kits/google-analytics-firebase-ga4/firebase-ga4-12/mParticle-FirebaseGA4-12.podspec
#	Kits/google-analytics-firebase/firebase-11/mParticle-Firebase-11.podspec
#	Kits/google-analytics-firebase/firebase-12/mParticle-Firebase-12.podspec
#	Kits/iterable/iterable-6/mParticle-Iterable-6.podspec
#	Kits/leanplum/leanplum-6/mParticle-Leanplum-6.podspec
#	Kits/localytics/localytics-6/mParticle-Localytics-6.podspec
#	Kits/localytics/localytics-7/mParticle-Localytics-7.podspec
#	Kits/onetrust/onetrust/mParticle-OneTrust.podspec
#	Kits/optimizely/optimizely-4/mParticle-Optimizely-4.podspec
#	Kits/optimizely/optimizely-5/mParticle-Optimizely-5.podspec
#	Kits/radar/radar-3/mParticle-Radar-3.podspec
#	Kits/rokt-sdk-plus/rokt-sdk-plus-ios/Package.swift
#	Kits/rokt-sdk-plus/rokt-sdk-plus-ios/RoktSDKPlus.podspec
#	Kits/rokt-sdk-plus/rokt-sdk-plus-ios/Sources/RoktSDKPlus/RoktSDKPlus.swift
#	Kits/rokt/rokt/Sources/mParticle-Rokt/MPKitRokt.m
#	Kits/rokt/rokt/mParticle-Rokt.podspec
#	Kits/singular/singular-12/mParticle-Singular-12.podspec
#	Kits/urbanairship/urbanairship-19/mParticle-UrbanAirship-19.podspec
#	Kits/urbanairship/urbanairship-20/mParticle-UrbanAirship-20.podspec
#	UnitTests/ObjCTests/MPConnectorTests.m
#	UnitTests/ObjCTests/MPRoktTests.m
#	UnitTests/ObjCTests/MParticleTests.m
#	VERSION
#	mParticle-Apple-SDK-ObjC.podspec
#	mParticle-Apple-SDK-Swift.podspec
#	mParticle-Apple-SDK.podspec
#	mParticle-Apple-SDK/Include/MPKitContainer.h
#	mParticle-Apple-SDK/Kits/MPKitContainer.m
#	mParticle-Apple-SDK/MPIConstants.m
#	mParticle-Apple-SDK/Network/MPConnector.m
…ce-detach fix

flushSerializedKits (ported from main's #878/#916 during the merge) now detaches
wrapperInstance to nil synchronously before removeAllSideloadedKits runs, so its
old check (wrapperInstance respondsToSelector:sideloadedKitCode) stopped matching
anything - exactly the regression #916 already fixed on main for the same reason.
Identify sideloaded registers by their assigned code instead, which survives the
detach.
@cursor

cursor Bot commented Sep 9, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Changes core kit lifecycle during workspace switch/reset and release XCFramework signing; regressions could cause kit overlap, missed sideload cleanup, or distribution signing issues.

Overview
Ports main’s kit-registry concurrency and workspace-switch teardown fixes into the Swift-migrated MPKitContainerExecutionAdapter path, and bumps the ecosystem to 9.4.3 with matching changelog and release workflow tweaks.

Kit container: Adds a dedicated bracketsSemaphore so bracket updates don’t deadlock inside configureKits. Kit stop/cleanup is deferred off kitsSemaphore via detached wrapper teardown on the main queue, coordinated by a static kitTeardownGroup on MPKitContainer_PRIVATE. reset / resetForSwitchingWorkspaces now wait on notifyWhenKitTeardownComplete before completing so a new workspace’s kits don’t start while the old ones are still stopping. removeAllSideloadedKits identifies sideloaded kits by integration code (≥ sideload threshold) instead of wrapperInstance respondsToSelector:, which broke after earlier wrapperInstance nil’ing.

Release artifacts: make_artifacts.sh code-signs the nested mParticle_Apple_SDK_Swift.framework inside each XCFramework slice before signing the outer framework and wrapper, with --force and deep verify.

Versioning / CI: VERSION, plist, podspecs, and kit version strings → 9.4.3; changelog entries for nested XCFramework signing; release workflow drops prerelease: true on release-note extraction.

Tests: Tightens workspace-switch tests to wait on initialized; hardens bracket stress-test failure reporting; moves GoDaddy cert-pinning coverage to Swift MPConnectorTests; trims obsolete ObjC connector tests; Rokt async test timeouts/drain helper.

Reviewed by Cursor Bugbot for commit 500b1ea. Bugbot is set up for automated code reviews on this repo. Configure here.

@coderabbitai

coderabbitai Bot commented Sep 9, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 76b48c3c-5ea8-4b6d-853f-b7f71de63ef3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

🐦 Swift Migration Progress

Production implementation code at 500b1ea666b6 compared with 176ac3a43378.

Area Goal Progress Base This PR Swift SLOC Objective-C remaining Change
Core SDK Short term — in scope ██████▍░░░ 63.94% 63.94% 10,926 6,161 ➖ 0.00 pp
Core SDK Long term — all Objective-C ████▊░░░░░ 47.16% 47.16% 10,926 12,240 ➖ 0.00 pp
SDK kit infrastructure Short term — in scope ███████▍░░ 73.36% 73.45% 2,031 734 🚀 +0.09 pp
SDK kit infrastructure Long term — all Objective-C ████░░░░░░ 39.96% 39.82% 2,031 3,070 ↩️ -0.14 pp
Standalone kits Short term — in scope ▋░░░░░░░░░ 5.84% 5.84% 864 13,921 ➖ 0.00 pp
Standalone kits Long term — all Objective-C ▋░░░░░░░░░ 5.84% 5.84% 864 13,921 ➖ 0.00 pp

Objective-C retained by design: Core SDK 6,079 · SDK kit infrastructure 2,336 (+33) · Standalone kits 0.

This PR's code movement

Area Swift lines added Objective-C lines removed
Core SDK 0 3
SDK kit infrastructure 26 29
Standalone kits 1 2
How this is measured
  • Current composition uses production source lines of code (SLOC) from cloc; comments and blank lines are excluded.
  • Short term — in scope excludes the Objective-C the migration will not delete, so 100% is the end of this project: every in-scope implementation gone.
  • Long term — all Objective-C keeps the full denominator. Reaching 100% there means the public API itself becomes Swift, which is a breaking change reserved for a future major release.
  • The gap between the two rows is the retained public/kit contract, runtime-identity, and boundary-glue surface listed in Tools/swift-migration-retained-objc.txt.
  • Retained wrappers keep their Objective-C interface but still shed logic to Swift. That thinning moves the long-term row and the retained figure, not the short-term row.
  • Both revisions are measured with the manifest from the head revision, so a manifest edit does not by itself move the reported change. A retained file this pull request renamed or deleted still counts as retained at the base.
  • Pull request movement uses physical additions/deletions from git diff base...head --numstat; it counts retained files too and is intentionally separate from SLOC totals.
  • Core excludes SDK kit infrastructure and vendored libraries. Standalone kits include only files below Kits/**/Sources.
  • Tests, examples, headers, build outputs, vendored libraries, and the MParticle/Sources Swift overlay are excluded.
  • Objective-C++ (.mm) is included in the Objective-C figures and removed counts.

Generated with cloc 2.10. This report is informational and does not gate migration direction.

@nickolas-dimitrakas nickolas-dimitrakas self-assigned this Sep 9, 2026
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

📦 SDK Size Impact Report

Measures how much the SDK adds to an app's size (with-SDK minus without-SDK).

Metric Target Branch This PR Change
App Bundle Impact 2.59 MB 2.59 MB +N/A
Executable Impact 848 bytes 848 bytes +N/A
XCFramework Size 6.84 MB 6.85 MB +8 KB

➡️ SDK size impact change is minimal.

Raw measurements

Target branch (workstation/swift-migration):

{"baseline_app_size_kb":84,"baseline_executable_size_bytes":75464,"with_sdk_app_size_kb":2740,"with_sdk_executable_size_bytes":76312,"sdk_impact_kb":2656,"sdk_executable_impact_bytes":848,"xcframework_size_kb":7004}

This PR:

{"baseline_app_size_kb":84,"baseline_executable_size_bytes":75464,"with_sdk_app_size_kb":2740,"with_sdk_executable_size_bytes":76312,"sdk_impact_kb":2656,"sdk_executable_impact_bytes":848,"xcframework_size_kb":7012}

@nickolas-dimitrakas
nickolas-dimitrakas merged commit 033f721 into workstation/swift-migration Sep 10, 2026
156 of 161 checks passed
@nickolas-dimitrakas
nickolas-dimitrakas deleted the fix/merge-main-into-workstation-swift-migration branch September 10, 2026 16:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants