Skip to content

Commit f0093cd

Browse files
committed
ios: produce signed build
- points to `sign-ios-release` branch in status-jenkins-lib which provides iOS provisioning profile and appstore certs. - builds signed iOS app and pushes to testflight. - updates testflight metadata with PR link and commit sha it was built from - adds `scripts/testflight-changelog.mjs` script used by CI to update PR metadata. - set `ITSAppUsesNonExemptEncryption` to `false`
1 parent 15aefec commit f0093cd

File tree

11 files changed

+546
-36
lines changed

11 files changed

+546
-36
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,3 +113,4 @@ ui/StatusQ/src/StatusQ/Core/TestConfig.qml
113113
mobile/bin/
114114
mobile/lib/
115115
mobile/build/
116+
scripts/node_modules/

ci/Jenkinsfile.ios

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
#!/usr/bin/env groovy
2-
library 'status-jenkins-lib@v1.9.29'
2+
library 'status-jenkins-lib@sign-ios-release'
33

44
/* Options section can't access functions in objects. */
55
def isPRBuild = utils.isPRBuild()
6-
def isNightlyBuild = utils.isNightlyBuild()
76

87
pipeline {
98

@@ -20,6 +19,16 @@ pipeline {
2019
description: 'Level of verbosity based on nimbus-build-system setup.',
2120
choices: ['0','1','2','3']
2221
)
22+
string(
23+
name: 'TESTFLIGHT_POLL_TIMEOUT',
24+
description: 'TestFlight build polling timeout in minutes.',
25+
defaultValue: '30'
26+
)
27+
string(
28+
name: 'TESTFLIGHT_POLL_INTERVAL',
29+
description: 'TestFlight build polling interval in seconds.',
30+
defaultValue: '30'
31+
)
2332
}
2433

2534
options {
@@ -65,8 +74,11 @@ pipeline {
6574
IPHONE_SDK = "iphoneos"
6675
ARCH = "x86_64"
6776
/* iOS app paths */
68-
STATUS_IOS_APP_ARTIFACT = "pkg/${utils.pkgFilename(ext: 'app.zip', arch: getArch(), version: env.VERSION, type: env.APP_TYPE)}"
77+
STATUS_IOS_APP_ARTIFACT = "pkg/${utils.pkgFilename(ext: 'ipa', arch: getArch(), version: env.VERSION, type: env.APP_TYPE)}"
6978
STATUS_IOS_APP = "${WORKSPACE}/mobile/bin/ios/qt6/Status.app"
79+
STATUS_IOS_IPA = "${WORKSPACE}/mobile/bin/ios/qt6/Status.ipa"
80+
TESTFLIGHT_POLL_TIMEOUT = "${params.TESTFLIGHT_POLL_TIMEOUT}"
81+
TESTFLIGHT_POLL_INTERVAL = "${params.TESTFLIGHT_POLL_INTERVAL}"
7082
}
7183

7284
stages {
@@ -91,25 +103,42 @@ pipeline {
91103

92104
stage('Build iOS App') {
93105
steps {
94-
sh 'make mobile-build'
106+
script {
107+
app.buildSignedIOS(target='mobile-build', verbose='3')
108+
}
95109
}
96110
}
97111

98112
stage('Package iOS App') {
99113
steps {
100114
sh 'mkdir -p pkg'
101-
sh "cd mobile/bin/ios/qt6 && zip -r ${env.WORKSPACE}/${env.STATUS_IOS_APP_ARTIFACT} Status.app"
102-
sh "ls -la ${env.STATUS_IOS_APP_ARTIFACT}"
115+
sh "cp ${env.STATUS_IOS_IPA} ${env.STATUS_IOS_APP_ARTIFACT}"
116+
sh "ls -lh ${env.STATUS_IOS_APP_ARTIFACT}"
117+
}
118+
}
119+
120+
stage('Upload to TestFlight') {
121+
steps {
122+
script {
123+
def changelog = sh(script: './scripts/generate-changelog.sh', returnStdout: true).trim()
124+
125+
app.uploadToTestFlight(
126+
ipaPath: env.STATUS_IOS_APP_ARTIFACT,
127+
changelog: changelog,
128+
pollTimeout: env.TESTFLIGHT_POLL_TIMEOUT,
129+
pollInterval: env.TESTFLIGHT_POLL_INTERVAL
130+
)
131+
}
103132
}
104133
}
105134

106135
stage('Parallel Upload') {
107136
parallel {
108-
stage('Upload') {
137+
stage('Upload to S3') {
109138
steps {
110139
script {
111140
env.PKG_URL = s5cmd.upload(env.STATUS_IOS_APP_ARTIFACT)
112-
jenkins.setBuildDesc(APP: env.PKG_URL)
141+
jenkins.setBuildDesc(IPA: env.PKG_URL)
113142
}
114143
}
115144
}

mobile/ios/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,5 +64,7 @@
6464
<string>Status uses Media Library to save and send Images. The Media Library module internally requires permissions to Apple Music</string>
6565
<key>NSFaceIDUsageDescription</key>
6666
<string>Log in securely to your account.</string>
67+
<key>ITSAppUsesNonExemptEncryption</key>
68+
<false/>
6769
</dict>
6870
</plist>

mobile/scripts/android/sign.sh

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
if [[ $# -lt 1 ]]; then
5+
echo "Usage: $0 <aab-file-path>"
6+
exit 1
7+
fi
8+
9+
AAB_FILE="$1"
10+
11+
if [[ ! -f "$AAB_FILE" ]]; then
12+
echo "Error: AAB file not found at $AAB_FILE"
13+
exit 1
14+
fi
15+
16+
if [[ -z "$KEYSTORE_PATH" || -z "$KEYSTORE_PASSWORD" || -z "$KEY_ALIAS" || -z "$KEY_PASSWORD" ]]; then
17+
echo "Error: Missing signing credentials"
18+
echo "Required: KEYSTORE_PATH, KEYSTORE_PASSWORD, KEY_ALIAS, KEY_PASSWORD"
19+
exit 1
20+
fi
21+
22+
if [[ ! -f "$KEYSTORE_PATH" ]]; then
23+
echo "Error: Keystore file not found at $KEYSTORE_PATH"
24+
exit 1
25+
fi
26+
27+
echo "Signing AAB with jarsigner..."
28+
jarsigner -sigalg SHA256withRSA -digestalg SHA-256 \
29+
-keystore "$KEYSTORE_PATH" \
30+
-storepass "$KEYSTORE_PASSWORD" \
31+
-keypass "$KEY_PASSWORD" \
32+
"$AAB_FILE" "$KEY_ALIAS"
33+
34+
if [[ $? -ne 0 ]]; then
35+
echo "Error: AAB signing failed"
36+
exit 1
37+
fi
38+
39+
echo "Verifying AAB signature..."
40+
VERIFY_OUTPUT=$(jarsigner -verify "$AAB_FILE" 2>&1)
41+
if echo "$VERIFY_OUTPUT" | grep -q "jar verified"; then
42+
echo "AAB signature verification: PASSED"
43+
else
44+
echo "Error: AAB signature verification failed"
45+
echo "Verify output: $VERIFY_OUTPUT"
46+
exit 1
47+
fi
48+
49+
echo "AAB signed successfully: $AAB_FILE"

mobile/scripts/buildApp.sh

Lines changed: 23 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env bash
2-
set -ef pipefail
2+
set -eo pipefail
33

44
CWD=$(realpath "$(dirname "$0")")
55

@@ -10,6 +10,7 @@ BIN_DIR=${BIN_DIR:-"$CWD/../bin/ios"}
1010
BUILD_DIR=${BUILD_DIR:-"$CWD/../build"}
1111
ANDROID_ABI=${ANDROID_ABI:-"arm64-v8a"}
1212
BUILD_TYPE=${BUILD_TYPE:-"apk"}
13+
SIGN_IOS=${SIGN_IOS:-"false"}
1314

1415
echo "Building wrapperApp for ${OS}, ${ANDROID_ABI}"
1516

@@ -60,27 +61,8 @@ if [[ "${OS}" == "android" ]]; then
6061
exit 1
6162
fi
6263

63-
# Note: androiddeployqt --sign does not work for AAB files, so we sign with jarsigner
64-
echo "Signing AAB with jarsigner..."
65-
jarsigner -sigalg SHA256withRSA -digestalg SHA-256 \
66-
-keystore "$KEYSTORE_PATH" \
67-
-storepass "$KEYSTORE_PASSWORD" \
68-
-keypass "$KEY_PASSWORD" \
69-
"$OUTPUT_FILE" "$KEY_ALIAS"
70-
71-
if [[ $? -ne 0 ]]; then
72-
echo "Error: AAB signing failed"
73-
exit 1
74-
fi
75-
76-
VERIFY_OUTPUT=$(jarsigner -verify "$OUTPUT_FILE" 2>&1)
77-
if echo "$VERIFY_OUTPUT" | grep -q "jar verified"; then
78-
echo "AAB signature verification: PASSED"
79-
else
80-
echo "Error: AAB signature verification failed"
81-
echo "Verify output: $VERIFY_OUTPUT"
82-
exit 1
83-
fi
64+
# Sign the AAB file (androiddeployqt --sign does not work for AAB files)
65+
"$CWD/android/sign.sh" "$OUTPUT_FILE"
8466

8567
ANDROID_OUTPUT_DIR="bin/android/qt6"
8668
BIN_DIR_ANDROID=${BIN_DIR:-"$CWD/$ANDROID_OUTPUT_DIR"}
@@ -128,17 +110,32 @@ if [[ "${OS}" == "android" ]]; then
128110
fi
129111
fi
130112
else
113+
BUILD_VERSION=$(($(date +%s) * 1000 / 60000))
114+
115+
if [[ -n "${CHANGE_ID:-}" ]]; then
116+
VERSION_STRING="${CHANGE_ID}.${BUILD_VERSION}"
117+
else
118+
VERSION_STRING="${BUILD_VERSION}"
119+
fi
120+
121+
echo "Using version: $VERSION_STRING"
122+
131123
QMAKE_BIN="${QMAKE:-qmake}"
132-
"$QMAKE_BIN" "$CWD/../wrapperApp/Status.pro" -spec macx-ios-clang CONFIG+=release CONFIG+="$SDK" CONFIG+=device -after
124+
"$QMAKE_BIN" "$CWD/../wrapperApp/Status.pro" -spec macx-ios-clang CONFIG+=release CONFIG+="$SDK" CONFIG+=device VERSION="$VERSION_STRING" -after
125+
133126
# Compile resources
134127
xcodebuild -configuration Release -target "Qt Preprocess" -sdk "$SDK" -arch "$ARCH" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | xcbeautify
135128
# Compile the app
136129
xcodebuild -configuration Release -target Status install -sdk "$SDK" -arch "$ARCH" DSTROOT="$BIN_DIR" INSTALL_PATH="/" TARGET_BUILD_DIR="$BIN_DIR" CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO | xcbeautify
137130

138-
if [[ -e "$BIN_DIR/Status.app/Info.plist" ]]; then
139-
echo "Build succeeded"
140-
else
131+
if [[ ! -e "$BIN_DIR/Status.app/Info.plist" ]]; then
141132
echo "Build failed"
142133
exit 1
143134
fi
135+
136+
if [[ "$SIGN_IOS" == "true" ]]; then
137+
"$CWD/ios/sign.sh"
138+
fi
139+
140+
echo "Build succeeded"
144141
fi

mobile/scripts/ios/sign.sh

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
CWD=$(realpath "$(dirname "$0")")
5+
BIN_DIR=${BIN_DIR:-"$CWD/../../bin/ios"}
6+
7+
if [[ ! -e "$BIN_DIR/Status.app/Info.plist" ]]; then
8+
echo "Error: Status.app not found at $BIN_DIR/Status.app"
9+
exit 1
10+
fi
11+
12+
if [[ -z "$IOS_CERT_PATH" || -z "$IOS_CERT_PASSWORD" || -z "$IOS_PROVISIONING_PROFILE" ]]; then
13+
echo "Error: Missing iOS signing credentials"
14+
echo "Required: IOS_CERT_PATH, IOS_CERT_PASSWORD, IOS_PROVISIONING_PROFILE"
15+
exit 1
16+
fi
17+
18+
echo "Signing iOS app at $BIN_DIR/Status.app..."
19+
20+
KEYCHAIN_NAME="build-$$.keychain"
21+
KEYCHAIN_PASSWORD=$(openssl rand -base64 16)
22+
23+
cleanup_keychain() {
24+
echo "Cleaning up keychain..."
25+
security default-keychain -s login.keychain 2>/dev/null || true
26+
security delete-keychain "$KEYCHAIN_NAME" 2>/dev/null || true
27+
}
28+
29+
trap cleanup_keychain EXIT
30+
31+
security delete-keychain "$KEYCHAIN_NAME" 2>/dev/null || true
32+
33+
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
34+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
35+
security set-keychain-settings -t 3600 -u "$KEYCHAIN_NAME"
36+
security list-keychains -s "$KEYCHAIN_NAME" login.keychain
37+
security default-keychain -s "$KEYCHAIN_NAME"
38+
39+
echo "Importing Apple WWDR G3 certificate..."
40+
WWDR_TEMP_DIR=$(mktemp -d)
41+
curl -sS -o "$WWDR_TEMP_DIR/AppleWWDRCAG3.cer" https://www.apple.com/certificateauthority/AppleWWDRCAG3.cer
42+
security import "$WWDR_TEMP_DIR/AppleWWDRCAG3.cer" -k "$KEYCHAIN_NAME" -T /usr/bin/codesign
43+
rm -rf "$WWDR_TEMP_DIR"
44+
echo "Apple WWDR G3 certificate imported"
45+
46+
security import "$IOS_CERT_PATH" -k "$KEYCHAIN_NAME" -P "$IOS_CERT_PASSWORD" -T /usr/bin/codesign
47+
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
48+
49+
PROFILE_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"
50+
mkdir -p "$PROFILE_DIR"
51+
52+
PROFILE_UUID=$(security cms -D -i "$IOS_PROVISIONING_PROFILE" 2>/dev/null | grep -A1 "<key>UUID</key>" | grep "<string>" | sed 's/.*<string>\(.*\)<\/string>.*/\1/')
53+
54+
rm -f "$PROFILE_DIR/$PROFILE_UUID.mobileprovision"
55+
56+
cp "$IOS_PROVISIONING_PROFILE" "$PROFILE_DIR/$PROFILE_UUID.mobileprovision"
57+
58+
echo "Installed provisioning profile: $PROFILE_UUID"
59+
60+
echo "Embedding provisioning profile into app..."
61+
cp "$IOS_PROVISIONING_PROFILE" "$BIN_DIR/Status.app/embedded.mobileprovision"
62+
63+
echo "Searching for signing identity in keychain..."
64+
security find-identity -v -p codesigning "$KEYCHAIN_NAME"
65+
66+
SIGNING_IDENTITY=$(security find-identity -v -p codesigning "$KEYCHAIN_NAME" | grep -E "iPhone Distribution|Apple Distribution" | head -1 | awk '{print $2}')
67+
68+
if [[ -z "$SIGNING_IDENTITY" ]]; then
69+
echo "ERROR: No Distribution certificate found in keychain!"
70+
echo "Available identities:"
71+
security find-identity -v -p codesigning "$KEYCHAIN_NAME"
72+
exit 1
73+
fi
74+
75+
echo "Signing with identity: $SIGNING_IDENTITY"
76+
77+
echo "Extracting entitlements from provisioning profile..."
78+
ENTITLEMENTS_PLIST=$(mktemp -t entitlements).plist
79+
80+
security cms -D -i "$IOS_PROVISIONING_PROFILE" | \
81+
plutil -extract Entitlements xml1 - -o "$ENTITLEMENTS_PLIST"
82+
83+
echo "Entitlements extracted to: $ENTITLEMENTS_PLIST"
84+
cat "$ENTITLEMENTS_PLIST"
85+
86+
echo "Signing embedded frameworks..."
87+
if [ -d "$BIN_DIR/Status.app/Frameworks" ]; then
88+
find "$BIN_DIR/Status.app/Frameworks" -name "*.framework" -type d | while read framework; do
89+
echo "Signing framework: $(basename "$framework")"
90+
codesign --force --sign "$SIGNING_IDENTITY" --timestamp "$framework"
91+
done
92+
fi
93+
94+
echo "Signing main app bundle..."
95+
codesign --force --sign "$SIGNING_IDENTITY" --entitlements "$ENTITLEMENTS_PLIST" --timestamp "$BIN_DIR/Status.app"
96+
97+
rm -f "$ENTITLEMENTS_PLIST"
98+
99+
echo "Verifying signature..."
100+
codesign --verify --verbose=4 "$BIN_DIR/Status.app"
101+
102+
echo "Signature details:"
103+
codesign -d --entitlements :- "$BIN_DIR/Status.app"
104+
105+
echo "iOS app signed successfully"
106+
107+
echo "Creating IPA file..."
108+
IPA_DIR=$(mktemp -d)
109+
mkdir -p "$IPA_DIR/Payload"
110+
cp -R "$BIN_DIR/Status.app" "$IPA_DIR/Payload/"
111+
112+
cd "$IPA_DIR"
113+
zip -r "$BIN_DIR/Status.ipa" Payload
114+
cd -
115+
116+
rm -rf "$IPA_DIR"
117+
echo "IPA created at $BIN_DIR/Status.ipa"

mobile/wrapperApp/Status.pro

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ ios {
4444

4545
QMAKE_INFO_PLIST = $$PWD/../ios/Info.plist
4646
QMAKE_IOS_DEPLOYMENT_TARGET=16.0
47-
QMAKE_TARGET_BUNDLE_PREFIX = im.status
48-
QMAKE_BUNDLE = app
47+
QMAKE_TARGET_BUNDLE_PREFIX = app.status
48+
QMAKE_BUNDLE = mobile
4949
QMAKE_ASSET_CATALOGS += $$PWD/../ios/Images.xcassets
5050
QMAKE_IOS_LAUNCH_SCREEN = $$PWD/../ios/launch-image-universal.storyboard
5151

scripts/extract-bundle-version.sh

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
if [[ $# -lt 1 ]]; then
5+
echo "Usage: $0 <ipa-file-path>" >&2
6+
exit 1
7+
fi
8+
9+
IPA_PATH="$1"
10+
11+
if [[ ! -f "$IPA_PATH" ]]; then
12+
echo "Error: IPA file not found at $IPA_PATH" >&2
13+
exit 1
14+
fi
15+
16+
unzip -p "$IPA_PATH" 'Payload/*.app/Info.plist' | \
17+
plutil -extract CFBundleVersion raw -o - -

0 commit comments

Comments
 (0)