Skip to content
This repository was archived by the owner on Jun 19, 2026. It is now read-only.

chore: source posthog-ios via SPM as opt-in#81

Merged
turnipdabeets merged 15 commits into
mainfrom
chore/posthog-ios-via-spm-dependency
May 26, 2026
Merged

chore: source posthog-ios via SPM as opt-in#81
turnipdabeets merged 15 commits into
mainfrom
chore/posthog-ios-via-spm-dependency

Conversation

@turnipdabeets

@turnipdabeets turnipdabeets commented May 22, 2026

Copy link
Copy Markdown
Contributor

Problem

CocoaPods trunk goes permanently read-only on 2026-12-02. Ahead of that, the posthog-ios team is removing CocoaPods publishing from their release pipeline (tracked in PostHog/posthog-ios#472) and is gated on the two hybrid SDKs (this one and posthog-flutter) being SPM-ready first — otherwise the s.dependency 'PostHog' line in this podspec stops resolving the moment posthog-ios drops its .podspec, and every RN consumer's pod install breaks.

posthog-flutter already ships a Package.swift (PR #346). This PR is the analogous unblock from the React Native side, so the iOS team can proceed.

Changes

1. Opt-in SPM resolution path for posthog-ios (the load-bearing change)

The podspec now reads posthog.useSpm from the consumer's ios/Podfile.properties.json. When that property is "true" and React Native >= 0.75 is in use, posthog-ios is resolved through Swift Package Manager using RN's spm_dependency helper; otherwise the dependency continues to come from CocoaPods trunk via s.dependency 'PostHog', '~> 3.58.1' (byte-identical to today).

podfile_properties_path = File.join(Pod::Config.instance.installation_root.to_s, 'Podfile.properties.json')
podfile_properties = File.exist?(podfile_properties_path) ? (JSON.parse(File.read(podfile_properties_path)) rescue {}) : {}
posthog_use_spm = podfile_properties['posthog.useSpm'].to_s == 'true'

if posthog_use_spm && respond_to?(:spm_dependency, true)
  spm_dependency(s,
    url: 'https://github.com/PostHog/posthog-ios.git',
    requirement: { kind: 'upToNextMinorVersion', minimumVersion: '3.58.1' },
    products: ['PostHog']
  )
else
  s.dependency 'PostHog', '~> 3.58.1'
end

The SPM constraint upToNextMinorVersion: 3.58.1 matches the existing CocoaPods ~> 3.58.1 semantic exactly (>= 3.58.1, < 3.59.0), so the per-release bump cadence for posthog-ios is unchanged.

2. Consumer migration: opting into SPM

// ios/Podfile.properties.json
{
  "posthog.useSpm": "true"
}
USE_FRAMEWORKS=dynamic pod install

The opt-in path requires use_frameworks! :linkage => :dynamic (or the USE_FRAMEWORKS=dynamic env var). This is a documented constraint of RN's spm_dependency helper (RN PR #44627 comment) — Swift modules from SPM packages do not resolve correctly under static linkage from a podspec.

3. Example app upgrade (RN 0.77.3 → 0.79.6)

The opt-in path forces dynamic frameworks. RN 0.77.3 + dynamic frameworks + Xcode 26 has a baseline build issue where React-cxxreact cannot find React-debug's headers (reproduced locally; not introduced by this diff). RN 0.79.6 builds cleanly under dynamic frameworks on the same toolchain, so the example app is scaffolded fresh on RN 0.79.6 to exercise the SPM CI job.

The fresh scaffold uses the new AppDelegate.swift template and pulls in @react-native-community/cli@20. App-level files (App.tsx, react-native.config.js, package.json deps and scripts) are ported forward; the example's App.tsx PostHog usage is unchanged.

4. pnpm hoisted node_modules at the workspace root

.npmrc now sets node-linker=hoisted. pnpm's default symlinked layout (the .pnpm/ store with symlinks into each package's node_modules) causes Xcode "Multiple commands produce" errors when CocoaPods copies React Native ReactCommon headers from the shared react-native node_modules path — multiple Pods all resolve to the same .pnpm/react-native@.../... source path, so Xcode registers duplicate copy commands and fails. The hoisted layout gives bare RN's iOS autolinking and CocoaPods the flat dependency tree they expect.

Without this change, neither build-ios-spm (opt-in + dynamic) nor build-android (on the new RN 0.79.6 scaffold) builds in CI.

5. fmt/base.h patch in the example Podfile post_install

Xcode 26.4+ Clang rejects fmt's FMT_STRING consteval. Patch fmt/base.h in post_install to force FMT_USE_CONSTEVAL=0. Same workaround used in posthog-js's examples/example-expo-53. Upstream issues:

6. New build-ios-spm CI job

Parallel to the existing build-ios-static-lib (default path), the new job exercises the opt-in path with USE_FRAMEWORKS=dynamic and posthog.useSpm: true injected into the example app. Runs on macos-26 with the runner's default Xcode 26.x. Builds for iOS Simulator.

7. Workflow-level permissions: contents: read

Addresses the CodeQL "Actions job or workflow does not limit the permissions of the GITHUB_TOKEN" finding for every job.

How this unblocks us

  • RN >= 0.75 consumers who opt in get posthog-ios via SPM. The moment posthog-ios deletes its .podspec, this path keeps working.
  • All other RN consumers (default static-linked, no opt-in) keep building exactly as today via the CocoaPods trunk dependency. No behavior change.
  • posthog-ios: with posthog-flutter already SPM-ready and this PR landing on the RN side, the hybrid-SDK gate from PostHog/posthog-ios#472 is fully cleared.

Verification

Path CI job Local
Default (CocoaPods, static linkage) build-ios-static-lib
Opt-in (SPM, dynamic frameworks) build-ios-spm
Android build-android n/a

Both CI jobs run on every PR. Local verification used Xcode 26.4.1 with clean DerivedData; the example app builds in both modes from a fresh pnpm install.

Checklist

  • Tests for new code (if applicable) — CI matrix covers both default and opt-in paths
  • Accounted for the impact of any changes across iOS and Android platforms — iOS opt-in is the active change; Android resolution unchanged; both CI jobs pass
  • Accounted for backwards compatibility of any changes (no breaking changes!) — default path is byte-identical to today

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

Adds an spm_dependency branch to the podspec so RN >= 0.75 consumers fetch posthog-ios through Swift Package Manager. CocoaPods trunk fallback retained for RN < 0.75. Unblocks PostHog/posthog-ios#472.
@turnipdabeets turnipdabeets requested a review from a team as a code owner May 22, 2026 18:02
@ioannisj

Copy link
Copy Markdown
Contributor

Thank you for this @turnipdabeets! CI is not happy, though. I assume this worked locally so Xcode versions maybe?

The Pods.xcodeproj written by the React Native spm_dependency helper requires Xcode 16.3+ to parse; macos-15-xlarge picks Xcode 16.4 as latest-stable.
@turnipdabeets

Copy link
Copy Markdown
Contributor Author

Thank you for this @turnipdabeets! CI is not happy, though. I assume this worked locally so Xcode versions maybe?

Yep, tested on Xcode 26.4.1 locally, looks like CI runner has Xcode 16.2 and spm_dependency needs Xcode 16.3+ so bumping the CI runner.

dustinbyrne
dustinbyrne previously approved these changes May 22, 2026
Xcode 26.3's xcodebuild fails to load Pods.xcodeproj entries written by the spm_dependency helper (XCSwiftPackageProductDependency _setSavedArchiveVersion: selector). 26.4 parses the file.
@dustinbyrne

dustinbyrne commented May 22, 2026

Copy link
Copy Markdown
Contributor

CI failure related? I looked into how others were solving this and our solution looks functionally equivalent

edit: I see the comment above

Xcode 16.4 is the macos-15 default and the Xcode shipped alongside the React Native spm_dependency helper (RN 0.75). Xcode 16.2 (macos-14 default) and Xcode 26.0-26.3 fail to load the Pods.xcodeproj produced by the helper.
@dustinbyrne dustinbyrne dismissed their stale review May 22, 2026 20:07

will re-check after CI

The previous commit on this branch made spm_dependency the default for RN >= 0.75. That broke the build-ios-static-lib CI job and would have broken every consumer using static linking, because RN's spm_dependency helper requires use_frameworks! :linkage => :dynamic.

Instead, gate the SPM path on `"posthog.useSpm": "true"` in the consumer's ios/Podfile.properties.json. Default behavior stays on CocoaPods trunk (byte-identical to today). Adds a parallel build-ios-spm CI job that exercises the opt-in path with dynamic frameworks.

Revert the CI runner / Xcode pin commits since the default job runs on the same configuration as before.
Comment thread .github/workflows/ci.yml Fixed
Set `contents: read` at workflow level so every job runs with the minimum necessary GITHUB_TOKEN scope. Addresses the CodeQL "Actions job or workflow does not limit the permissions of the GITHUB_TOKEN" finding.
The opt-in SPM path requires a runner Xcode that simultaneously parses spm_dependency's Pods.xcodeproj output AND compiles RN 0.77's cxxreact sources cleanly. No GitHub-hosted Xcode currently covers both: 16.2 and 26.3 fail the pbxproj load, 16.4 fails the cxxreact compile. Verified locally on Xcode 26.4.1 (not yet on hosted runners). Add CI coverage in a follow-up once the RN/Xcode compatibility window aligns.
Switch the SPM CI job to macos-26 (latest hosted runner, default Xcode), build for iOS Simulator instead of generic ios, and drop the xcpretty wrapper so any compile error surfaces in the log. Mirrors maplibre-react-native's iOS CI, which is a production library successfully shipping SPM-resolved native deps via React Native.
Xcode 26.4+ Clang rejects fmt's consteval-backed FMT_STRING. Patch fmt/base.h in post_install to force FMT_USE_CONSTEVAL=0 so the example app builds on macos-26. Ports the same workaround from posthog-js's example-expo-53 Podfile.
Replace the bare RN 0.77.3 example with a fresh @react-native-community/cli init scaffold on RN 0.79.6. The opt-in SPM path forces dynamic frameworks (an spm_dependency constraint) and RN 0.77.3 + dynamic frameworks + Xcode 26 has baseline build issues (React-debug headers don't expose). RN 0.79.6 builds clean.

Add example/.npmrc with node-linker=hoisted so pnpm provides a flat node_modules layout to bare RN's iOS autolinking; the symlinked layout caused Xcode "Multiple commands produce" errors when Pods copied headers from shared node_modules paths.

Ports forward: workspace dep on session-replay, fmt/base.h FMT_USE_CONSTEVAL patch, react-native.config.js autolinking config, App.tsx PostHog usage in src/.
Set node-linker=hoisted at the workspace root .npmrc. pnpm's default symlinked layout (with .pnpm/) causes Xcode 'Multiple commands produce' errors when CocoaPods copies React Native ReactCommon headers from the shared react-native node_modules path. The hoisted layout gives bare RN iOS autolinking and CocoaPods the flat dependency tree they expect.

Required for the SPM opt-in CI job to build (which forces dynamic frameworks). The default static-linked path also builds clean with the hoisted layout.

Remove the redundant per-package example/.npmrc; the root setting applies workspace-wide.
@ioannisj

Copy link
Copy Markdown
Contributor

What was the CI fix afterall? Does this mean that we will be potentially breaking people's builds?

Comment thread example/android/build.gradle Outdated
@marandaneto

Copy link
Copy Markdown
Member

dont we need also a Package.swift? because technically the spm dep. is still resolved by the podspec file but at some point the whole podspec file should be gone

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left a few comments but prob not blockers

Address review feedback on the RN 0.79.6 scaffold side effects:

- Restore buildToolsVersion 36.0.0, compileSdkVersion 36, targetSdkVersion 36 (the fresh scaffold defaulted to 35, which is a regression from the previous example).
- Move MainActivity.kt and MainApplication.kt back to the original 'posthogreactnativesessionreplay.example' package; restore namespace/applicationId in app/build.gradle. The scaffold defaulted to 'com.posthogreactnativesessionreplayexample' and the rename was unnecessary noise.

Locally verified: iOS default + opt-in paths both '** BUILD SUCCEEDED **'; Android 'pnpm run build:android' 'BUILD SUCCESSFUL'.
@turnipdabeets

turnipdabeets commented May 23, 2026

Copy link
Copy Markdown
Contributor Author

dont we need also a Package.swift? because technically the spm dep. is still resolved by the podspec file but at some point the whole podspec file should be gone

Good question — yes eventually, but not yet, and not in this PR. Two reasons:

  1. RN itself isn't ready to consume libraries via SPM yet: tracked here

  2. posthog-react-native-session-replay's iOS source is 100% RN bridge glue (RCT_EXTERN_MODULE, RCTBridgeModule, RCTPromiseResolveBlock) and RN's iOS modules aren't shipped as SPM packages yet so we can't even declare them as deps cleanly.

The pattern I'm seeing is to offer an opt-in approach to help unblock. See getsentry/sentry-react-native#6182

One thing worth calling out for consumers: opting into SPM today requires use_frameworks! :linkage => :dynamic (a constraint of RN's spm_dependency helper — Swift modules from SPM packages don't resolve under static linkage from a podspec). That's why this PR adds a separate build-ios-spm CI job alongside the existing build-ios-static-lib — to cover both linkage modes.

@turnipdabeets turnipdabeets changed the title chore: source posthog-ios via SPM when available chore: source posthog-ios via SPM as opt-in when available May 23, 2026
@turnipdabeets turnipdabeets changed the title chore: source posthog-ios via SPM as opt-in when available chore: source posthog-ios via SPM as opt-in May 23, 2026
The job-level permissions block previously only granted security-events:write and packages:read. CodeQL's analyze action needs actions:read (to inspect the workflow run) and contents:read (to checkout the source and upload SARIF results). Without these, Perform CodeQL Analysis fails with:

  Please check that your token is valid and has the required permissions:
  contents: read, security-events: write

Matches the canonical permissions block in github/codeql-action README.
@marandaneto

Copy link
Copy Markdown
Member

The pattern I'm seeing is to offer an opt-in approach to help unblock. See getsentry/sentry-react-native#5780

i dont follow this, what should i see from sentry's issue? any new info that is not written down here already? PostHog/posthog-ios#472 or just AI slop?

@turnipdabeets

Copy link
Copy Markdown
Contributor Author

The pattern I'm seeing is to offer an opt-in approach to help unblock. See getsentry/sentry-react-native#5780

i dont follow this, what should i see from sentry's issue? any new info that is not written down here already? PostHog/posthog-ios#472 or just AI slop?

Updated the link in message, it was just an example of one opt-in approach, but there are others. Only to show we're not reinventing the wheel.

The podspec previously had the posthog-ios version literal in two places
(s.dependency '~> 3.58.1' and spm_dependency minimumVersion: '3.58.1').
Hoist into a local 'posthog_ios_version' variable so future bumps are a
single-line edit, matching the manual chore-PR workflow shown by recent
commits #74/#78/etc.
@turnipdabeets turnipdabeets merged commit b2bd035 into main May 26, 2026
17 checks passed
@turnipdabeets turnipdabeets deleted the chore/posthog-ios-via-spm-dependency branch May 26, 2026 20:41
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants