#!/usr/bin/env bash set -euo pipefail repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/../.." && pwd)" cd "$repo_root" platform="${1:-all}" build_number="${BUILD_NUMBER:-${RELEASE_BUILD_NUMBER:-${RELEASE_REF:-manual}}}" out_root="${BURROW_RELEASE_OUT:-${repo_root}/dist/builds/${build_number}}" apple_root="${out_root}/apple" archives_root="${apple_root}/archives" derived_data="${BURROW_DERIVED_DATA_PATH:-${repo_root}/.derived-data/apple}" source_packages="${BURROW_SWIFTPM_CACHE_PATH:-${XDG_CACHE_HOME:-${HOME}/.cache}/burrow/swiftpm}" project="Apple/Burrow.xcodeproj" scheme="${BURROW_APPLE_SCHEME:-App}" bundle_id="${BURROW_APPLE_BUNDLE_ID:-net.burrow.app}" network_extension_bundle_id="${BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID:-${bundle_id}.network}" signing_ready="${BURROW_APPLE_SIGNING_READY:-0}" signing_mode="${BURROW_APPLE_SIGNING_MODE:-keychain}" require_signing="${BURROW_APPLE_REQUIRE_SIGNING:-false}" sparkle_feed_url="${BURROW_SPARKLE_FEED_URL:-https://releases.burrow.net/sparkle/appcast.xml}" sparkle_public_ed_key="${BURROW_SPARKLE_PUBLIC_ED_KEY:-uugZuJeqvvKd91NZ6F1Fv2cQenUbIG/ZW3L9MuaEz30=}" sparkle_sign_with_kms="${BURROW_SPARKLE_SIGN_WITH_KMS:-false}" notarize_macos="${BURROW_NOTARIZE_MACOS:-false}" mkdir -p "$apple_root" "$archives_root" "$derived_data" "$source_packages" truthy() { case "${1:-}" in 1|true|TRUE|True|yes|YES|Yes|y|Y|on|ON|On) return 0 ;; *) return 1 ;; esac } sha256_file() { local file="$1" if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$file" > "${file}.sha256" else sha256sum "$file" > "${file}.sha256" fi } safe_profile_slug() { python3 - "$1" <<'PY' import re import sys print(re.sub(r"[^A-Za-z0-9]", "", sys.argv[1]).lower()) PY } profile_for() { local identifier="$1" local suffix="$2" local slug path slug="$(safe_profile_slug "$identifier")" for path in \ ".forgejo/actions/export/${slug}${suffix}.provisionprofile" \ ".forgejo/actions/export/${slug}${suffix}.mobileprovision" \ "Apple/Profiles/${slug}${suffix}.provisionprofile" \ "Apple/Profiles/${slug}${suffix}.mobileprovision"; do if [[ -s "$path" ]]; then printf '%s\n' "$path" return 0 fi done return 1 } generate_entitlements_from_profile() { local profile="$1" local output="$2" local template="${3:-}" local args=("$profile" --output "$output") if [[ -n "$template" ]]; then args+=(--template "$template") fi Scripts/apple/profile-entitlements.py "${args[@]}" } apple_signing_available() { if [[ "$signing_mode" == "google-kms" || "$signing_mode" == "google-kms-staged" || "$signing_mode" == "kms" ]]; then return 0 fi [[ "$signing_ready" == "1" ]] } kms_sign_bundle() { local family="$1" local bundle_path="$2" local entitlements="$3" local runtime_flag=() if [[ "$family" == "developer-id" ]]; then runtime_flag=(--runtime) fi Scripts/apple/sign-with-google-kms.sh \ --family "$family" \ --path "$bundle_path" \ --entitlements "$entitlements" \ "${runtime_flag[@]}" } kms_sign_file() { local family="$1" local file_path="$2" local runtime_flag=() if [[ "$family" == "developer-id" ]]; then runtime_flag=(--runtime) fi Scripts/apple/sign-with-google-kms.sh \ --family "$family" \ --path "$file_path" \ "${runtime_flag[@]}" } write_version() { mkdir -p Apple/Configuration printf 'CURRENT_PROJECT_VERSION = %s\n' "$build_number" > Apple/Configuration/Version.xcconfig } xcode_common_args=( -project "$project" -scheme "$scheme" -configuration Release -derivedDataPath "$derived_data" -clonedSourcePackagesDirPath "$source_packages" CURRENT_PROJECT_VERSION="$build_number" APP_BUNDLE_IDENTIFIER="$bundle_id" NETWORK_EXTENSION_BUNDLE_IDENTIFIER="$network_extension_bundle_id" BURROW_SPARKLE_FEED_URL="$sparkle_feed_url" BURROW_SPARKLE_PUBLIC_ED_KEY="$sparkle_public_ed_key" ) xcode_auth_args=() if [[ -n "${ASC_API_KEY_ID:-}" && -n "${ASC_API_ISSUER_ID:-}" && -n "${ASC_API_KEY_PATH:-}" && -f "${ASC_API_KEY_PATH:-}" ]]; then xcode_auth_args+=( -authenticationKeyID "$ASC_API_KEY_ID" -authenticationKeyIssuerID "$ASC_API_ISSUER_ID" -authenticationKeyPath "$ASC_API_KEY_PATH" ) fi if [[ -n "${BURROW_APPLE_KEYCHAIN_PATH:-}" ]]; then xcode_common_args+=(OTHER_CODE_SIGN_FLAGS="--keychain ${BURROW_APPLE_KEYCHAIN_PATH}") fi require_xcodebuild() { if ! command -v xcodebuild >/dev/null 2>&1; then echo "xcodebuild is required for Apple release artifacts" >&2 exit 1 fi xcodebuild -version xcrun --find swiftc >/dev/null } unsigned_note() { local platform_name="$1" local artifact_name="$2" { echo "Burrow ${platform_name} signing assets were not available." echo echo "The lane built an unsigned validation artifact instead of an uploadable release artifact." echo "Seal the Apple release credentials with agenix and let CI sync provisioning profiles from App Store Connect to produce ${artifact_name}." } > "${apple_root}/README-${platform_name}-unsigned.txt" } ensure_signing_or_note() { local platform_name="$1" local artifact_name="$2" if apple_signing_available; then return 0 fi if truthy "$require_signing"; then echo "Apple signing is required for ${platform_name}, but signing assets are not configured." >&2 exit 1 fi unsigned_note "$platform_name" "$artifact_name" return 1 } write_export_options() { local method="$1" local path="$2" cat > "$path" < method ${method} destination export signingStyle automatic stripSwiftSymbols teamID ${DEVELOPMENT_TEAM:-P6PV2R9443} EOF } build_ios_unsigned() { local product_dir zip_path xcodebuild build \ "${xcode_common_args[@]}" \ -destination "generic/platform=iOS Simulator" \ ARCHS=arm64 \ ONLY_ACTIVE_ARCH=YES \ CODE_SIGNING_ALLOWED=NO \ CODE_SIGNING_REQUIRED=NO product_dir="${derived_data}/Build/Products/Release-iphonesimulator" zip_path="${apple_root}/Burrow-iOS-simulator-${build_number}.unsigned.zip" if [[ -d "${product_dir}/Burrow.app" ]]; then (cd "$product_dir" && zip -qr "$zip_path" Burrow.app) sha256_file "$zip_path" fi } build_ios_release() { local archive_path export_dir export_options ipa if ! ensure_signing_or_note "iOS" "Burrow-iOS-${build_number}.ipa"; then build_ios_unsigned return 0 fi if [[ "$signing_mode" == "google-kms-staged" ]]; then build_ios_kms_staged return 0 fi if [[ "$signing_mode" == "google-kms" || "$signing_mode" == "kms" ]]; then build_ios_kms_release return 0 fi archive_path="${archives_root}/Burrow-iOS-${build_number}.xcarchive" export_dir="${apple_root}/ios-export" export_options="${apple_root}/ExportOptions-iOS.plist" write_export_options "app-store-connect" "$export_options" xcodebuild archive \ "${xcode_common_args[@]}" \ -destination "generic/platform=iOS" \ -archivePath "$archive_path" \ ARCHS=arm64 \ -allowProvisioningUpdates \ "${xcode_auth_args[@]}" xcodebuild -exportArchive \ -archivePath "$archive_path" \ -exportPath "$export_dir" \ -exportOptionsPlist "$export_options" \ -allowProvisioningUpdates \ "${xcode_auth_args[@]}" ipa="$(find "$export_dir" -maxdepth 1 -type f -name '*.ipa' | sort | head -n 1 || true)" if [[ -z "$ipa" ]]; then echo "iOS export completed but did not produce an IPA" >&2 exit 1 fi cp "$ipa" "${apple_root}/Burrow-iOS-${build_number}.ipa" sha256_file "${apple_root}/Burrow-iOS-${build_number}.ipa" } build_ios_kms_staged() { local product_dir source_app stage_dir ipa xcodebuild build \ "${xcode_common_args[@]}" \ -destination "generic/platform=iOS" \ ARCHS=arm64 \ ONLY_ACTIVE_ARCH=YES \ SKIP_INSTALL=NO \ CODE_SIGNING_ALLOWED=NO \ CODE_SIGNING_REQUIRED=NO \ CODE_SIGN_IDENTITY= product_dir="${derived_data}/Build/Products/Release-iphoneos" source_app="${product_dir}/Burrow.app" if [[ ! -d "$source_app" ]]; then echo "iOS build completed but did not produce Burrow.app" >&2 exit 1 fi stage_dir="${apple_root}/ios-unsigned/Payload" rm -rf "$stage_dir" mkdir -p "$stage_dir" ditto "$source_app" "${stage_dir}/Burrow.app" ipa="${apple_root}/Burrow-iOS-${build_number}.unsigned.ipa" rm -f "$ipa" (cd "${apple_root}/ios-unsigned" && zip -qry "$ipa" Payload) sha256_file "$ipa" } build_ios_kms_release() { local product_dir source_app stage_dir app ext app_profile ext_profile app_entitlements ext_entitlements ipa xcodebuild build \ "${xcode_common_args[@]}" \ -destination "generic/platform=iOS" \ ARCHS=arm64 \ ONLY_ACTIVE_ARCH=YES \ SKIP_INSTALL=NO \ CODE_SIGNING_ALLOWED=NO \ CODE_SIGNING_REQUIRED=NO \ CODE_SIGN_IDENTITY= product_dir="${derived_data}/Build/Products/Release-iphoneos" source_app="${product_dir}/Burrow.app" if [[ ! -d "$source_app" ]]; then echo "iOS build completed but did not produce Burrow.app" >&2 exit 1 fi stage_dir="${apple_root}/ios-kms/Payload" rm -rf "$stage_dir" mkdir -p "$stage_dir" ditto "$source_app" "${stage_dir}/Burrow.app" app="${stage_dir}/Burrow.app" ext="${app}/PlugIns/BurrowNetworkExtension.appex" if [[ ! -d "$ext" ]]; then echo "iOS app is missing BurrowNetworkExtension.appex" >&2 exit 1 fi app_profile="$(profile_for "$bundle_id" "appstore")" || { echo "missing iOS App Store provisioning profile for ${bundle_id}" >&2 exit 1 } ext_profile="$(profile_for "$network_extension_bundle_id" "appstore")" || { echo "missing iOS App Store provisioning profile for ${network_extension_bundle_id}" >&2 exit 1 } cp "$app_profile" "${app}/embedded.mobileprovision" cp "$ext_profile" "${ext}/embedded.mobileprovision" app_entitlements="${apple_root}/ios-kms/app-entitlements.plist" ext_entitlements="${apple_root}/ios-kms/extension-entitlements.plist" generate_entitlements_from_profile "$app_profile" "$app_entitlements" Apple/App/App-iOS.entitlements generate_entitlements_from_profile "$ext_profile" "$ext_entitlements" Apple/NetworkExtension/NetworkExtension-iOS.entitlements rm -rf "${app}/_CodeSignature" "${ext}/_CodeSignature" kms_sign_bundle ios "$ext" "$ext_entitlements" kms_sign_bundle ios "$app" "$app_entitlements" ipa="${apple_root}/Burrow-iOS-${build_number}.ipa" rm -f "$ipa" (cd "${apple_root}/ios-kms" && zip -qry "$ipa" Payload) sha256_file "$ipa" } build_macos_unsigned() { local product_dir zip_path xcodebuild build \ "${xcode_common_args[@]}" \ -destination "platform=macOS" \ CODE_SIGNING_ALLOWED=NO \ CODE_SIGNING_REQUIRED=NO product_dir="${derived_data}/Build/Products/Release" zip_path="${apple_root}/Burrow-macOS-${build_number}.unsigned.zip" if [[ -d "${product_dir}/Burrow.app" ]]; then ditto -c -k --keepParent "${product_dir}/Burrow.app" "$zip_path" sha256_file "$zip_path" fi } build_macos_release() { local archive_path export_dir export_options app dmg channel sparkle_dir length pubdate url if ! ensure_signing_or_note "macOS" "Burrow-macOS-${build_number}.dmg"; then build_macos_unsigned return 0 fi if [[ "$signing_mode" == "google-kms-staged" ]]; then build_macos_kms_staged return 0 fi if [[ "$signing_mode" == "google-kms" || "$signing_mode" == "kms" ]]; then build_macos_kms_release return 0 fi archive_path="${archives_root}/Burrow-macOS-${build_number}.xcarchive" export_dir="${apple_root}/macos-export" export_options="${apple_root}/ExportOptions-macOS.plist" write_export_options "${BURROW_MACOS_EXPORT_METHOD:-developer-id}" "$export_options" xcodebuild archive \ "${xcode_common_args[@]}" \ -destination "generic/platform=macOS" \ -archivePath "$archive_path" \ -allowProvisioningUpdates \ "${xcode_auth_args[@]}" xcodebuild -exportArchive \ -archivePath "$archive_path" \ -exportPath "$export_dir" \ -exportOptionsPlist "$export_options" \ -allowProvisioningUpdates \ "${xcode_auth_args[@]}" app="$(find "$export_dir" -maxdepth 2 -type d -name 'Burrow.app' | sort | head -n 1 || true)" if [[ -z "$app" ]]; then echo "macOS export completed but did not produce Burrow.app" >&2 exit 1 fi dmg="${apple_root}/Burrow-macOS-${build_number}.dmg" rm -f "$dmg" hdiutil create -volname "Burrow ${build_number}" -srcfolder "$app" -ov -format UDZO "$dmg" sha256_file "$dmg" channel="${SPARKLE_CHANNEL:-build-${build_number}}" sparkle_dir="${out_root}/sparkle/${channel}" mkdir -p "$sparkle_dir" cp "$dmg" "$sparkle_dir/" length="$(wc -c < "$dmg" | tr -d ' ')" pubdate="$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S +0000')" url="${SPARKLE_DOWNLOAD_PREFIX:-https://releases.burrow.net/sparkle}/${channel}/$(basename "$dmg")" cat > "${sparkle_dir}/appcast.xml" < Burrow ${channel} Burrow ${build_number} ${pubdate} ${build_number} 0.1 EOF if truthy "$sparkle_sign_with_kms"; then Scripts/sparkle/sign-appcast-kms.py \ --appcast "${sparkle_dir}/appcast.xml" \ --artifact-dir "$sparkle_dir" fi } build_macos_kms_staged() { local product_dir source_app zip_path xcodebuild build \ "${xcode_common_args[@]}" \ -destination "platform=macOS" \ CODE_SIGNING_ALLOWED=NO \ CODE_SIGNING_REQUIRED=NO \ CODE_SIGN_IDENTITY= product_dir="${derived_data}/Build/Products/Release" source_app="${product_dir}/Burrow.app" if [[ ! -d "$source_app" ]]; then echo "macOS build completed but did not produce Burrow.app" >&2 exit 1 fi zip_path="${apple_root}/Burrow-macOS-${build_number}.unsigned.zip" rm -f "$zip_path" ditto -c -k --keepParent "$source_app" "$zip_path" sha256_file "$zip_path" } write_sparkle_appcast() { local dmg="$1" local channel sparkle_dir length pubdate url channel="${SPARKLE_CHANNEL:-build-${build_number}}" sparkle_dir="${out_root}/sparkle/${channel}" mkdir -p "$sparkle_dir" cp "$dmg" "$sparkle_dir/" length="$(wc -c < "$dmg" | tr -d ' ')" pubdate="$(LC_ALL=C date -u '+%a, %d %b %Y %H:%M:%S +0000')" url="${SPARKLE_DOWNLOAD_PREFIX:-https://releases.burrow.net/sparkle}/${channel}/$(basename "$dmg")" cat > "${sparkle_dir}/appcast.xml" < Burrow ${channel} Burrow ${build_number} ${pubdate} ${build_number} 0.1 EOF if truthy "$sparkle_sign_with_kms"; then Scripts/sparkle/sign-appcast-kms.py \ --appcast "${sparkle_dir}/appcast.xml" \ --artifact-dir "$sparkle_dir" fi } notarize_dmg_if_requested() { local dmg="$1" local key_json if ! truthy "$notarize_macos"; then return 0 fi if [[ -z "${ASC_API_KEY_ID:-}" || -z "${ASC_API_ISSUER_ID:-}" || -z "${ASC_API_KEY_PATH:-}" ]]; then echo "macOS notarization requires ASC_API_KEY_ID, ASC_API_ISSUER_ID, and ASC_API_KEY_PATH" >&2 exit 1 fi key_json="${apple_root}/notary-api-key.json" rcodesign encode-app-store-connect-api-key \ --output-path "$key_json" \ "$ASC_API_ISSUER_ID" \ "$ASC_API_KEY_ID" \ "$ASC_API_KEY_PATH" rcodesign notary-submit --api-key-file "$key_json" --wait --staple "$dmg" rm -f "$key_json" } build_macos_kms_release() { local product_dir source_app app ext app_profile ext_profile app_entitlements ext_entitlements dmg xcodebuild build \ "${xcode_common_args[@]}" \ -destination "platform=macOS" \ CODE_SIGNING_ALLOWED=NO \ CODE_SIGNING_REQUIRED=NO \ CODE_SIGN_IDENTITY= product_dir="${derived_data}/Build/Products/Release" source_app="${product_dir}/Burrow.app" if [[ ! -d "$source_app" ]]; then echo "macOS build completed but did not produce Burrow.app" >&2 exit 1 fi app="${apple_root}/macos-kms/Burrow.app" rm -rf "$app" mkdir -p "$(dirname "$app")" ditto "$source_app" "$app" ext="${app}/Contents/PlugIns/BurrowNetworkExtension.appex" app_profile="$(profile_for "$bundle_id" "developerid")" || { echo "missing macOS Developer ID provisioning profile for ${bundle_id}" >&2 exit 1 } cp "$app_profile" "${app}/Contents/embedded.provisionprofile" app_entitlements="${apple_root}/macos-kms/app-entitlements.plist" generate_entitlements_from_profile "$app_profile" "$app_entitlements" if [[ -d "$ext" ]]; then ext_profile="$(profile_for "$network_extension_bundle_id" "developerid")" || { echo "missing macOS Developer ID provisioning profile for ${network_extension_bundle_id}" >&2 exit 1 } cp "$ext_profile" "${ext}/Contents/embedded.provisionprofile" ext_entitlements="${apple_root}/macos-kms/extension-entitlements.plist" generate_entitlements_from_profile "$ext_profile" "$ext_entitlements" rm -rf "${ext}/Contents/_CodeSignature" kms_sign_bundle developer-id "$ext" "$ext_entitlements" fi rm -rf "${app}/Contents/_CodeSignature" kms_sign_bundle developer-id "$app" "$app_entitlements" dmg="${apple_root}/Burrow-macOS-${build_number}.dmg" rm -f "$dmg" hdiutil create -volname "Burrow ${build_number}" -srcfolder "$app" -ov -format UDZO "$dmg" kms_sign_file developer-id "$dmg" notarize_dmg_if_requested "$dmg" sha256_file "$dmg" write_sparkle_appcast "$dmg" } require_xcodebuild write_version case "$platform" in all) build_ios_release build_macos_release ;; ios) build_ios_release ;; macos) build_macos_release ;; *) echo "unknown Apple release platform: $platform" >&2 exit 64 ;; esac find "$out_root" -type f -print | sort