Skip to content

Commit f13adf0

Browse files
authored
Merge pull request #713 from session-foundation/dev
Release 2.15.4
2 parents 60b12fb + 1ad6932 commit f13adf0

38 files changed

Lines changed: 5978 additions & 11712 deletions

‎.gitignore‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ DerivedData
2828
*.xcuserstate
2929
Index/
3030

31+
# Release credentials (never commit)
32+
.env
33+
*.p8
34+
*.p12
35+
3136
# CocoaPods
3237
Pods
3338

‎BUILDING.md‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,27 @@ The database for the app is stored within an `App Group` directory which is base
6464

6565
### Push Notifications
6666
Features related to push notifications are known to be not working for third-party contributors since Apple's Push Notification service pushes will only work with the Session production code signing certificate.
67+
68+
## Building the app for the Appium regression suite
69+
70+
End-to-end UI regression tests live in the **`session-appium`** repo (checked out alongside
71+
this one as `Session_Appium`), which drives a built `Session.app` through Appium. Harness
72+
setup, `.env` config, simulator creation, and the run commands are documented there
73+
(`Session_Appium/CLAUDE.md`) — that's the source of truth for running the suite. The only
74+
iOS-repo responsibility is producing the `.app` it installs:
75+
76+
- Build a **simulator** `.app` (not a device build). A **Debug** build is recommended
77+
because the launch-arg test instrumentation used by the suite is compiled only under
78+
`#if targetEnvironment(simulator)` and surfaced via
79+
`DeveloperSettingsViewModel.processUnitTestEnvVariablesIfNeeded`.
80+
81+
```sh
82+
xcodebuild build -project Session.xcodeproj -scheme Session \
83+
-configuration Debug -destination 'generic/platform=iOS Simulator' \
84+
-derivedDataPath build
85+
# → build/Build/Products/Debug-iphonesimulator/Session.app
86+
```
87+
88+
- Point the suite's `IOS_APP_PATH_PREFIX` at that `.app`. Using `-derivedDataPath` (above)
89+
keeps the path stable across rebuilds, so you only set it once. After a code change,
90+
rebuild and re-run the suite — no `.env` change needed.

‎CLAUDE.md‎

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# CLAUDE.md — Session iOS
2+
3+
Operational guide for AI agents working in this repo. For the "what and why" of the
4+
system (module responsibilities, config sync, networking, message pipeline, Pro
5+
subsystem, end-to-end flows), read **[ARCHITECTURE.md](ARCHITECTURE.md)** — it is
6+
thorough and current. This file covers **how to work here**: build, test, conventions,
7+
and gotchas. Don't duplicate architecture content into here.
8+
9+
## Project shape
10+
11+
- Xcode project: `Session.xcodeproj` (no CocoaPods/Pods despite `.gitignore` entries).
12+
- In-project Swift frameworks + app target + 2 extensions. Dependency graph flows
13+
strictly downward — see ARCHITECTURE.md §5.2. Never introduce an upward import
14+
(e.g. `SessionUtilitiesKit` must not import `SessionMessagingKit`).
15+
- Minimum deployment target iOS 15; builds against the iOS 26 SDK.
16+
- libSession (the C/C++ core, repo `LibSession-Util`) is consumed via SPM and imported
17+
in Swift as **`import SessionUtil`**.
18+
19+
## Schemes
20+
21+
| Scheme | Use |
22+
|---|---|
23+
| `Session` | Main app + the scheme used for building and running all tests. |
24+
| `Session_CompileLibSession` | Same app, but builds libSession **from source** instead of the prebuilt SPM binary. Use this when your change touches the C/C++ layer in `../LibSession-Util`. Requires `brew install cmake m4 pkg-config` and a correctly pointed `xcode-select` (see BUILDING.md). Point `LIB_SESSION_SOURCE_DIR` at the source (defaults to `${SOURCE_DIR}/../LibSession-Util`). |
25+
| Per-framework schemes | `SessionMessagingKit`, `SessionNetworkingKit`, `SessionUIKit`, `SessionUtilitiesKit`, `SignalUtilitiesKit`, `TestUtilities`, and the two extensions — build a single module in isolation. |
26+
27+
Build configurations: `Debug` and `App_Store_Release` (plus `Debug_Compile_LibSession`
28+
/ `App_Store_Release_Compile_LibSession` variants used by the compile-from-source scheme).
29+
30+
## Building & testing (local)
31+
32+
Prefer a plain `xcodebuild` invocation locally. Pick any installed simulator for the
33+
destination (`xcrun simctl list devices available`).
34+
35+
Build:
36+
```sh
37+
xcodebuild build \
38+
-project Session.xcodeproj \
39+
-scheme Session \
40+
-destination 'platform=iOS Simulator,name=iPhone 16'
41+
```
42+
43+
Run a single test module (much faster than the whole suite):
44+
```sh
45+
xcodebuild test \
46+
-project Session.xcodeproj \
47+
-scheme Session \
48+
-destination 'platform=iOS Simulator,name=iPhone 16' \
49+
-only-testing:SessionUtilitiesKitTests
50+
```
51+
52+
Notes:
53+
- `Scripts/build_ci.sh test` exists but is **CI-flavoured** (bounces the simulator
54+
between suites, retries, parses xcresults, needs a passed-in simulator UUID). Use it
55+
only to reproduce CI behaviour, not for routine local runs.
56+
- CI (Drone, `.drone.jsonnet`) builds under `App_Store_Release` and runs four suites:
57+
`SessionTests`, `SessionUtilitiesKitTests`, `SessionNetworkingKitTests`,
58+
`SessionMessagingKitTests`.
59+
- Test targets exist per module. Shared mocking/fixtures/database helpers live in the
60+
`TestUtilities` target.
61+
62+
### End-to-end / regression tests (Appium)
63+
64+
Automated UI regression tests live in a **separate repository**, checked out alongside
65+
this one as `Session_Appium` (repo: `session-appium`). They drive the built app through
66+
Appium using Playwright as the runner, and cover both iOS and Android from one codebase
67+
(specs are tagged `@ios` / `@android`). The in-repo Quick/Nimble suites do not cover
68+
end-to-end UI flows — for regression coverage of a user-facing change, the corresponding
69+
Appium tests need to run from that repo (e.g. `pnpm test-ios`). If a change warrants
70+
regression testing, note that the Appium suite should be triggered there.
71+
72+
To run the suite locally: build the instrumented simulator `.app` (see **BUILDING.md →
73+
"Building the app for the Appium regression suite"**), then follow `Session_Appium/CLAUDE.md`
74+
for harness setup and the run commands.
75+
76+
## Testing conventions
77+
78+
- Tests are **Quick + Nimble** BDD specs, not XCTest. A test file is a `QuickSpec`
79+
subclass overriding `spec()`, structured with `describe` / `context` / `it`, and
80+
annotated with `// MARK: -`, `// MARK: --`, `// MARK: ----` comments mirroring the
81+
nesting depth. Match this style when adding tests.
82+
- Components are tested in isolation by constructing a `Dependencies` container with
83+
mock implementations — no global singleton patching. Follow the existing pattern
84+
rather than reaching for real services.
85+
86+
## Code style & conventions
87+
88+
- **Prefer refactoring over duplication.** If you need behaviour that already exists,
89+
extract/reuse it rather than copy-pasting. Pull shared logic into the appropriate
90+
lower-level module rather than repeating it across call sites.
91+
- **File header** (top of every new Swift file):
92+
`// Copyright © <year> Rangeproof Pty Ltd. All rights reserved.`
93+
- **C++** is formatted with `.clang-format` (WebKit base, 120-column, no tabs).
94+
- A `.swiftlint.yml` exists but SwiftLint is **not actually run** on this project — don't
95+
rely on it as a lint gate or spend effort satisfying it.
96+
- SwiftUI-migrated types sometimes use a temporary `{name}_SwiftUI` suffix to coexist
97+
with their UIKit predecessor during the ongoing UIKit→SwiftUI migration (ARCHITECTURE.md
98+
§16.1, §23). This is a known transitional naming convention, not a mistake to "fix".
99+
- Match the surrounding code's idioms in each module; they differ in age and style.
100+
101+
## Patterns you must respect (see ARCHITECTURE.md for detail)
102+
103+
- **Config-first shared state.** Anything that should sync across a user's devices is
104+
authored in the libSession config object first, then projected into GRDB for querying —
105+
not written directly to SQLite. (§7.2, §8)
106+
- **JobRunner is a reliability primitive.** Work that must eventually complete across
107+
restarts/outages (send, upload, download, config sync) is modelled as a persistent job.
108+
Don't bypass it for such work. (§7.3)
109+
- **ObservationManager drives UI reactivity** (progressively replacing GRDB
110+
`ValueObservation`). If UI isn't updating, check the relevant `ObservableKey` is emitted
111+
and observed. (§7.1)
112+
- **Dependencies DI.** Services are resolved from an injected `Dependencies` container,
113+
not singletons. (§7.4, §17)
114+
115+
## Git / contribution flow
116+
117+
- `dev` is the active integration branch; `master` reflects the current production
118+
release. Releases are cut by merging `dev` → `master`, tagging, and releasing.
119+
- Branch feature work off **`dev`** and merge back into `dev`; conflicts are resolved at
120+
merge time.
121+
- Follow the repo default: commit/push or open PRs only when explicitly asked — leave
122+
git operations to the maintainer otherwise.
123+
124+
## Gotchas
125+
126+
- **Third-party signing / push:** push-notification features only work with the
127+
production signing cert; third-party contributors can't exercise them (BUILDING.md).
128+
- **App Group container:** the app identifier / App Group is extracted into `Info.plist`
129+
by a build phase; if that fails, the fallback lives in
130+
`SessionUtilitiesKit/Types/UserDefaultsType` (`UserDefaults.applicationGroup`).
131+
- **`0xDEAD10CC` / DB relocation:** the database currently lives in the App Group
132+
container, which risks the background file-access crash. A migration to the Documents
133+
directory is in progress and the extensions are being "de-databased" (ARCHITECTURE.md
134+
§23). Be careful adding new direct DB access from extensions.
135+
- **libSession changes:** if you modify anything requiring a libSession source rebuild,
136+
switch to the `Session_CompileLibSession` scheme — the default scheme uses the prebuilt
137+
SPM binary and won't pick up local C/C++ edits.

‎RELEASING.md‎

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Releasing Session iOS
2+
3+
A small suite of scripts under `Scripts/` builds a distribution IPA and submits it to
4+
TestFlight without opening Xcode. They run locally (a maintainer just runs them) and are
5+
CI-ready (all credentials come from environment variables). No Fastlane.
6+
7+
## Prerequisites
8+
9+
- Xcode + command-line tools, `xcbeautify`, and the GitHub CLI (`gh`, authenticated).
10+
- An **App Store Connect API key** (`.p8`) — see below. For cloud signing it must be an
11+
**Admin Team Key** (an Individual key, or an App Manager key, will not work).
12+
13+
Signing uses **cloud-managed certificates** (the default when a team sets signing up through
14+
Xcode — Apple holds the distribution private key). With the Admin API key and
15+
`-allowProvisioningUpdates`, `xcodebuild` cloud-signs the distribution build without any
16+
local certificate. **You do not need to create or export a `.p12`.** Providing one is only a
17+
fallback for teams not using cloud-managed signing (see the last section).
18+
19+
## Obtaining credentials
20+
21+
### App Store Connect API key
22+
Create it in App Store Connect → **Users and Access → Integrations**. Two requirements,
23+
both confirmed necessary in practice for the cloud-signed release:
24+
25+
- It must be a **Team Key** (the "Team Keys" tab), **not an Individual Key**. An Individual
26+
key — even one owned by an Admin user — was rejected for cloud signing (export failed with
27+
`Cloud signing permission error` and xcodebuild fell back to a local Apple ID account).
28+
- The key must have the **Admin** role. App Manager is not sufficient for cloud signing /
29+
distribution-certificate operations (same `Cloud signing permission error`). App Manager is
30+
only enough if you sign with a local `.p12` (see the fallback section).
31+
32+
Then **Download** the `.p8` (only downloadable once). Note the **Key ID** (10 chars) and the
33+
**Issuer ID** (UUID) shown on that page.
34+
- Apple: [Creating API keys for the App Store Connect API](https://developer.apple.com/documentation/appstoreconnectapi/creating-api-keys-for-app-store-connect-api)
35+
- Apple: [App Store Connect API — Get started](https://developer.apple.com/help/app-store-connect/get-started/app-store-connect-api/)
36+
37+
### Cloud-managed signing certificate
38+
Nothing to export — it is managed remotely by Apple and used automatically during the
39+
cloud-signed archive/export. Background:
40+
- Apple: [Cloud-managed certificates](https://developer.apple.com/help/account/certificates/cloud-managed-certificates/)
41+
- WWDC21: [Distribute apps in Xcode with cloud signing](https://developer.apple.com/videos/play/wwdc2021/10204/)
42+
43+
## Environment variables
44+
45+
| Variable | Description |
46+
|---|---|
47+
| `ASC_KEY_ID` | 10-char App Store Connect API key ID |
48+
| `ASC_ISSUER_ID` | App Store Connect issuer UUID |
49+
| `ASC_KEY_P8_BASE64` | base64 of the `.p8` private key |
50+
| `SKIP_KEYCHAIN=1` | **use cloud-managed signing** — no local certificate/keychain. This is the normal path for this project. |
51+
| `ASC_DIST_CERT_P12_BASE64` | *(fallback only)* base64 of an Apple Distribution `.p12` (incl. private key) |
52+
| `ASC_DIST_CERT_PASSWORD` | *(fallback only)* the `.p12` export password |
53+
| `KEYCHAIN_PASSWORD` | *(fallback only, optional)* temp-keychain password (generated if unset) |
54+
55+
To base64-encode the key:
56+
57+
```sh
58+
base64 -i AuthKey_XXXXXXXXXX.p8 | pbcopy # -> ASC_KEY_P8_BASE64
59+
```
60+
61+
All secrets are written to restricted temp locations and deleted on exit (success,
62+
failure, or Ctrl-C) by a trap in `Scripts/release_env.sh`.
63+
64+
## How versioning fits the branch model
65+
66+
Version numbers live at the **project level** in `project.pbxproj` (inherited by the app +
67+
extension targets). A version bump is a normal change that lands on **`dev`** and reaches
68+
**`master`** through the reviewed `dev → master` merge — the release scripts **never** change
69+
the version. Bumping is a separate step (`bump_version.sh`), and a release just reads whatever
70+
version is already on `master` and tags it.
71+
72+
### Bumping the version (on dev)
73+
74+
```sh
75+
Scripts/bump_version.sh 2.15.4 # optional 2nd arg sets the build number (default +1)
76+
```
77+
78+
This branches off `dev`, bumps the project-level `MARKETING_VERSION` / `CURRENT_PROJECT_VERSION`,
79+
pushes, and opens a PR into `dev` for review. Once merged and promoted to `master`, release it.
80+
81+
## Quick start (one command)
82+
83+
```sh
84+
Scripts/release.sh # reads the version already on master; add a version to sanity-check
85+
```
86+
87+
`release.sh` orchestrates the release: it loads credentials from `.env` if present, prompts
88+
for anything missing (offering to save them), reads the version on `master`, checks the tag,
89+
prints a plan, and pauses before each outward-facing step. It runs:
90+
91+
1. `prepare_github_release.sh` — create the tag + GitHub draft release on `master` (no version change)
92+
2. `build_release.sh --attach` — build, sign, export `session-<version>.ipa`, attach to the draft
93+
3. `testflight_upload.sh` — upload the IPA to TestFlight
94+
95+
**Tag gating:** if `master`'s version has **no** tag yet, it creates the tag + draft and
96+
proceeds. If a tag for that version **already exists** (i.e. the version hasn't been bumped
97+
since the last release), it pauses so you can (1) re-check after merging a bump, (2) build and
98+
submit with the current version anyway, or (3) abort.
99+
100+
Then it prints (and offers to open) the GitHub releases page and App Store Connect for the two
101+
remaining manual actions: publishing the GitHub release and managing/submitting in TestFlight.
102+
103+
Useful flags: `-y/--yes` (no prompts, for CI), `--allow-existing-version` (build even if the
104+
tag exists), `--skip-upload`, `--skip-draft`. Env: `RELEASE_REMOTE` (default `origin`),
105+
`RELEASE_BRANCH` (default `master`). Run `Scripts/release.sh --help` for details.
106+
107+
### Using a `.env`
108+
109+
Put credentials in a `.env` at the repo root (git-ignored — never committed) so you don't
110+
re-enter them each time. First interactive run offers to create it for you. Format:
111+
112+
```sh
113+
ASC_KEY_ID="XXXXXXXXXX"
114+
ASC_ISSUER_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
115+
ASC_KEY_P8_BASE64="<base64 of AuthKey_XXXXXXXXXX.p8>"
116+
SKIP_KEYCHAIN=1
117+
```
118+
119+
## Running the steps individually
120+
121+
The orchestrator just calls these; run them directly if you need finer control. Credentials
122+
come from the environment or `.env` (`build_release.sh` and `testflight_upload.sh` auto-load
123+
`.env` via `release_env.sh`; set them explicitly if you're not using a `.env`):
124+
125+
```sh
126+
export ASC_KEY_ID="XXXXXXXXXX"
127+
export ASC_ISSUER_ID="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
128+
export ASC_KEY_P8_BASE64="$(base64 -i AuthKey_XXXXXXXXXX.p8)"
129+
export SKIP_KEYCHAIN=1
130+
131+
Scripts/prepare_github_release.sh 2.15.4 # create tag + draft release on master
132+
Scripts/build_release.sh 2.15.4 --attach # version defaults to the project's if omitted
133+
Scripts/testflight_upload.sh 2.15.4
134+
gh release edit 2.15.4 --draft=false # publish the GitHub release when ready
135+
```
136+
137+
Outputs:
138+
- `build/Session.xcarchive` — the archive, including `dSYMs/` for crash symbolication.
139+
- `build/export/session-<version>.ipa` — the signed, App-Store-ready IPA (naming matches
140+
the historical builds in `~/Projects/Builds`).
141+
142+
## Notes
143+
144+
- **Version numbers** are set at the project level in `project.pbxproj` and inherited by
145+
the app + extension targets. `bump_version.sh` changes only those project-level values (on
146+
`dev`); the release scripts never modify them.
147+
- **dSYMs**: `App_Store_Release` defaults to `DEBUG_INFORMATION_FORMAT = dwarf`; the device
148+
archive (`build_ci.sh archive-device`) overrides this to `dwarf-with-dsym` so symbols are
149+
produced and uploaded (`uploadSymbols` in `exportOptions.plist`).
150+
- **CI**: there is intentionally no Drone deploy pipeline yet (matching Session Android).
151+
The scripts already read everything from env vars, so wiring them into CI later is just a
152+
matter of injecting the secrets above.
153+
- `Scripts/testflight_upload.sh` is decoupled so you can re-upload an existing IPA without
154+
re-archiving. It uses `xcrun altool`; if Apple removes `altool`, switch it to
155+
`exportOptions.plist` `destination=upload` (drops the local artifact) or Transporter.
156+
157+
## Fallback: manual `.p12` signing (not needed for this project)
158+
159+
If a team is *not* using cloud-managed signing, provide an Apple Distribution certificate
160+
instead of setting `SKIP_KEYCHAIN=1`: export the identity from Keychain Access as a `.p12`
161+
(with its private key), then set `ASC_DIST_CERT_P12_BASE64` (=`base64 -i cert.p12`) and
162+
`ASC_DIST_CERT_PASSWORD`. `release_env.sh` imports it into a temporary keychain for the build
163+
and deletes it on exit. This project uses cloud signing, so this path should not be required.

‎Scripts/build_ci.sh‎

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ IFS=$' \t\n'
55
set -euo pipefail
66

77
if [ $# -lt 1 ]; then
8-
echo "Error: Missing mode. Usage: $0 [test|archive] [unique_xcodebuild_args...]"
8+
echo "Error: Missing mode. Usage: $0 [test|archive|archive-device] [unique_xcodebuild_args...]"
99
exit 1
1010
fi
1111

@@ -254,7 +254,40 @@ elif [[ "$MODE" == "archive" ]]; then
254254
"${UNIQUE_ARGS[@]}" 2>&1 | xcbeautify --is-ci
255255
fi
256256

257+
elif [[ "$MODE" == "archive-device" ]]; then
258+
259+
echo "--- Running Device Archive Build for Distribution (App_Store_Release) ---"
260+
261+
# Distribution-signed device archive for App Store / TestFlight.
262+
# Device archive for distribution. We rely on the project's existing automatic
263+
# signing (CODE_SIGN_STYLE=Automatic, DEVELOPMENT_TEAM=SUQ8J2PCT7) plus
264+
# -allowProvisioningUpdates + the App Store Connect API key (passed by
265+
# build_release.sh as UNIQUE_ARGS). The distribution identity is resolved
266+
# automatically at export time (method: app-store-connect) via the cloud-managed
267+
# certificate — do NOT force CODE_SIGN_IDENTITY here: a global override is applied
268+
# to every target, including the automatically-signed SPM packages, which then fail
269+
# with "conflicting provisioning settings". We only override DEBUG_INFORMATION_FORMAT
270+
# so the archive emits dSYMs (the App_Store_Release default is plain dwarf).
271+
DEVICE_ARCHIVE_ARGS=(
272+
-destination "generic/platform=iOS"
273+
-sdk iphoneos
274+
-allowProvisioningUpdates
275+
DEBUG_INFORMATION_FORMAT=dwarf-with-dsym
276+
)
277+
278+
if [[ "$USE_RAW_LOGS" -eq 1 ]]; then
279+
NSUnbufferedIO=YES xcodebuild archive \
280+
"${COMMON_ARGS[@]}" \
281+
"${DEVICE_ARCHIVE_ARGS[@]}" \
282+
"${UNIQUE_ARGS[@]}" 2>&1
283+
else
284+
NSUnbufferedIO=YES xcodebuild archive \
285+
"${COMMON_ARGS[@]}" \
286+
"${DEVICE_ARCHIVE_ARGS[@]}" \
287+
"${UNIQUE_ARGS[@]}" 2>&1 | xcbeautify --is-ci
288+
fi
289+
257290
else
258-
echo "Error: Invalid mode '$MODE' specified. Use 'test' or 'archive'."
291+
echo "Error: Invalid mode '$MODE' specified. Use 'test', 'archive', or 'archive-device'."
259292
exit 1
260293
fi

0 commit comments

Comments
 (0)