Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,13 @@ mobile/bin/
mobile/lib/
mobile/build/
scripts/node_modules/

# fastlane
mobile/fastlane/.gems/
mobile/fastlane/vendor/bundle/
mobile/fastlane/.bundle/
mobile/fastlane/report.xml
mobile/fastlane/Preview.html
mobile/fastlane/screenshots/
mobile/fastlane/test_output/
mobile/fastlane/result
7 changes: 6 additions & 1 deletion ci/Jenkinsfile.combined
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env groovy

library 'status-jenkins-lib@v1.9.31'
library 'status-jenkins-lib@fix-ios-signing-with-fastlane'

/* Object to store public URLs for description. */
urls = [:]
Expand Down Expand Up @@ -113,6 +113,11 @@ pipeline {
'MacOS/aarch64', jenkins.Build('status-app/systems/macos/aarch64/package')
)
} } }
stage('iOS/aarch64') { steps { script {
ios_aarch64 = getArtifacts(
'iOS/aarch64', jenkins.Build('status-app/systems/ios/arm64/package')
)
} } }
}
}
stage('Publish') {
Expand Down
23 changes: 17 additions & 6 deletions ci/Jenkinsfile.ios
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env groovy
library 'status-jenkins-lib@v1.9.31'
library 'status-jenkins-lib@fix-ios-signing-with-fastlane'

/* Options section can't access functions in objects. */
def isPRBuild = utils.isPRBuild()
Expand Down Expand Up @@ -73,10 +73,11 @@ pipeline {
/* iOS build configuration */
IPHONE_SDK = "iphoneos"
ARCH = "x86_64"
/* iOS app paths */
/* iOS app paths - PR builds use StatusPR, release builds use Status */
STATUS_IOS_APP_NAME = "${utils.isReleaseBuild() ? 'Status' : 'StatusPR'}"
STATUS_IOS_APP_ARTIFACT = "pkg/${utils.pkgFilename(ext: 'ipa', arch: getArch(), version: env.VERSION, type: env.APP_TYPE)}"
STATUS_IOS_APP = "${WORKSPACE}/mobile/bin/ios/qt6/Status.app"
STATUS_IOS_IPA = "${WORKSPACE}/mobile/bin/ios/qt6/Status.ipa"
STATUS_IOS_APP = "${WORKSPACE}/mobile/bin/ios/qt6/${env.STATUS_IOS_APP_NAME}.app"
STATUS_IOS_IPA = "${WORKSPACE}/mobile/bin/ios/qt6/${env.STATUS_IOS_APP_NAME}.ipa"
TESTFLIGHT_POLL_TIMEOUT = "${params.TESTFLIGHT_POLL_TIMEOUT}"
TESTFLIGHT_POLL_INTERVAL = "${params.TESTFLIGHT_POLL_INTERVAL}"
/* nwaku source directory */
Expand Down Expand Up @@ -108,7 +109,7 @@ pipeline {
stage('Build iOS App') {
steps {
script {
app.buildSignedIOS(target='mobile-build', verbose=params.VERBOSE)
app.buildSignedIOS(target='mobile-build', verbose='3')
}
}
}
Expand All @@ -124,6 +125,7 @@ pipeline {
stage('Parallel Upload') {
parallel {
stage('Upload to TestFlight') {
when { expression { utils.isReleaseBuild() } }
steps {
script {
def changelog = sh(script: './scripts/generate-changelog.sh', returnStdout: true).trim()
Expand All @@ -137,11 +139,20 @@ pipeline {
}
}
}
stage('Upload to Diawi') {
when { expression { !utils.isReleaseBuild() } }
steps {
script {
def comment = "status-desktop PR build ${env.VERSION} (${env.GIT_COMMIT?.take(8) ?: 'unknown'})"
env.DIAWI_URL = app.uploadToDiawi(env.STATUS_IOS_APP_ARTIFACT, comment)
jenkins.setBuildDesc(IPA: env.DIAWI_URL)
}
}
}
stage('Upload to S3') {
steps {
script {
env.PKG_URL = s5cmd.upload(env.STATUS_IOS_APP_ARTIFACT)
jenkins.setBuildDesc(IPA: env.PKG_URL)
}
}
}
Expand Down
63 changes: 63 additions & 0 deletions ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,66 @@ It also expects the presence of the following credentials:
* `macos-keychain-file` - Keychain file with the MacOS signing certificate.

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).

## iOS
Copy link
Member

Choose a reason for hiding this comment

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

I think this could be somewhere in mobile, maybe mobile/fastlane


iOS builds use **fastlane** with **match** for code signing management. This provides:
- Automatic certificate and profile management
- Separate signing for PR vs release builds

### Bundle Identifiers

| Build Type | Bundle ID | Fastlane Lane |
|------------|------------------------|---------------|
| PR builds | `app.status.mobile.pr` | `pr` |
| Release | `app.status.mobile` | `release` |

### Certificate Types

| Build Type | Certificate Type | Match Type | Purpose |
|------------|--------------------|-------------|-------------------------------|
| PR builds | Apple Distribution | `adhoc` | Testing on registered devices |
| Release | Apple Distribution | `appstore` | App Store / TestFlight |

### Fastlane Files

The `fastlane` configuration is located in `mobile/fastlane/`:

| File | Purpose |
|-------------|----------------------------------------------|
| `Fastfile` | Defines signing lanes (`pr`, `release`) |
| `Matchfile` | Configures match for certificate management |
| `Appfile` | App identifiers and team configuration |
| `Gemfile` | Ruby dependencies |


### Local Development

To run `fastlane` locally for testing:

```bash
cd mobile/fastlane
nix --extra-experimental-features 'nix-command flakes' develop
bundle install

# Run a specific lane
bundle exec fastlane ios pr
bundle exec fastlane ios release
```

### Revoking/Rotating Certificates

If a certificate is compromised or revoked:

```bash
cd mobile/fastlane

# Nuke existing certificates (warning!! watch what you nuke)
bundle exec fastlane match nuke development
bundle exec fastlane match nuke distribution

# Regenerate
bundle exec fastlane match development --app_identifier "app.status.mobile.pr"
bundle exec fastlane match appstore --app_identifier "app.status.mobile"
```

7 changes: 7 additions & 0 deletions mobile/fastlane/Appfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# App identifiers for Status App iOS builds
app_identifier("app.status.mobile")
team_id(ENV["FASTLANE_TEAM_ID"])

for_lane :pr do
app_identifier("app.status.mobile.pr")
end
172 changes: 172 additions & 0 deletions mobile/fastlane/Fastfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# This file defines the signing and packaging lanes for iOS
# Building is done separately via make targets

default_platform(:ios)

# Build configuration
APP_NAME_RELEASE = "Status.app"
APP_NAME_PR = "StatusPR.app"
DISPLAY_NAME_RELEASE = "Status"
DISPLAY_NAME_PR = "Status PR"
PROJECT_DIR = File.expand_path("../", __dir__)
BUILD_DIR = File.join(PROJECT_DIR, "bin", "ios", "qt6")

platform :ios do
before_all do
UI.message("Project directory: #{PROJECT_DIR}")
UI.message("Build directory: #{BUILD_DIR}")
end

after_all do
# Clean up CI keychain after signing
if is_ci
delete_keychain(name: keychain_name) if File.exist?(File.expand_path("~/Library/Keychains/#{keychain_name}-db"))
end
end

error do
# Clean up CI keychain on failure too
if is_ci
delete_keychain(name: keychain_name) rescue nil
end
end

# ============================================
# PR Builds - Sign and package for ad-hoc distribution
# ============================================
desc "Sign and package iOS app for PRs"
lane :pr do
setup_ci_keychain

run_match(type: "adhoc")

resign_and_package(
app_name: APP_NAME_PR,
display_name: DISPLAY_NAME_PR,
profile_type: "adhoc"
)
end

# ============================================
# Release Builds - Sign and package for App Store
# ============================================
desc "Sign and package iOS app for release"
lane :release do
setup_ci_keychain

run_match(type: "appstore")

resign_and_package(
app_name: APP_NAME_RELEASE,
display_name: DISPLAY_NAME_RELEASE,
profile_type: "appstore"
)

if ENV["UPLOAD_TO_TESTFLIGHT"] == "true"
upload_to_testflight(
ipa: File.join(BUILD_DIR, "Status.ipa"),
skip_waiting_for_build_processing: true,
changelog: ENV["CHANGELOG"] || "New build from CI"
)
end
end

# ============================================
# Helper Methods
# ============================================

private_lane :setup_ci_keychain do
if is_ci
create_keychain(
name: keychain_name,
password: keychain_password,
default_keychain: true,
unlock: true,
timeout: 3600,
lock_when_sleeps: false
)
end
end

private_lane :run_match do |options|
match_params = {
type: options[:type],
readonly: false,
# Auto-regenerate profiles when new devices are registered (for dev and adhoc)
force_for_new_devices: options[:type] == "adhoc"
}

# Only specify keychain params in CI where we create a custom keychain
if is_ci
match_params[:keychain_name] = keychain_name
match_params[:keychain_password] = keychain_password
end

match(match_params)
end

private_lane :resign_and_package do |options|
app_identifier = CredentialsManager::AppfileConfig.try_fetch_value(:app_identifier)
app_name = options[:app_name] || "Status.app"
display_name = options[:display_name] || "Status"
profile_type = options[:profile_type]

app_path = File.join(BUILD_DIR, app_name)
ipa_name = app_name.sub(".app", ".ipa")
ipa_path = File.join(BUILD_DIR, ipa_name)

unless File.exist?(app_path)
UI.user_error!("#{app_name} not found at #{app_path}")
end

# Get signing identity and provisioning profile from match
signing_identity = ENV["sigh_#{app_identifier}_#{profile_type}_certificate-name"]
provisioning_profile = ENV["sigh_#{app_identifier}_#{profile_type}_profile-path"]

UI.message("Signing identity: #{signing_identity}")
UI.message("Provisioning profile: #{provisioning_profile}")

unless provisioning_profile && File.exist?(provisioning_profile)
UI.user_error!("Provisioning profile not found!")
end

unless signing_identity
UI.user_error!("Signing identity not found!")
end

UI.message("Creating unsigned IPA...")
FileUtils.rm_f(ipa_path)

Dir.mktmpdir do |tmpdir|
payload_dir = File.join(tmpdir, "Payload")
FileUtils.mkdir_p(payload_dir)
FileUtils.cp_r(app_path, payload_dir)

Dir.chdir(tmpdir) do
sh("zip -r '#{ipa_path}' Payload")
end
end

# This handles frameworks, entitlements patching, and bundle ID updates
UI.message("Resigning IPA with fastlane resign action...")
resign(
ipa: ipa_path,
signing_identity: signing_identity,
provisioning_profile: {
app_identifier => provisioning_profile
},
bundle_id: app_identifier,
display_name: display_name
)

UI.success("Signed and packaged: #{ipa_path}")
end

def keychain_name
"status_ci_#{ENV['BUILD_NUMBER'] || 'local'}.keychain"
end

def keychain_password
ENV["MATCH_PASSWORD"] || "fastlane_ci_password"
end
end
7 changes: 7 additions & 0 deletions mobile/fastlane/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
source "https://rubygems.org"

# Core dependencies
gem "fastlane", "~> 2.225"

plugins_path = File.join(File.dirname(__FILE__), 'Pluginfile')
eval_gemfile(plugins_path) if File.exist?(plugins_path)
Loading