diff options
| author | Emīls <emils@mullvad.net> | 2023-06-13 16:46:47 +0200 |
|---|---|---|
| committer | Emīls <emils@mullvad.net> | 2023-08-31 12:55:50 +0200 |
| commit | 1372db81edfe2d18bcaf7684f4a8f23f205c06f8 (patch) | |
| tree | 2871f084c33e46084b7e4eee344fd5cf774c16ac | |
| parent | 2674538fbaab8b139b20431f2678e0111f8013ae (diff) | |
| download | mullvadvpn-1372db81edfe2d18bcaf7684f4a8f23f205c06f8.tar.xz mullvadvpn-1372db81edfe2d18bcaf7684f4a8f23f205c06f8.zip | |
Add buildscripts for CI
| -rw-r--r-- | ci/ios/README.md | 20 | ||||
| -rw-r--r-- | ci/ios/build-app.sh | 26 | ||||
| -rw-r--r-- | ci/ios/buildserver-build-ios.sh | 55 | ||||
| -rw-r--r-- | ci/ios/run-build-and-upload.sh | 26 | ||||
| -rw-r--r-- | ci/ios/run-in-vm.sh | 30 | ||||
| -rw-r--r-- | ci/ios/upload-app.sh | 8 | ||||
| -rw-r--r-- | ci/ios/upload-vm/Gemfile | 3 | ||||
| -rw-r--r-- | ci/ios/upload-vm/Gemfile.lock | 218 | ||||
| -rw-r--r-- | ci/ios/upload-vm/README.md | 17 | ||||
| -rwxr-xr-x | ios/build.sh | 28 |
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 |
