summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorEmīls <emils@mullvad.net>2023-06-13 16:46:47 +0200
committerEmīls <emils@mullvad.net>2023-08-31 12:55:50 +0200
commit1372db81edfe2d18bcaf7684f4a8f23f205c06f8 (patch)
tree2871f084c33e46084b7e4eee344fd5cf774c16ac
parent2674538fbaab8b139b20431f2678e0111f8013ae (diff)
downloadmullvadvpn-1372db81edfe2d18bcaf7684f4a8f23f205c06f8.tar.xz
mullvadvpn-1372db81edfe2d18bcaf7684f4a8f23f205c06f8.zip
Add buildscripts for CI
-rw-r--r--ci/ios/README.md20
-rw-r--r--ci/ios/build-app.sh26
-rw-r--r--ci/ios/buildserver-build-ios.sh55
-rw-r--r--ci/ios/run-build-and-upload.sh26
-rw-r--r--ci/ios/run-in-vm.sh30
-rw-r--r--ci/ios/upload-app.sh8
-rw-r--r--ci/ios/upload-vm/Gemfile3
-rw-r--r--ci/ios/upload-vm/Gemfile.lock218
-rw-r--r--ci/ios/upload-vm/README.md17
-rwxr-xr-xios/build.sh28
10 files changed, 380 insertions, 51 deletions
diff --git a/ci/ios/README.md b/ci/ios/README.md
new file mode 100644
index 0000000000..df41bd1e95
--- /dev/null
+++ b/ci/ios/README.md
@@ -0,0 +1,20 @@
+# Automating iOS builds
+Building an app on iOS is a hodge-podge of 2 VMs and too many bash scripts.
+It all starts with `buildserver-build-ios.sh`. It does 2 main things:
+- polls the app repo
+- tries to build _well signed_ tags that match `^ios/` with `run-build-and-upload.sh`.
+
+_Well signed_ in this case implies that the tag has been signed by a GPG key that is signed by
+Mullvad's code signing key.
+
+
+## `run-build-and-upload.sh`
+This script builds the iOS app archive in one VM, which has our app signing keys, and uploads the
+resulting archive in the upload VM, which has our AppStoreConnect API keys. This segregation is
+intentional, compromising the build VM won't allow an attacker to take down our app store listing,
+compromising the upload VM won't allow the attacker to compromise our signing keys.
+
+The script uses `run-in-vm.sh` to execute scripts inside these 2 VMs:
+- `build-app.sh` is executed in the build VM, which eventually executes `ios/build.sh`.
+- `upload-app.sh` is executed in the upload VM
+
diff --git a/ci/ios/build-app.sh b/ci/ios/build-app.sh
new file mode 100644
index 0000000000..abf6fac1cd
--- /dev/null
+++ b/ci/ios/build-app.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+# Buildscript to run inside a build VM to build a new IPA for the iOS app.
+
+set -eu
+
+# This single path really screws with XCode and wireguard-go's makefiles, which
+# really do not like the whitespace. Thus, the build source is copied to a
+# non-whitespaced `~/build`, built there and the resulting `MullvadVPN.ipa` is
+# copied back.
+VM_BUILD_DIR="/Volumes/My Shared Files/build"
+
+security unlock-keychain -p 'build'
+
+rm -rf ~/build
+cp -r "$VM_BUILD_DIR" ~/build || true
+cd ~/build/ios
+rm -r Build
+
+# Instantiate Xcconfig templates.
+for file in ./Configurations/*.template ; do cp $file ${file//.template/} ; done
+
+IOS_PROVISIONING_PROFILES_DIR=~/provisioning-profiles \
+ PATH=/usr/local/go/bin:$PATH \
+ bash build.sh
+
+cp "$HOME/build/ios/Build/MullvadVPN.ipa" "${VM_BUILD_DIR}/ios/Build/"
diff --git a/ci/ios/buildserver-build-ios.sh b/ci/ios/buildserver-build-ios.sh
index cf6aa54c34..fab47acd97 100644
--- a/ci/ios/buildserver-build-ios.sh
+++ b/ci/ios/buildserver-build-ios.sh
@@ -8,11 +8,20 @@ BUILD_DIR="$SCRIPT_DIR/mullvadvpn-app/ios"
LAST_BUILT_DIR="$SCRIPT_DIR/last-built"
mkdir -p "$LAST_BUILT_DIR"
+# Convince git to work on our checkout of the app repository, regardless of PWD.
+export GIT_WORK_TREE="$SCRIPT_DIR/mullvadvpn-app/"
+export GIT_DIR="$GIT_WORK_TREE/.git"
+function run_git {
+ # `git submodule` needs more info than just $GIT_DIR and $GIT_WORK_TREE.
+ # But -C makes it work.
+ git -C $GIT_WORK_TREE $@
+}
+
function build_ref() {
local tag=$1;
local current_hash="";
- if ! current_hash=$(git rev-parse "$tag^{commit}"); then
+ if ! current_hash=$(run_git rev-parse "$tag^{commit}"); then
echo "!!!"
echo "[#] Failed to get commit for $tag"
echo "!!!"
@@ -24,6 +33,20 @@ function build_ref() {
return 0
fi
+
+ if ! run_git verify-tag "$tag"; then
+ echo "!!!"
+ echo "[#] $tag failed GPG verification!"
+ echo "!!!"
+ sleep 60
+ return 0
+ fi
+
+ run_git reset --hard
+ run_git checkout $tag
+ run_git submodule update
+ run_git clean -df
+
local app_build_version="";
if ! app_build_version=$(read_app_version); then
echo "!!!"
@@ -35,30 +58,17 @@ function build_ref() {
if [ -f "$LAST_BUILT_DIR/build-$app_build_version" ]; then
echo "!!!"
- echo "[#] App version already built in commit $(cat "${LAST_BUILT_DIR}/build-${app_build_version}")"
+ echo "[#] App version $app_build_version already built in commit $(cat "${LAST_BUILT_DIR}/build-${app_build_version}")"
echo "[#] The build version in Configuration/Version.xcconfig should be bumped."
+ echo "[#] Failed to build $current_hash"
echo "!!!"
- sleep 60
return 0
fi
echo ""
echo "[#] $tag: $app_build_version $current_hash, building new packages."
- if ! git verify-tag "$tag"; then
- echo "!!!"
- echo "[#] $tag failed GPG verification!"
- echo "!!!"
- sleep 60
- return 0
- fi
-
- git reset --hard
- git checkout $tag
- git submodule update
- git clean -df
-
- if "$BUILD_DIR"/build.sh; then
+ if "$SCRIPT_DIR"/run-build-and-upload.sh; then
touch "$LAST_BUILT_DIR"/"commit-$current_hash"
echo "$current_hash" > "$LAST_BUILT_DIR"/"build-${app_build_version}"
echo "Successfully built ${app_build_version} ${tag} with hash ${current_hash}"
@@ -66,8 +76,8 @@ function build_ref() {
}
function read_app_version() {
- project_version=$(sed -n "s/CURRENT_PROJECT_VERSION = \([[:digit:]]\)/\1/p" Configurations/Version.xcconfig)
- marketing_version=$(sed -n "s/MARKETING_VERSION = \([[:digit:]]\)/\1/p" Configurations/Version.xcconfig)
+ project_version=$(sed -n "s/CURRENT_PROJECT_VERSION = \([[:digit:]]\)/\1/p" "$BUILD_DIR/Configurations/Version.xcconfig")
+ marketing_version=$(sed -n "s/MARKETING_VERSION = \([[:digit:]]\)/\1/p" "$BUILD_DIR/Configurations/Version.xcconfig")
echo "${marketing_version}-${project_version}"
if [ -z "$project_version" ] || [ -z "$marketing_version" ]; then
exit 1;
@@ -77,13 +87,12 @@ function read_app_version() {
function run_build_loop() {
- cd "$BUILD_DIR"
while true; do
# Delete all tags. So when fetching we only get the ones existing on the remote
- git tag | xargs git tag -d > /dev/null
+ run_git tag | xargs git tag -d > /dev/null
- git fetch --prune --tags 2> /dev/null || continue
- local tags=( $(git tag | grep "$TAG_PATTERN_TO_BUILD") )
+ run_git fetch --prune --tags 2> /dev/null || continue
+ local tags=( $(run_git tag | grep "$TAG_PATTERN_TO_BUILD") )
for tag in "${tags[@]}"; do
build_ref "refs/tags/$tag"
diff --git a/ci/ios/run-build-and-upload.sh b/ci/ios/run-build-and-upload.sh
new file mode 100644
index 0000000000..6c15389331
--- /dev/null
+++ b/ci/ios/run-build-and-upload.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+# Buildscript to run on our buildserver to create and upload an iOS app.
+#
+# The script expects two tart VMs, `ios-build` and `ios-upload` to exist and be
+# executable. The build VM is expected to have the appropriate environment to
+# be able to build the app and the upload VM - to upload the app. Both must be
+# reachable via SSH without a password via the SSH key located in
+# ~/.ssh/ios-vm-key.
+
+set -eu -o pipefail
+
+# Add SSH key for iOS build VMs to be able to SSH into them without user interaction
+eval $(ssh-agent)
+ssh-add ~/.ssh/ios-vm-key
+
+# This single path really screws with XCode and wireguard-go's makefiles, which
+# really do not like the whitespace. Thus, the build source is copied to a
+# non-whitespaced `~/build`, built there and the resulting `MullvadVPN.ipa` is
+# copied back.
+MULLVAD_CHECKOUT_DIR="${MULLVAD_CHECKOUT_DIR:-mullvadvpn-app}"
+
+bash run-in-vm.sh "ios-build" "$(pwd)/build-app.sh" "build:${MULLVAD_CHECKOUT_DIR}"
+mkdir -p build-output
+cp "${MULLVAD_CHECKOUT_DIR}/ios/Build/MullvadVPN.ipa" build-output/MullvadVPN.ipa
+
+bash run-in-vm.sh "ios-upload" "$(pwd)/upload-app.sh" "build-output:build-output"
diff --git a/ci/ios/run-in-vm.sh b/ci/ios/run-in-vm.sh
new file mode 100644
index 0000000000..331ba04af4
--- /dev/null
+++ b/ci/ios/run-in-vm.sh
@@ -0,0 +1,30 @@
+# This takes the following positional arguments 
+# 1. tart VM name
+# 2. Script to execute in the VM
+# 3. Passthrough directory path, formatted like "$guest_mount_name:$host_dir_path"
+#
+# The script expects that with the current SSH agent, it's possible to SSH into
+# the `admin` user on the VM without any user interaction. The script will
+# bring the VM up, execute the specified script via SSH and shut down the VM.
+#
+# The script returns the exit code of the SSH command.
+
+set -o pipefail
+
+VM_NAME=${1:?"No VM name provided"}
+SCRIPT=${2:?"No script path provided"}
+PASSTHROUGH_DIR=${3:?"No passthrough directory provided"}
+
+tart run --no-graphics "--dir=${PASSTHROUGH_DIR}" "$VM_NAME" &
+vm_pid=$!
+
+# Sleep to wait until VM is up
+sleep 10
+
+# apparently, there's a difference between piping into zsh like this and doing
+# a <(echo $SCRIPT).
+cat "$SCRIPT" | ssh admin@$(tart ip "$VM_NAME") bash /dev/stdin
+script_status=$?
+
+kill $vm_pid
+exit $script_status
diff --git a/ci/ios/upload-app.sh b/ci/ios/upload-app.sh
new file mode 100644
index 0000000000..eb25ac8600
--- /dev/null
+++ b/ci/ios/upload-app.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+# Upload script to run in `ios-upload` VM to upload a newly built IPA to TestFlight
+
+VM_UPLOAD_IPA_PATH="/Volumes/My Shared Files/build-output/MullvadVPN.ipa"
+API_KEY_PATH="$HOME/ci/app-store-connect-key.json"
+cd ci/
+source ~/.bash_profile
+bundle exec fastlane pilot upload --api-key-path ${API_KEY_PATH} --ipa ${VM_UPLOAD_IPA_PATH}
diff --git a/ci/ios/upload-vm/Gemfile b/ci/ios/upload-vm/Gemfile
new file mode 100644
index 0000000000..7a118b49be
--- /dev/null
+++ b/ci/ios/upload-vm/Gemfile
@@ -0,0 +1,3 @@
+source "https://rubygems.org"
+
+gem "fastlane"
diff --git a/ci/ios/upload-vm/Gemfile.lock b/ci/ios/upload-vm/Gemfile.lock
new file mode 100644
index 0000000000..206273553a
--- /dev/null
+++ b/ci/ios/upload-vm/Gemfile.lock
@@ -0,0 +1,218 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ CFPropertyList (3.0.6)
+ rexml
+ addressable (2.8.4)
+ public_suffix (>= 2.0.2, < 6.0)
+ artifactory (3.0.15)
+ atomos (0.1.3)
+ aws-eventstream (1.2.0)
+ aws-partitions (1.769.0)
+ aws-sdk-core (3.173.1)
+ aws-eventstream (~> 1, >= 1.0.2)
+ aws-partitions (~> 1, >= 1.651.0)
+ aws-sigv4 (~> 1.5)
+ jmespath (~> 1, >= 1.6.1)
+ aws-sdk-kms (1.64.0)
+ aws-sdk-core (~> 3, >= 3.165.0)
+ aws-sigv4 (~> 1.1)
+ aws-sdk-s3 (1.122.0)
+ aws-sdk-core (~> 3, >= 3.165.0)
+ aws-sdk-kms (~> 1)
+ aws-sigv4 (~> 1.4)
+ aws-sigv4 (1.5.2)
+ aws-eventstream (~> 1, >= 1.0.2)
+ babosa (1.0.4)
+ claide (1.1.0)
+ colored (1.2)
+ colored2 (3.1.2)
+ commander (4.6.0)
+ highline (~> 2.0.0)
+ declarative (0.0.20)
+ digest-crc (0.6.4)
+ rake (>= 12.0.0, < 14.0.0)
+ domain_name (0.5.20190701)
+ unf (>= 0.0.5, < 1.0.0)
+ dotenv (2.8.1)
+ emoji_regex (3.2.3)
+ excon (0.99.0)
+ faraday (1.10.3)
+ faraday-em_http (~> 1.0)
+ faraday-em_synchrony (~> 1.0)
+ faraday-excon (~> 1.1)
+ faraday-httpclient (~> 1.0)
+ faraday-multipart (~> 1.0)
+ faraday-net_http (~> 1.0)
+ faraday-net_http_persistent (~> 1.0)
+ faraday-patron (~> 1.0)
+ faraday-rack (~> 1.0)
+ faraday-retry (~> 1.0)
+ ruby2_keywords (>= 0.0.4)
+ faraday-cookie_jar (0.0.7)
+ faraday (>= 0.8.0)
+ http-cookie (~> 1.0.0)
+ faraday-em_http (1.0.0)
+ faraday-em_synchrony (1.0.0)
+ faraday-excon (1.1.0)
+ faraday-httpclient (1.0.1)
+ faraday-multipart (1.0.4)
+ multipart-post (~> 2)
+ faraday-net_http (1.0.1)
+ faraday-net_http_persistent (1.2.0)
+ faraday-patron (1.0.0)
+ faraday-rack (1.0.0)
+ faraday-retry (1.0.3)
+ faraday_middleware (1.2.0)
+ faraday (~> 1.0)
+ fastimage (2.2.7)
+ fastlane (2.213.0)
+ CFPropertyList (>= 2.3, < 4.0.0)
+ addressable (>= 2.8, < 3.0.0)
+ artifactory (~> 3.0)
+ aws-sdk-s3 (~> 1.0)
+ babosa (>= 1.0.3, < 2.0.0)
+ bundler (>= 1.12.0, < 3.0.0)
+ colored
+ commander (~> 4.6)
+ dotenv (>= 2.1.1, < 3.0.0)
+ emoji_regex (>= 0.1, < 4.0)
+ excon (>= 0.71.0, < 1.0.0)
+ faraday (~> 1.0)
+ faraday-cookie_jar (~> 0.0.6)
+ faraday_middleware (~> 1.0)
+ fastimage (>= 2.1.0, < 3.0.0)
+ gh_inspector (>= 1.1.2, < 2.0.0)
+ google-apis-androidpublisher_v3 (~> 0.3)
+ google-apis-playcustomapp_v1 (~> 0.1)
+ google-cloud-storage (~> 1.31)
+ highline (~> 2.0)
+ json (< 3.0.0)
+ jwt (>= 2.1.0, < 3)
+ mini_magick (>= 4.9.4, < 5.0.0)
+ multipart-post (>= 2.0.0, < 3.0.0)
+ naturally (~> 2.2)
+ optparse (~> 0.1.1)
+ plist (>= 3.1.0, < 4.0.0)
+ rubyzip (>= 2.0.0, < 3.0.0)
+ security (= 0.1.3)
+ simctl (~> 1.6.3)
+ terminal-notifier (>= 2.0.0, < 3.0.0)
+ terminal-table (>= 1.4.5, < 2.0.0)
+ tty-screen (>= 0.6.3, < 1.0.0)
+ tty-spinner (>= 0.8.0, < 1.0.0)
+ word_wrap (~> 1.0.0)
+ xcodeproj (>= 1.13.0, < 2.0.0)
+ xcpretty (~> 0.3.0)
+ xcpretty-travis-formatter (>= 0.0.3)
+ gh_inspector (1.1.3)
+ google-apis-androidpublisher_v3 (0.42.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-core (0.11.0)
+ addressable (~> 2.5, >= 2.5.1)
+ googleauth (>= 0.16.2, < 2.a)
+ httpclient (>= 2.8.1, < 3.a)
+ mini_mime (~> 1.0)
+ representable (~> 3.0)
+ retriable (>= 2.0, < 4.a)
+ rexml
+ webrick
+ google-apis-iamcredentials_v1 (0.17.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-playcustomapp_v1 (0.13.0)
+ google-apis-core (>= 0.11.0, < 2.a)
+ google-apis-storage_v1 (0.19.0)
+ google-apis-core (>= 0.9.0, < 2.a)
+ google-cloud-core (1.6.0)
+ google-cloud-env (~> 1.0)
+ google-cloud-errors (~> 1.0)
+ google-cloud-env (1.6.0)
+ faraday (>= 0.17.3, < 3.0)
+ google-cloud-errors (1.3.1)
+ google-cloud-storage (1.44.0)
+ addressable (~> 2.8)
+ digest-crc (~> 0.4)
+ google-apis-iamcredentials_v1 (~> 0.1)
+ google-apis-storage_v1 (~> 0.19.0)
+ google-cloud-core (~> 1.6)
+ googleauth (>= 0.16.2, < 2.a)
+ mini_mime (~> 1.0)
+ googleauth (1.5.2)
+ faraday (>= 0.17.3, < 3.a)
+ jwt (>= 1.4, < 3.0)
+ memoist (~> 0.16)
+ multi_json (~> 1.11)
+ os (>= 0.9, < 2.0)
+ signet (>= 0.16, < 2.a)
+ highline (2.0.3)
+ http-cookie (1.0.5)
+ domain_name (~> 0.5)
+ httpclient (2.8.3)
+ jmespath (1.6.2)
+ json (2.6.3)
+ jwt (2.7.0)
+ memoist (0.16.2)
+ mini_magick (4.12.0)
+ mini_mime (1.1.2)
+ multi_json (1.15.0)
+ multipart-post (2.3.0)
+ nanaimo (0.3.0)
+ naturally (2.2.1)
+ optparse (0.1.1)
+ os (1.1.4)
+ plist (3.7.0)
+ public_suffix (5.0.1)
+ rake (13.0.6)
+ representable (3.2.0)
+ declarative (< 0.1.0)
+ trailblazer-option (>= 0.1.1, < 0.2.0)
+ uber (< 0.2.0)
+ retriable (3.1.2)
+ rexml (3.2.5)
+ rouge (2.0.7)
+ ruby2_keywords (0.0.5)
+ rubyzip (2.3.2)
+ security (0.1.3)
+ signet (0.17.0)
+ addressable (~> 2.8)
+ faraday (>= 0.17.5, < 3.a)
+ jwt (>= 1.5, < 3.0)
+ multi_json (~> 1.10)
+ simctl (1.6.10)
+ CFPropertyList
+ naturally
+ terminal-notifier (2.0.0)
+ terminal-table (1.8.0)
+ unicode-display_width (~> 1.1, >= 1.1.1)
+ trailblazer-option (0.1.2)
+ tty-cursor (0.7.1)
+ tty-screen (0.8.1)
+ tty-spinner (0.9.3)
+ tty-cursor (~> 0.7)
+ uber (0.1.0)
+ unf (0.1.4)
+ unf_ext
+ unf_ext (0.0.8.2)
+ unicode-display_width (1.8.0)
+ webrick (1.8.1)
+ word_wrap (1.0.0)
+ xcodeproj (1.22.0)
+ CFPropertyList (>= 2.3.3, < 4.0)
+ atomos (~> 0.1.3)
+ claide (>= 1.0.2, < 2.0)
+ colored2 (~> 3.1)
+ nanaimo (~> 0.3.0)
+ rexml (~> 3.2.4)
+ xcpretty (0.3.0)
+ rouge (~> 2.0.7)
+ xcpretty-travis-formatter (1.0.1)
+ xcpretty (~> 0.2, >= 0.0.7)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ fastlane
+
+BUNDLED WITH
+ 2.1.4
diff --git a/ci/ios/upload-vm/README.md b/ci/ios/upload-vm/README.md
new file mode 100644
index 0000000000..0f97df1dec
--- /dev/null
+++ b/ci/ios/upload-vm/README.md
@@ -0,0 +1,17 @@
+## Gemfiles for uploading to Appstore Connect.
+These gemfiles specify the `fastlane` dependencies needed to run our script that uploads our iOS app
+to the AppStore. This should be done once when setting up the upload VM.
+
+To set up fastlane, you should invoke `bundle` like so:
+```bash
+bundle install
+```
+
+To run fastlane, one should use:
+```bash
+bundle exec fastlane $fastlane_command
+```
+
+To update fastlane dependencies, one should run `bundle update` on a local machine, verify that the
+changes are sound, and update the upload VM accordingly if they are.
+
diff --git a/ios/build.sh b/ios/build.sh
index 406cd5738b..647aee9f80 100755
--- a/ios/build.sh
+++ b/ios/build.sh
@@ -10,17 +10,6 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# Verify environment configuration
###########################################
-if [[ -z ${IOS_APPLE_ID-} ]]; then
- echo "The variable IOS_APPLE_ID is not set."
- exit 1
-fi
-
-if [[ -z ${IOS_APPLE_ID_PASSWORD-} ]]; then
- echo "The variable IOS_APPLE_ID_PASSWORD is not set."
- read -sp "IOS_APPLE_ID_PASSWORD = " IOS_APPLE_ID_PASSWORD
- echo ""
- export IOS_APPLE_ID_PASSWORD
-fi
# Provisioning profiles directory
if [[ -z ${IOS_PROVISIONING_PROFILES_DIR-} ]]; then
@@ -122,20 +111,3 @@ xcodebuild \
-archivePath "$XCODE_ARCHIVE_DIR" \
-exportOptionsPlist "$EXPORT_OPTIONS_PATH" \
-exportPath "$BUILD_OUTPUT_DIR"
-
-
-###########################################
-# Deploy to AppStore
-###########################################
-
-if [[ "${1:-""}" == "--deploy" ]]; then
- xcrun altool \
- --upload-app --verbose \
- --type ios \
- --file "$IPA_PATH" \
- --username "$IOS_APPLE_ID" \
- --password "$IOS_APPLE_ID_PASSWORD"
-else
- echo "Deployment to AppStore will not be performed."
- echo "Run with --deploy to upload the binary."
-fi