Skip to content

Commit a97c44f

Browse files
committed
ci: create and sign separate bundle ID for PRs
- points to `fix-ios-signing-with-fastlane` branch of `status-jenkins-lib` which provides development certs for PR builds and distribution certs for release builds. - adds nix flake to provide ruby dependencies for `fastlane` - adds config for new build identifier for PRs: `app.status.mobile.pr`
1 parent dcafa99 commit a97c44f

File tree

17 files changed

+863
-139
lines changed

17 files changed

+863
-139
lines changed

ci/Jenkinsfile.ios

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env groovy
2-
library 'status-jenkins-lib@v1.9.31'
2+
library 'status-jenkins-lib@fix-ios-signing-with-fastlane'
33

44
/* Options section can't access functions in objects. */
55
def isPRBuild = utils.isPRBuild()
@@ -73,10 +73,11 @@ pipeline {
7373
/* iOS build configuration */
7474
IPHONE_SDK = "iphoneos"
7575
ARCH = "x86_64"
76-
/* iOS app paths */
76+
/* iOS app paths - PR builds use StatusPR, release builds use Status */
77+
STATUS_IOS_APP_NAME = "${utils.isReleaseBuild() ? 'Status' : 'StatusPR'}"
7778
STATUS_IOS_APP_ARTIFACT = "pkg/${utils.pkgFilename(ext: 'ipa', arch: getArch(), version: env.VERSION, type: env.APP_TYPE)}"
78-
STATUS_IOS_APP = "${WORKSPACE}/mobile/bin/ios/qt6/Status.app"
79-
STATUS_IOS_IPA = "${WORKSPACE}/mobile/bin/ios/qt6/Status.ipa"
79+
STATUS_IOS_APP = "${WORKSPACE}/mobile/bin/ios/qt6/${env.STATUS_IOS_APP_NAME}.app"
80+
STATUS_IOS_IPA = "${WORKSPACE}/mobile/bin/ios/qt6/${env.STATUS_IOS_APP_NAME}.ipa"
8081
TESTFLIGHT_POLL_TIMEOUT = "${params.TESTFLIGHT_POLL_TIMEOUT}"
8182
TESTFLIGHT_POLL_INTERVAL = "${params.TESTFLIGHT_POLL_INTERVAL}"
8283
/* nwaku source directory */
@@ -124,6 +125,7 @@ pipeline {
124125
stage('Parallel Upload') {
125126
parallel {
126127
stage('Upload to TestFlight') {
128+
when { expression { utils.isReleaseBuild() } }
127129
steps {
128130
script {
129131
def changelog = sh(script: './scripts/generate-changelog.sh', returnStdout: true).trim()

ci/README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,66 @@ It also expects the presence of the following credentials:
2626
* `macos-keychain-file` - Keychain file with the MacOS signing certificate.
2727

2828
You can read about how to create such a keychain [here](https://github.com/status-im/infra-docs/blob/master/articles/macos_signing_keychain.md).
29+
30+
## iOS
31+
32+
iOS builds use **fastlane** with **match** for code signing management. This provides:
33+
- Automatic certificate and profile management
34+
- Separate signing for PR vs release builds
35+
36+
### Bundle Identifiers
37+
38+
| Build Type | Bundle ID | Fastlane Lane |
39+
|------------|-----------|---------------|
40+
| PR builds | `app.status.mobile.pr` | `pr` |
41+
| Release | `app.status.mobile` | `release` |
42+
43+
### Certificate Types
44+
45+
| Build Type | Certificate Type | Match Type | Purpose |
46+
|------------|------------------|------------|---------|
47+
| PR builds | Apple Development | `development` | Testing on registered devices |
48+
| Release | Apple Distribution | `appstore` | App Store / TestFlight |
49+
50+
### Fastlane Files
51+
52+
The `fastlane` configuration is located in `mobile/fastlane/`:
53+
54+
| File | Purpose |
55+
|------|---------|
56+
| `Fastfile` | Defines build lanes (pr, nightly, release) |
57+
| `Matchfile` | Configures match for certificate management |
58+
| `Appfile` | App identifiers and team configuration |
59+
| `Gemfile` | Ruby dependencies |
60+
61+
62+
### Local Development
63+
64+
To run `fastlane` locally for testing:
65+
66+
```bash
67+
cd mobile/fastlane
68+
nix --extra-experimental-features 'nix-command flakes' develop
69+
bundle install
70+
71+
# Run a specific lane
72+
bundle exec fastlane ios pr
73+
bundle exec fastlane ios release
74+
```
75+
76+
### Revoking/Rotating Certificates
77+
78+
If a certificate is compromised or revoked:
79+
80+
```bash
81+
cd mobile/fastlane
82+
83+
# Nuke existing certificates (warning!! watch what you nuke)
84+
bundle exec fastlane match nuke development
85+
bundle exec fastlane match nuke distribution
86+
87+
# Regenerate
88+
bundle exec fastlane match development --app_identifier "app.status.mobile.pr"
89+
bundle exec fastlane match appstore --app_identifier "app.status.mobile"
90+
```
91+

mobile/fastlane/.env.default

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Default environment variables for fastlane
2+
# These can be overridden by CI environment
3+
4+
# Disable fastlane colors in CI
5+
FASTLANE_DISABLE_COLORS=1
6+
7+
# Skip session verification
8+
FASTLANE_SESSION=""
9+
10+
# Team ID
11+
FASTLANE_TEAM_ID=8B5X2M6H2Y

mobile/fastlane/.gitignore

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Gems installed locally
2+
.gems/
3+
vendor/bundle/
4+
5+
# Bundler
6+
.bundle/
7+
8+
# Fastlane
9+
fastlane/report.xml
10+
fastlane/Preview.html
11+
fastlane/screenshots/
12+
fastlane/test_output/
13+
14+
# Nix
15+
result

mobile/fastlane/.ruby-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.2.2

mobile/fastlane/Appfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# App identifiers for Status App iOS builds
2+
app_identifier("app.status.mobile")
3+
apple_id(ENV["FASTLANE_APPLE_ID"])
4+
team_id("8B5X2M6H2Y")
5+
itc_team_id(ENV["FASTLANE_ITC_TEAM_ID"])
6+
7+
for_lane :pr do
8+
app_identifier("app.status.mobile.pr")
9+
end

mobile/fastlane/Fastfile

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
# This file defines the build and signing lanes for iOS
2+
3+
default_platform(:ios)
4+
5+
TEAM_ID = "8B5X2M6H2Y"
6+
APP_ID_RELEASE = "app.status.mobile"
7+
APP_ID_PR = "app.status.mobile.pr"
8+
APP_NAME_RELEASE = "Status.app"
9+
APP_NAME_PR = "StatusPR.app"
10+
PROJECT_DIR = File.expand_path("../", __dir__)
11+
BUILD_DIR = File.join(PROJECT_DIR, "bin", "ios", "qt6")
12+
13+
platform :ios do
14+
before_all do
15+
UI.message("Project directory: #{PROJECT_DIR}")
16+
UI.message("Build directory: #{BUILD_DIR}")
17+
end
18+
19+
after_all do
20+
# Clean up CI keychain after build
21+
if is_ci
22+
delete_keychain(name: keychain_name) if File.exist?(File.expand_path("~/Library/Keychains/#{keychain_name}-db"))
23+
end
24+
end
25+
26+
error do
27+
# Clean up CI keychain on failure too
28+
if is_ci
29+
delete_keychain(name: keychain_name) rescue nil
30+
end
31+
end
32+
33+
# ============================================
34+
# PR Builds
35+
# ============================================
36+
desc "Build iOS app for PRs"
37+
lane :pr do
38+
setup_ci_keychain
39+
40+
match(
41+
type: "development",
42+
app_identifier: APP_ID_PR,
43+
readonly: true,
44+
keychain_name: keychain_name,
45+
keychain_password: keychain_password
46+
)
47+
48+
sign_app(
49+
app_identifier: APP_ID_PR,
50+
app_name: APP_NAME_PR,
51+
profile_type: "development"
52+
)
53+
54+
create_ipa(app_name: APP_NAME_PR)
55+
end
56+
57+
# ============================================
58+
# Release Builds
59+
# ============================================
60+
desc "Build iOS app for release"
61+
lane :release do
62+
setup_ci_keychain
63+
64+
match(
65+
type: "appstore",
66+
app_identifier: APP_ID_RELEASE,
67+
readonly: true,
68+
keychain_name: keychain_name,
69+
keychain_password: keychain_password
70+
)
71+
72+
sign_app(
73+
app_identifier: APP_ID_RELEASE,
74+
app_name: APP_NAME_RELEASE,
75+
profile_type: "appstore"
76+
)
77+
78+
create_ipa(app_name: APP_NAME_RELEASE)
79+
80+
if ENV["UPLOAD_TO_TESTFLIGHT"] == "true"
81+
upload_to_testflight_lane
82+
end
83+
end
84+
85+
# ============================================
86+
# TestFlight Upload
87+
# ============================================
88+
desc "Upload IPA to TestFlight"
89+
lane :upload_to_testflight_lane do
90+
api_key = app_store_connect_api_key(
91+
key_id: ENV["ASC_KEY_ID"],
92+
issuer_id: ENV["ASC_ISSUER_ID"],
93+
key_filepath: ENV["ASC_KEY_FILE"],
94+
duration: 1200,
95+
in_house: false
96+
)
97+
98+
upload_to_testflight(
99+
api_key: api_key,
100+
ipa: File.join(BUILD_DIR, "Status.ipa"),
101+
skip_waiting_for_build_processing: true,
102+
changelog: ENV["CHANGELOG"] || "New build from CI"
103+
)
104+
end
105+
106+
# ============================================
107+
# Helper Methods
108+
# ============================================
109+
110+
private_lane :setup_ci_keychain do
111+
if is_ci
112+
create_keychain(
113+
name: keychain_name,
114+
password: keychain_password,
115+
default_keychain: true,
116+
unlock: true,
117+
timeout: 3600,
118+
lock_when_sleeps: false
119+
)
120+
end
121+
end
122+
123+
private_lane :sign_app do |options|
124+
app_identifier = options[:app_identifier]
125+
app_name = options[:app_name] || "Status.app"
126+
profile_type = options[:profile_type]
127+
128+
app_path = File.join(BUILD_DIR, app_name)
129+
130+
unless File.exist?(app_path)
131+
UI.user_error!("#{app_name} not found at #{app_path}")
132+
end
133+
134+
update_info_plist(
135+
plist_path: File.join(app_path, "Info.plist"),
136+
bundle_identifier: app_identifier
137+
)
138+
139+
UI.message("Signing app with identifier: #{app_identifier}")
140+
141+
profile_name = "match #{profile_type.capitalize} #{app_identifier}"
142+
143+
signing_identity = ENV["sigh_#{app_identifier}_#{profile_type}_certificate-name"] ||
144+
lane_context[SharedValues::MATCH_PROVISIONING_PROFILE_MAPPING]&.dig(app_identifier) ||
145+
get_signing_identity(profile_type)
146+
147+
# Need to Sign all frameworks first, else App crashes at runtime
148+
frameworks_path = File.join(app_path, "Frameworks")
149+
if File.directory?(frameworks_path)
150+
Dir.glob("#{frameworks_path}/*.framework").each do |framework|
151+
UI.message("Signing framework: #{File.basename(framework)}")
152+
sh("codesign --force --sign '#{signing_identity}' --timestamp '#{framework}'")
153+
end
154+
end
155+
156+
UI.message("Signing main app bundle...")
157+
158+
profile_path = ENV["sigh_#{app_identifier}_#{profile_type}_profile-path"]
159+
160+
if profile_path && File.exist?(profile_path)
161+
FileUtils.cp(profile_path, File.join(app_path, "embedded.mobileprovision"))
162+
end
163+
164+
sh("codesign --force --sign '#{signing_identity}' --timestamp '#{app_path}'")
165+
166+
UI.success("App signed successfully!")
167+
end
168+
169+
private_lane :create_ipa do |options|
170+
app_name = options[:app_name] || "Status.app"
171+
ipa_name = app_name.sub(".app", ".ipa")
172+
173+
app_path = File.join(BUILD_DIR, app_name)
174+
ipa_path = File.join(BUILD_DIR, ipa_name)
175+
176+
UI.message("Creating IPA at #{ipa_path}...")
177+
178+
FileUtils.rm_f(ipa_path)
179+
180+
Dir.mktmpdir do |tmpdir|
181+
payload_dir = File.join(tmpdir, "Payload")
182+
FileUtils.mkdir_p(payload_dir)
183+
FileUtils.cp_r(app_path, payload_dir)
184+
185+
Dir.chdir(tmpdir) do
186+
sh("zip -r '#{ipa_path}' Payload")
187+
end
188+
end
189+
190+
UI.success("IPA created at #{ipa_path}")
191+
end
192+
193+
def keychain_name
194+
"status_ci_#{ENV['BUILD_NUMBER'] || 'local'}.keychain"
195+
end
196+
197+
def keychain_password
198+
# Reuse MATCH_PASSWORD for keychain to reduce credential count
199+
ENV["MATCH_PASSWORD"] || "fastlane_ci_password"
200+
end
201+
202+
def get_signing_identity(profile_type)
203+
case profile_type
204+
when "development"
205+
"Apple Development"
206+
when "adhoc", "appstore"
207+
"Apple Distribution"
208+
else
209+
"Apple Distribution"
210+
end
211+
end
212+
end

mobile/fastlane/Gemfile

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
source "https://rubygems.org"
2+
3+
ruby ">= 3.0.0", "< 4.0.0"
4+
5+
# Core dependencies
6+
gem "fastlane", "~> 2.225"
7+
8+
plugins_path = File.join(File.dirname(__FILE__), 'Pluginfile')
9+
eval_gemfile(plugins_path) if File.exist?(plugins_path)

0 commit comments

Comments
 (0)