#!/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" bundle_id="${BURROW_APPLE_BUNDLE_ID:-net.burrow.app}" network_extension_bundle_id="${BURROW_APPLE_NETWORK_EXTENSION_BUNDLE_ID:-${bundle_id}.network}" notarize_macos="${BURROW_NOTARIZE_MACOS:-false}" sparkle_sign_with_kms="${BURROW_SPARKLE_SIGN_WITH_KMS:-true}" 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 } require_tool() { if ! command -v "$1" >/dev/null 2>&1; then echo "error: missing required tool: $1" >&2 exit 1 fi } extract_profile_entitlements() { 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[@]}" } 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[@]}" } write_notary_api_key_json() { local out="$1" 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 rcodesign encode-app-store-connect-api-key \ --output-path "$out" \ "$ASC_API_ISSUER_ID" \ "$ASC_API_KEY_ID" \ "$ASC_API_KEY_PATH" } sign_ios() { local unsigned_ipa app_profile ext_profile work_dir app ext app_entitlements ext_entitlements output_ipa unsigned_ipa="${apple_root}/Burrow-iOS-${build_number}.unsigned.ipa" if [[ ! -s "$unsigned_ipa" ]]; then echo "error: missing staged unsigned iOS IPA: $unsigned_ipa" >&2 exit 1 fi app_profile="$(profile_for "$bundle_id" "appstore")" || { echo "error: missing iOS App Store provisioning profile for ${bundle_id}" >&2 exit 1 } ext_profile="$(profile_for "$network_extension_bundle_id" "appstore")" || { echo "error: missing iOS App Store provisioning profile for ${network_extension_bundle_id}" >&2 exit 1 } work_dir="$(mktemp -d "${TMPDIR:-/tmp}/burrow-ios-kms-sign.XXXXXX")" trap 'rm -rf "${work_dir:-}"' RETURN unzip -q "$unsigned_ipa" -d "$work_dir" app="${work_dir}/Payload/Burrow.app" ext="${app}/PlugIns/BurrowNetworkExtension.appex" if [[ ! -d "$app" || ! -d "$ext" ]]; then echo "error: unsigned IPA is missing Burrow.app or BurrowNetworkExtension.appex" >&2 exit 1 fi cp "$app_profile" "${app}/embedded.mobileprovision" cp "$ext_profile" "${ext}/embedded.mobileprovision" app_entitlements="${work_dir}/app-entitlements.plist" ext_entitlements="${work_dir}/extension-entitlements.plist" extract_profile_entitlements "$app_profile" "$app_entitlements" Apple/App/App-iOS.entitlements extract_profile_entitlements "$ext_profile" "$ext_entitlements" Apple/NetworkExtension/NetworkExtension-iOS.entitlements rm -rf "${app}/_CodeSignature" "${ext}/_CodeSignature" sign_bundle ios "$ext" "$ext_entitlements" sign_bundle ios "$app" "$app_entitlements" output_ipa="${apple_root}/Burrow-iOS-${build_number}.ipa" rm -f "$output_ipa" (cd "$work_dir" && zip -qry "$output_ipa" Payload) sha256_file "$output_ipa" } write_sparkle_appcast() { local artifact="$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 "$artifact" "$sparkle_dir/" length="$(wc -c < "$artifact" | 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 "$artifact")" 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 } sign_macos() { local unsigned_zip app_profile ext_profile work_dir app ext app_entitlements ext_entitlements output_zip notary_zip key_json unsigned_zip="${apple_root}/Burrow-macOS-${build_number}.unsigned.zip" if [[ ! -s "$unsigned_zip" ]]; then echo "error: missing staged unsigned macOS ZIP: $unsigned_zip" >&2 exit 1 fi app_profile="$(profile_for "$bundle_id" "developerid")" || { echo "error: missing macOS Developer ID provisioning profile for ${bundle_id}" >&2 exit 1 } ext_profile="$(profile_for "$network_extension_bundle_id" "developerid")" || { echo "error: missing macOS Developer ID provisioning profile for ${network_extension_bundle_id}" >&2 exit 1 } work_dir="$(mktemp -d "${TMPDIR:-/tmp}/burrow-macos-kms-sign.XXXXXX")" trap 'rm -rf "${work_dir:-}"' RETURN unzip -q "$unsigned_zip" -d "$work_dir" app="$(find "$work_dir" -maxdepth 2 -type d -name 'Burrow.app' -print | head -n 1)" if [[ -z "$app" || ! -d "$app" ]]; then echo "error: unsigned macOS ZIP does not contain Burrow.app" >&2 exit 1 fi ext="${app}/Contents/PlugIns/BurrowNetworkExtension.appex" cp "$app_profile" "${app}/Contents/embedded.provisionprofile" app_entitlements="${work_dir}/app-entitlements.plist" extract_profile_entitlements "$app_profile" "$app_entitlements" if [[ -d "$ext" ]]; then cp "$ext_profile" "${ext}/Contents/embedded.provisionprofile" ext_entitlements="${work_dir}/extension-entitlements.plist" extract_profile_entitlements "$ext_profile" "$ext_entitlements" rm -rf "${ext}/Contents/_CodeSignature" sign_bundle developer-id "$ext" "$ext_entitlements" fi rm -rf "${app}/Contents/_CodeSignature" sign_bundle developer-id "$app" "$app_entitlements" if truthy "$notarize_macos"; then key_json="${work_dir}/notary-api-key.json" notary_zip="${work_dir}/Burrow.notary.zip" write_notary_api_key_json "$key_json" (cd "$(dirname "$app")" && zip -qry "$notary_zip" "$(basename "$app")") rcodesign notary-submit --api-key-file "$key_json" --wait "$notary_zip" rcodesign staple "$app" fi output_zip="${apple_root}/Burrow-macOS-${build_number}.zip" rm -f "$output_zip" (cd "$(dirname "$app")" && zip -qry "$output_zip" "$(basename "$app")") sha256_file "$output_zip" write_sparkle_appcast "$output_zip" } require_tool python3 require_tool unzip require_tool zip require_tool rcodesign require_tool gcloud mkdir -p "$apple_root" case "$platform" in all) sign_ios sign_macos ;; ios) sign_ios ;; macos) sign_macos ;; *) echo "unknown Apple KMS signing platform: $platform" >&2 exit 64 ;; esac find "$out_root" -type f -print | sort