Build, sign, and publish Apple apps for TestFlight distribution (#2285)

- Update `CODE_SIGN_STYLE=Manual`. You'll need to make sure to click
`Download manual profiles` in `Settings -> Account` in Xcode to have
them show up and be usable for local development. This is required to do
all this stuff from GitHub Actions.
- Sign the Apple app for distribution on each PR
- Publish the Apple app builds to App Store Connect on merges to `main`
This commit is contained in:
Jamil
2023-10-10 13:51:24 -07:00
committed by GitHub
parent dbb1dd4a3a
commit 0d411f60aa
13 changed files with 205 additions and 68 deletions

View File

@@ -19,6 +19,7 @@ jobs:
uses: ./.github/workflows/kotlin.yml
swift:
uses: ./.github/workflows/swift.yml
secrets: inherit
static-analysis:
uses: ./.github/workflows/static-analysis.yml
terraform:

View File

@@ -2,6 +2,10 @@ name: Swift
on:
workflow_call:
env:
# TODO: Change this to "prod" when app is released
CLIENT_ENV: staging
jobs:
build:
runs-on: macos-13
@@ -37,20 +41,137 @@ jobs:
key: ${{ matrix.target.platform }}-swift-${{ hashFiles('swift/*', 'rust/**/*.rs', 'rust/**/*.toml', 'rust/**/*.lock}') }}
restore-keys: |
${{ matrix.target.platform }}-swift-
- name: Build app
- name: Install the Apple build certificate and provisioning profile
env:
ONLY_ACTIVE_ARCH: no
BUILD_CERT: ${{ secrets.APPLE_BUILD_CERTIFICATE_BASE64 }}
BUILD_CERT_PASS: ${{ secrets.APPLE_BUILD_CERTIFICATE_P12_PASSWORD }}
INSTALLER_CERT: ${{ secrets.APPLE_MAC_INSTALLER_CERTIFICATE_BASE64 }}
INSTALLER_CERT_PASS: ${{ secrets.APPLE_MAC_INSTALLER_CERTIFICATE_P12_PASSWORD }}
KEYCHAIN_PASS: ${{ secrets.APPLE_RUNNER_KEYCHAIN_PASSWORD }}
IOS_APP_PP: ${{ secrets.APPLE_IOS_APP_PROVISIONING_PROFILE }}
IOS_NE_PP: ${{ secrets.APPLE_IOS_NE_PROVISIONING_PROFILE }}
MACOS_APP_PP: ${{ secrets.APPLE_MACOS_APP_PROVISIONING_PROFILE }}
MACOS_NE_PP: ${{ secrets.APPLE_MACOS_NE_PROVISIONING_PROFILE }}
run: |
BUILD_CERT_PATH=$RUNNER_TEMP/build_certificate.p12
INSTALLER_CERT_PATH=$RUNNER_TEMP/installer_certificate.cer
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
PP_PATH=~/Library/MobileDevice/Provisioning\ Profiles
mkdir -p "$PP_PATH"
# import certificate and provisioning profiles from secrets
echo -n "$BUILD_CERT" | base64 --decode -o $BUILD_CERT_PATH
# Matrix won't let us access secrets (for good reason), so use an explicit conditional here instead
if [ "${{ matrix.target.platform }}" = "iOS" ]; then
echo -n "$IOS_APP_PP" | base64 --decode -o "$PP_PATH"/app.mobileprovision
echo -n "$IOS_NE_PP" | base64 --decode -o "$PP_PATH"/ne.mobileprovision
elif [ "${{ matrix.target.platform }}" = "macOS" ]; then
echo -n "$MACOS_APP_PP" | base64 --decode -o "$PP_PATH"/app.provisionprofile
echo -n "$MACOS_NE_PP" | base64 --decode -o "$PP_PATH"/ne.provisionprofile
# Submission to the macOS app store requires an installer package
# which must be signed separately.
echo -n "$INSTALLER_CERT" | base64 --decode -o $INSTALLER_CERT_PATH
else
echo "Platform not supported"
exit 1
fi
# create temporary keychain
security create-keychain -p "$KEYCHAIN_PASS" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASS" $KEYCHAIN_PATH
# import certificate to keychain
security import $BUILD_CERT_PATH -P "$BUILD_CERT_PASS" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
if [ "${{ matrix.target.platform }}" = "macOS" ]; then
security import $INSTALLER_CERT_PATH -P "$INSTALLER_CERT_PASS" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
fi
security list-keychain -d user -s $KEYCHAIN_PATH
- name: Build and sign app
id: build
env:
# Build universal binary
ONLY_ACTIVE_ARCH: no
# Needed because `productbuild` doesn't support picking this up automatically like Xcode does
INSTALLER_CODE_SIGN_IDENTITY: "3rd Party Mac Developer Installer: Firezone, Inc. (47R2M6779T)"
run: |
# Explicitly select Xcode version to use
sudo xcode-select -s /Applications/Xcode_14.3.app
# Needed by the linter built into the build process
brew install swift-format
cp Firezone/xcconfig/Developer.xcconfig.ci-${{ matrix.target.platform }} Firezone/xcconfig/Developer.xcconfig
cp Firezone/xcconfig/Server.xcconfig.ci Firezone/xcconfig/Server.xcconfig
xcodebuild archive \
-configuration Release \
-scheme Firezone \
-sdk ${{ matrix.target.sdk }} \
-destination '${{ matrix.target.destination }}' \
CODE_SIGNING_ALLOWED=no
# Copy xcconfig templates
cp Firezone/xcconfig/Developer.xcconfig.firezone Firezone/xcconfig/Developer.xcconfig
cp Firezone/xcconfig/Server.xcconfig.${{ env.CLIENT_ENV }} Firezone/xcconfig/Server.xcconfig
# App Store Connect requires a new build version on each upload and it must be an integer.
# See https://developer.apple.com/documentation/xcode/build-settings-reference#Current-Project-Version
seconds_since_epoch=$(date +%s)
sed -i '' "s/CURRENT_PROJECT_VERSION = [0-9]/CURRENT_PROJECT_VERSION = $seconds_since_epoch/" \
Firezone.xcodeproj/project.pbxproj
# Unfortunately the macOS app requires an installer package to make it into the App Store,
# while iOS requires an ipa. The process for building each of these is slightly different.
if [ "${{ matrix.target.platform }}" = "iOS" ]; then
# Build archive
xcodebuild archive \
-archivePath ~/Firezone.xcarchive \
-configuration Release \
-scheme Firezone \
-sdk ${{ matrix.target.sdk }} \
-destination '${{ matrix.target.destination }}'
# Export IPA
xcodebuild \
-exportArchive \
-archivePath ~/Firezone.xcarchive \
-exportPath ~/ \
-exportOptionsPlist Firezone/ExportOptions.plist
# Save resulting file to use for upload
echo "app_bundle=~/Firezone.ipa" >> "$GITHUB_OUTPUT"
elif [ "${{ matrix.target.platform }}" = "macOS" ]; then
# Build app bundle
xcodebuild build \
-configuration Release \
-scheme Firezone \
-sdk ${{ matrix.target.sdk }} \
-destination '${{ matrix.target.destination }}'
# Move it from randomized build output dir to somewhere we can find it
mv ~/Library/Developer/Xcode/DerivedData/Firezone-*/Build/Products/Release/Firezone.app ~/.
# Create signed installer pkg
productbuild \
--sign "${{ env.INSTALLER_CODE_SIGN_IDENTITY }}" \
--component ~/Firezone.app /Applications ~/Firezone.pkg
# Save resulting file to use for upload
echo "app_bundle=~/Firezone.pkg" >> "$GITHUB_OUTPUT"
else
echo "Unsupported platform"
exit 1
fi
- name: Upload build to App Store Connect
if: ${{ github.ref == 'refs/heads/main' }}
env:
ISSUER_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_ISSUER_ID }}
API_KEY_ID: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY_ID }}
API_KEY: ${{ secrets.APPLE_APP_STORE_CONNECT_API_KEY }}
run: |
# set up private key from env
mkdir -p ~/private_keys
echo "$API_KEY" > ~/private_keys/AuthKey_$API_KEY_ID.p8
# Submit app to App Store Connect
xcrun altool \
--upload-app \
-f ${{ steps.build.outputs.app_bundle }} \
-t ${{ matrix.target.platform }} \
--apiKey $API_KEY_ID \
--apiIssuer $ISSUER_ID
- uses: actions/cache/save@v3
if: ${{ github.ref == 'refs/heads/main' }}
name: Save Swift DerivedData Cache

View File

@@ -1,8 +1,10 @@
# Format:
# MAJOR: This is "1" for now. Don't change it.
# MINOR: This is the current version of the portal API in YYYYMMDD format. Consumers (connlib, REST) will request
# MAJOR: This is the marketing version, e.g. 1. Don't change it.
# MINOR: This is the current version of the portal API in YYYYMMDD format. REST consumers will request
# this API from the portal with the X-Firezone-API-Version request header.
# PATCH: Increment this each time you want to publish a new Firezone version.
# Increment this for breaking API changes (e.g. once a quarter)
# PATCH: Increment this for each backwards-compatible release
# See discussion here: https://github.com/firezone/firezone/issues/2041
version = 1.20231001.0
.PHONY: version
@@ -14,10 +16,10 @@ SEDARG := -i
endif
version:
# Elixir can set its Application version from a file, but other components aren't so flexible.
@# Elixir can set its Application version from a file, but other components aren't so flexible.
@echo $(version) > elixir/VERSION
@find rust/ -name "Cargo.toml" -exec sed $(SEDARG) -e '/mark:automatic-version/{n;s/[0-9]*\.[0-9]*\.[0-9]*/$(version)/;}' {} \;
@find .github/ -name "*.yml" -exec sed $(SEDARG) -e '/mark:automatic-version/{n;s/[0-9]*\.[0-9]*\.[0-9]*/$(version)/;}' {} \;
@find swift/ -name "project.pbxproj" -exec sed $(SEDARG) -e 's/MARKETING_VERSION = .*;/MARKETING_VERSION = $(version);/' {} \;
@find kotlin/ -name "build.gradle.kts" -exec sed $(SEDARG) -e '/mark:automatic-version/{n;s/versionName =.*/versionName = "$(version)"/;}' {} \;
@find kotlin/ -name "*.gradle.kts" -exec sed $(SEDARG) -e '/mark:automatic-version/{n;s/versionName =.*/versionName = "$(version)"/;}' {} \;
@cd rust && cargo check

View File

@@ -541,8 +541,9 @@
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = FirezoneNetworkExtension/FirezoneNetworkExtension_iOS.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CODE_SIGN_IDENTITY = "Apple Distribution: Firezone, Inc. (47R2M6779T)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 0;
FRAMEWORK_SEARCH_PATHS = "";
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = FirezoneNetworkExtension/Info.plist;
@@ -564,6 +565,8 @@
OTHER_LDFLAGS = "-lconnlib";
PRODUCT_BUNDLE_IDENTIFIER = "${APP_ID}.network-extension";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "23402aaa-f72c-4947-a795-23a9cf495968";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
@@ -583,8 +586,9 @@
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = FirezoneNetworkExtension/FirezoneNetworkExtension_iOS.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CODE_SIGN_IDENTITY = "Apple Distribution: Firezone, Inc. (47R2M6779T)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 0;
FRAMEWORK_SEARCH_PATHS = "";
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = FirezoneNetworkExtension/Info.plist;
@@ -605,6 +609,8 @@
OTHER_LDFLAGS = "-lconnlib";
PRODUCT_BUNDLE_IDENTIFIER = "${APP_ID}.network-extension";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "23402aaa-f72c-4947-a795-23a9cf495968";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = "iphonesimulator iphoneos";
@@ -624,10 +630,9 @@
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = FirezoneNetworkExtension/FirezoneNetworkExtension_macOS.entitlements;
CODE_SIGN_IDENTITY = "-";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CODE_SIGN_IDENTITY = "Apple Distribution: Firezone, Inc. (47R2M6779T)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 0;
DEAD_CODE_STRIPPING = YES;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -647,6 +652,8 @@
OTHER_LDFLAGS = "-lconnlib";
PRODUCT_BUNDLE_IDENTIFIER = "${APP_ID}.network-extension";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "1e965794-0b2c-46d3-a955-d96aacf25546";
SDKROOT = macosx;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = macosx;
@@ -665,10 +672,9 @@
buildSettings = {
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = FirezoneNetworkExtension/FirezoneNetworkExtension_macOS.entitlements;
CODE_SIGN_IDENTITY = "-";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CODE_SIGN_IDENTITY = "Apple Distribution: Firezone, Inc. (47R2M6779T)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 0;
DEAD_CODE_STRIPPING = YES;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -688,6 +694,8 @@
OTHER_LDFLAGS = "-lconnlib";
PRODUCT_BUNDLE_IDENTIFIER = "${APP_ID}.network-extension";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "1e965794-0b2c-46d3-a955-d96aacf25546";
SDKROOT = macosx;
SKIP_INSTALL = YES;
SUPPORTED_PLATFORMS = macosx;
@@ -852,11 +860,9 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Firezone/Firezone.entitlements;
CODE_SIGN_IDENTITY = "-";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CODE_SIGN_IDENTITY = "Apple Distribution: Firezone, Inc. (47R2M6779T)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 0;
DEAD_CODE_STRIPPING = YES;
DEFINES_MODULE = NO;
DEVELOPMENT_ASSET_PATHS = "\"Firezone/Preview Content\"";
@@ -882,6 +888,9 @@
MARKETING_VERSION = 1.20231001.0;
PRODUCT_BUNDLE_IDENTIFIER = "${APP_ID}";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "b32a5853-699d-4f19-85d3-5b13b1ac5dbb";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "70055d90-0252-4ee5-a60c-4d6f3840ee62";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -904,11 +913,9 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Firezone/Firezone.entitlements;
CODE_SIGN_IDENTITY = "-";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CODE_SIGN_IDENTITY = "Apple Distribution: Firezone, Inc. (47R2M6779T)";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 0;
DEAD_CODE_STRIPPING = YES;
DEFINES_MODULE = NO;
DEVELOPMENT_ASSET_PATHS = "\"Firezone/Preview Content\"";
@@ -935,6 +942,9 @@
PRODUCT_BUNDLE_IDENTIFIER = "${APP_ID}";
PRODUCT_MODULE_NAME = "$(PRODUCT_NAME:c99extidentifier)";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
"PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = "b32a5853-699d-4f19-85d3-5b13b1ac5dbb";
"PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "70055d90-0252-4ee5-a60c-4d6f3840ee62";
SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator macosx";
SWIFT_EMIT_LOC_STRINGS = YES;

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>provisioningProfiles</key>
<dict>
<key>dev.firezone.firezone</key>
<string>b32a5853-699d-4f19-85d3-5b13b1ac5dbb</string>
<key>dev.firezone.firezone.network-extension</key>
<string>23402aaa-f72c-4947-a795-23a9cf495968</string>
</dict>
</dict>
</plist>

View File

@@ -25,5 +25,7 @@
<string>$(CONTROL_PLANE_URL_SCHEME)</string>
<key>ControlPlaneURLHost</key>
<string>$(CONTROL_PLANE_URL_HOST)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
</dict>
</plist>

View File

@@ -1,2 +0,0 @@
DEVELOPMENT_TEAM = 0000000000
APP_ID = dev.firezone.ios

View File

@@ -1,2 +0,0 @@
DEVELOPMENT_TEAM = 0000000000
APP_ID = dev.firezone.macos

View File

@@ -0,0 +1,2 @@
DEVELOPMENT_TEAM = 47R2M6779T
APP_ID = dev.firezone.firezone

View File

@@ -0,0 +1,4 @@
AUTH_URL_SCHEME = http
AUTH_URL_HOST = localhost:8080
CONTROL_PLANE_URL_SCHEME = ws
CONTROL_PLANE_URL_HOST = localhost:8081

View File

@@ -1,8 +1,3 @@
// Configure the Authentication URL and the Control Plane URL
// For staging:
AUTH_URL_SCHEME = https
AUTH_URL_HOST = app.firez.one
CONTROL_PLANE_URL_SCHEME = wss

View File

@@ -1,25 +1,13 @@
// Configure the Authentication URL and the Control Plane URL
// This file configures the Authentication URL and the Control Plane URL
// http or https
AUTH_URL_SCHEME = <auth_uri_scheme>
// For development:
// auth host and optional port, e.g. app.firezone.dev:443
AUTH_URL_HOST = <auth_uri_host>
// AUTH_URL_SCHEME = http
// AUTH_URL_HOST = localhost:8080
// CONTROL_PLANE_URL_SCHEME = ws
// CONTROL_PLANE_URL_HOST = localhost:8081
// websocket scheme, ws or wss
CONTROL_PLANE_URL_SCHEME = <control_plane_websocket_uri_scheme>
// For staging:
// AUTH_URL_SCHEME = https
// AUTH_URL_HOST = app.firez.one
// CONTROL_PLANE_URL_SCHEME = wss
// CONTROL_PLANE_URL_HOST = api.firez.one
// For production:
// AUTH_URL_SCHEME = https
// AUTH_URL_HOST = app.firezone.dev
// CONTROL_PLANE_URL_SCHEME = wss
// CONTROL_PLANE_URL_HOST = api.firezone.dev
// control plane host and optional port, e.g. app.firezone.dev:443
CONTROL_PLANE_URL_HOST = <control_plane_websocket_uri_host>

View File

@@ -23,7 +23,7 @@ Firezone app clients for macOS and iOS.
1. Rename and populate xcconfig files:
```bash
cp Firezone/xcconfig/Developer.xcconfig.template Firezone/xcconfig/Developer.xcconfig
cp Firezone/xcconfig/Developer.xcconfig.firezone Firezone/xcconfig/Developer.xcconfig
vim Firezone/xcconfig/Developer.xcconfig
```