Skip to content

Commit 47ae31a

Browse files
committed
ci: create and sign separate bundle ID for PRs
1 parent dcafa99 commit 47ae31a

File tree

17 files changed

+848
-139
lines changed

17 files changed

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