#!/bin/bash set -e # Ensure we're using self-distribution configuration echo "๐Ÿ”„ Switching to self-distribution configuration..." ./switch_to self # Load environment variables from .env file if [ -f .env ]; then set -a source .env set +a fi # Function to ask for version bump confirmation ask_version_bump() { echo "" echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" echo " Gaze Version Bump & Build Tool " echo "โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•" echo "" # Check if we're in a git repository if [ ! -d .git ] && [ ! -f .git ]; then echo "Error: Not in a git repository" exit 1 fi # Check for uncommitted changes if ! git diff-index --quiet HEAD --; then echo "Warning: You have uncommitted changes" git status --short echo "" read -p "Continue anyway? (y/n) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then echo "Aborted" exit 1 fi echo "" fi # Get current version from git tag or fallback to project file CURRENT_VERSION=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//') if [ -z "$CURRENT_VERSION" ]; then echo "Warning: No existing git tags found" echo "Attempting to read version from Xcode project..." PROJECT_FILE="Gaze.xcodeproj/project.pbxproj" CURRENT_VERSION=$(grep -A 1 "MARKETING_VERSION" "$PROJECT_FILE" | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1) if [ -z "$CURRENT_VERSION" ]; then echo "Error: Could not extract version from git tags or project file" exit 1 fi echo "Using version from project file as fallback" fi echo "Current version: v${CURRENT_VERSION}" IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT_VERSION" MAJOR=$(echo "$MAJOR" | sed 's/[^0-9].*//') MINOR=$(echo "$MINOR" | sed 's/[^0-9].*//') PATCH=$(echo "$PATCH" | sed 's/[^0-9].*//') echo "" echo "Select version bump type:" echo " 1) Major (breaking changes) ${MAJOR}.${MINOR}.${PATCH} โ†’ $((MAJOR+1)).0.0" echo " 2) Minor (new features) ${MAJOR}.${MINOR}.${PATCH} โ†’ ${MAJOR}.$((MINOR+1)).0" echo " 3) Patch (bug fixes) ${MAJOR}.${MINOR}.${PATCH} โ†’ ${MAJOR}.${MINOR}.$((PATCH+1))" echo " 4) Custom version" echo " 5) Keep current version" echo "" read -p "Enter choice (1-5): " -n 1 -r CHOICE echo "" echo "" case $CHOICE in 1) NEW_MAJOR=$((MAJOR+1)) NEW_MINOR=0 NEW_PATCH=0 BUMP_TYPE="major" ;; 2) NEW_MAJOR=$MAJOR NEW_MINOR=$((MINOR+1)) NEW_PATCH=0 BUMP_TYPE="minor" ;; 3) NEW_MAJOR=$MAJOR NEW_MINOR=$MINOR NEW_PATCH=$((PATCH+1)) BUMP_TYPE="patch" ;; 4) read -p "Enter custom version (e.g., 1.0.0-beta): " CUSTOM_VERSION NEW_VERSION="$CUSTOM_VERSION" BUMP_TYPE="custom" ;; 5) echo "Keeping current version v${CURRENT_VERSION}" NEW_VERSION="$CURRENT_VERSION" BUMP_TYPE="keep" ;; *) echo "Invalid choice" exit 1 ;; esac # Construct new version (unless custom) if [ "$BUMP_TYPE" != "custom" ] && [ "$BUMP_TYPE" != "keep" ]; then NEW_VERSION="${NEW_MAJOR}.${NEW_MINOR}.${NEW_PATCH}" fi echo "New version: v${NEW_VERSION}" # Get current build number for display DISPLAY_CURRENT_BUILD=$(grep -A 1 "CURRENT_PROJECT_VERSION" Gaze.xcodeproj/project.pbxproj | grep -o '[0-9]\+' | head -1) if [ -z "$DISPLAY_CURRENT_BUILD" ]; then DISPLAY_CURRENT_BUILD=0 fi DISPLAY_NEW_BUILD=$((DISPLAY_CURRENT_BUILD + 1)) # Ask for confirmation to proceed with version bumping echo "" if [ "$BUMP_TYPE" = "keep" ]; then echo "This will:" echo " 1. Keep MARKETING_VERSION = ${NEW_VERSION}" echo " 2. Update project.pbxproj โ†’ CURRENT_PROJECT_VERSION = ${DISPLAY_NEW_BUILD} (currently ${DISPLAY_CURRENT_BUILD})" echo " 3. Create git commit (no new tag)" else echo "This will:" echo " 1. Update project.pbxproj โ†’ MARKETING_VERSION = ${NEW_VERSION}" echo " 2. Update project.pbxproj โ†’ CURRENT_PROJECT_VERSION = ${DISPLAY_NEW_BUILD} (currently ${DISPLAY_CURRENT_BUILD})" echo " 3. Create git tag v${NEW_VERSION}" fi echo "" read -p "Proceed with version bump? (y/n) " -n 1 -r echo "" if [[ ! $REPLY =~ ^[Yy]$ ]]; then echo "Aborted" exit 0 fi # Update project files echo "[1/3] Updating project version..." # Update MARKETING_VERSION (unless keeping) if [ "$BUMP_TYPE" != "keep" ]; then sed -i.bak "s/MARKETING_VERSION = [0-9.]*;/MARKETING_VERSION = ${NEW_VERSION};/" Gaze.xcodeproj/project.pbxproj rm -f Gaze.xcodeproj/project.pbxproj.bak fi # Update CURRENT_PROJECT_VERSION (build number) - increment by 1 CURRENT_BUILD=$(grep -A 1 "CURRENT_PROJECT_VERSION" Gaze.xcodeproj/project.pbxproj | grep -o '[0-9]\+' | head -1) if [ -z "$CURRENT_BUILD" ]; then CURRENT_BUILD=0 fi BUILD_NUMBER=$((CURRENT_BUILD + 1)) sed -i.bak "s/CURRENT_PROJECT_VERSION = [0-9]*;/CURRENT_PROJECT_VERSION = ${BUILD_NUMBER};/" Gaze.xcodeproj/project.pbxproj rm -f Gaze.xcodeproj/project.pbxproj.bak echo "โœ“ Project version updated" # Stage changes and commit echo "[2/3] Committing changes..." git add Gaze.xcodeproj/project.pbxproj if [ "$BUMP_TYPE" = "keep" ]; then git commit -m "Build bump to v${NEW_VERSION} (${BUILD_NUMBER})" else git commit -m "Version bump to v${NEW_VERSION}" fi # Create tag (skip if keeping version) if [ "$BUMP_TYPE" != "keep" ]; then echo "[3/3] Creating tag..." git tag -a "v${NEW_VERSION}" -m "Release version ${NEW_VERSION}" echo "โœ“ Version bumped to v${NEW_VERSION}" else echo "[3/3] Skipping tag creation (keeping version)" echo "โœ“ Build number bumped to ${BUILD_NUMBER}" fi } # Ask whether to bump version before continuing read -p "Do you want to bump the version before building? (y/n) " -n 1 -r echo "" if [[ $REPLY =~ ^[Yy]$ ]]; then ask_version_bump fi # Validate required code signing and notarization credentials echo "๐Ÿ” Validating credentials..." MISSING_CREDS=() if [ -z "$DEVELOPER_ID_APPLICATION" ]; then MISSING_CREDS+=("DEVELOPER_ID_APPLICATION") fi if [ -z "$NOTARY_KEYCHAIN_PROFILE" ]; then MISSING_CREDS+=("NOTARY_KEYCHAIN_PROFILE") fi if [ -z "$APPLE_TEAM_ID" ]; then MISSING_CREDS+=("APPLE_TEAM_ID") fi if [ ${#MISSING_CREDS[@]} -gt 0 ]; then echo "โŒ ERROR: Missing required credentials in .env file:" for cred in "${MISSING_CREDS[@]}"; do echo " - $cred" done echo "" echo "Required environment variables:" echo " DEVELOPER_ID_APPLICATION='Developer ID Application: Your Name (TEAM_ID)'" echo " APPLE_TEAM_ID='XXXXXXXXXX'" echo " NOTARY_KEYCHAIN_PROFILE='notary-profile'" echo "" echo "Setup instructions:" echo " 1. Find your Developer ID certificate:" echo " security find-identity -v -p codesigning" echo "" echo " 2. Store notarization credentials in keychain (one-time setup):" echo " xcrun notarytool store-credentials \"notary-profile\" \\" echo " --apple-id \"your@email.com\" \\" echo " --team-id \"TEAM_ID\"" echo "" echo " You'll be prompted for an app-specific password." echo " Generate one at: https://appleid.apple.com/account/manage" echo "" echo " 3. Set NOTARY_KEYCHAIN_PROFILE='notary-profile' in .env" exit 1 fi # Verify keychain profile exists echo "๐Ÿ” Verifying keychain profile..." if ! xcrun notarytool history --keychain-profile "$NOTARY_KEYCHAIN_PROFILE" &>/dev/null; then echo "โŒ ERROR: Keychain profile '$NOTARY_KEYCHAIN_PROFILE' not found or invalid" echo "" echo "Create the profile with:" echo " xcrun notarytool store-credentials \"$NOTARY_KEYCHAIN_PROFILE\" \\" echo " --apple-id \"your@email.com\" \\" echo " --team-id \"$APPLE_TEAM_ID\"" exit 1 fi echo "โœ… All credentials validated" # Extract version from Xcode project (Release configuration) PROJECT_FILE="Gaze.xcodeproj/project.pbxproj" VERSION=$(grep -A 1 "MARKETING_VERSION" "$PROJECT_FILE" | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1) BUILD_NUMBER=$(grep -A 1 "CURRENT_PROJECT_VERSION" "$PROJECT_FILE" | grep -o '[0-9]\+' | head -1) # Fallback to manual values if extraction fails if [ -z "$VERSION" ]; then echo "โš ๏ธ Could not extract MARKETING_VERSION from project, using fallback" VERSION="0.2.0" fi if [ -z "$BUILD_NUMBER" ]; then echo "โš ๏ธ Could not extract CURRENT_PROJECT_VERSION from project, using fallback" BUILD_NUMBER="1" fi echo "๐Ÿ“ฆ Building Gaze v${VERSION} (build ${BUILD_NUMBER}) for distribution" RELEASES_DIR="./releases" ARCHIVE_PATH="./build/Gaze.xcarchive" EXPORT_PATH="./build/export" APPCAST_OUTPUT="${RELEASES_DIR}/appcast.xml" FEED_URL="https://freno.me/api/Gaze/appcast.xml" DOWNLOAD_URL_PREFIX="https://freno.me/api/downloads/" DMG_NAME="Gaze-${VERSION}.dmg" # Validate required code signing and notarization credentials echo "๐Ÿ” Validating credentials..." MISSING_CREDS=() if [ -z "$DEVELOPER_ID_APPLICATION" ]; then MISSING_CREDS+=("DEVELOPER_ID_APPLICATION") fi if [ -z "$NOTARY_KEYCHAIN_PROFILE" ]; then MISSING_CREDS+=("NOTARY_KEYCHAIN_PROFILE") fi if [ -z "$APPLE_TEAM_ID" ]; then MISSING_CREDS+=("APPLE_TEAM_ID") fi if [ ${#MISSING_CREDS[@]} -gt 0 ]; then echo "โŒ ERROR: Missing required credentials in .env file:" for cred in "${MISSING_CREDS[@]}"; do echo " - $cred" done echo "" echo "Required environment variables:" echo " DEVELOPER_ID_APPLICATION='Developer ID Application: Your Name (TEAM_ID)'" echo " APPLE_TEAM_ID='XXXXXXXXXX'" echo " NOTARY_KEYCHAIN_PROFILE='notary-profile'" echo "" echo "Setup instructions:" echo " 1. Find your Developer ID certificate:" echo " security find-identity -v -p codesigning" echo "" echo " 2. Store notarization credentials in keychain (one-time setup):" echo " xcrun notarytool store-credentials \"notary-profile\" \\" echo " --apple-id \"your@email.com\" \\" echo " --team-id \"TEAM_ID\"" echo "" echo " You'll be prompted for an app-specific password." echo " Generate one at: https://appleid.apple.com/account/manage" echo "" echo " 3. Set NOTARY_KEYCHAIN_PROFILE='notary-profile' in .env" exit 1 fi # Verify keychain profile exists echo "๐Ÿ” Verifying keychain profile..." if ! xcrun notarytool history --keychain-profile "$NOTARY_KEYCHAIN_PROFILE" &>/dev/null; then echo "โŒ ERROR: Keychain profile '$NOTARY_KEYCHAIN_PROFILE' not found or invalid" echo "" echo "Create the profile with:" echo " xcrun notarytool store-credentials \"$NOTARY_KEYCHAIN_PROFILE\" \\" echo " --apple-id \"your@email.com\" \\" echo " --team-id \"$APPLE_TEAM_ID\"" exit 1 fi echo "โœ… All credentials validated" # Extract version from Xcode project (Release configuration) PROJECT_FILE="Gaze.xcodeproj/project.pbxproj" VERSION=$(grep -A 1 "MARKETING_VERSION" "$PROJECT_FILE" | grep -o '[0-9]\+\.[0-9]\+\.[0-9]\+' | head -1) BUILD_NUMBER=$(grep -A 1 "CURRENT_PROJECT_VERSION" "$PROJECT_FILE" | grep -o '[0-9]\+' | head -1) # Fallback to manual values if extraction fails if [ -z "$VERSION" ]; then echo "โš ๏ธ Could not extract MARKETING_VERSION from project, using fallback" VERSION="0.2.0" fi if [ -z "$BUILD_NUMBER" ]; then echo "โš ๏ธ Could not extract CURRENT_PROJECT_VERSION from project, using fallback" BUILD_NUMBER="1" fi echo "๐Ÿ“ฆ Building Gaze v${VERSION} (build ${BUILD_NUMBER}) for distribution" RELEASES_DIR="./releases" ARCHIVE_PATH="./build/Gaze.xcarchive" EXPORT_PATH="./build/export" APPCAST_OUTPUT="${RELEASES_DIR}/appcast.xml" FEED_URL="https://freno.me/api/Gaze/appcast.xml" DOWNLOAD_URL_PREFIX="https://freno.me/api/downloads/" DMG_NAME="Gaze-${VERSION}.dmg" # Find Sparkle generate_appcast tool SPARKLE_BIN=$(find ~/Library/Developer/Xcode/DerivedData/Gaze-* -path "*/artifacts/sparkle/Sparkle/bin" -type d 2>/dev/null | head -1) if [ -z "$SPARKLE_BIN" ]; then echo "โš ๏ธ Warning: Sparkle bin directory not found" echo "Appcast generation will be skipped" SPARKLE_BIN="" fi # Create build and releases directories mkdir -p "$RELEASES_DIR" mkdir -p "$(dirname "$ARCHIVE_PATH")" # Clean previous builds echo "" echo "๐Ÿงน Cleaning previous builds..." rm -rf "$ARCHIVE_PATH" rm -rf "$EXPORT_PATH" rm -f "$DMG_NAME" # Clean Xcode build cache to ensure version changes are picked up echo "๐Ÿงน Cleaning Xcode build cache..." xcodebuild clean -project Gaze.xcodeproj -scheme Gaze -configuration Release # Step 1: Archive the application echo "" echo "๐Ÿ“ฆ Creating archive..." xcodebuild archive \ -project Gaze.xcodeproj \ -scheme Gaze \ -configuration Release \ -archivePath "$ARCHIVE_PATH" \ CODE_SIGN_IDENTITY="$DEVELOPER_ID_APPLICATION" \ CODE_SIGN_STYLE=Manual \ DEVELOPMENT_TEAM="$APPLE_TEAM_ID" if [ ! -d "$ARCHIVE_PATH" ]; then echo "โŒ ERROR: Archive creation failed" exit 1 fi echo "โœ… Archive created successfully" # Step 2: Create exportOptions.plist echo "" echo "๐Ÿ“ Creating export options..." cat > /tmp/exportOptions.plist < method developer-id signingStyle manual teamID $APPLE_TEAM_ID signingCertificate Developer ID Application stripSwiftSymbols uploadSymbols EOF # Step 3: Export the archive echo "" echo "๐Ÿ“ค Exporting signed application..." xcodebuild -exportArchive \ -archivePath "$ARCHIVE_PATH" \ -exportPath "$EXPORT_PATH" \ -exportOptionsPlist /tmp/exportOptions.plist if [ ! -d "$EXPORT_PATH/Gaze.app" ]; then echo "โŒ ERROR: Export failed - Gaze.app not found" exit 1 fi echo "โœ… Application exported and signed" # Step 4: Verify code signature echo "" echo "๐Ÿ” Verifying code signature..." codesign --verify --deep --strict --verbose=2 "$EXPORT_PATH/Gaze.app" if [ $? -eq 0 ]; then echo "โœ… Code signature valid" else echo "โŒ ERROR: Code signature verification failed" exit 1 fi # Show signature details echo "" echo "๐Ÿ“‹ Signature details:" codesign -dv --verbose=4 "$EXPORT_PATH/Gaze.app" 2>&1 | grep -E "Authority|TeamIdentifier|Identifier" # Step 5: Create ZIP archive for notarization echo "" echo "๐Ÿ“ฆ Creating ZIP archive for notarization..." APP_ZIP="/tmp/Gaze-notarize.zip" rm -f "$APP_ZIP" ditto -c -k --keepParent "$EXPORT_PATH/Gaze.app" "$APP_ZIP" if [ ! -f "$APP_ZIP" ]; then echo "โŒ ERROR: Failed to create ZIP archive" exit 1 fi echo "โœ… ZIP archive created" # Step 6: Notarize the application echo "" echo "๐Ÿ” Submitting application for notarization..." NOTARIZE_OUTPUT=$(xcrun notarytool submit "$APP_ZIP" \ --keychain-profile "$NOTARY_KEYCHAIN_PROFILE" \ --wait \ --timeout 30m 2>&1) echo "$NOTARIZE_OUTPUT" if echo "$NOTARIZE_OUTPUT" | grep -q "status: Accepted"; then echo "โœ… Application notarization accepted" # Extract submission ID for logging SUBMISSION_ID=$(echo "$NOTARIZE_OUTPUT" | grep "id:" | head -1 | awk '{print $2}') echo " Submission ID: $SUBMISSION_ID" else echo "โŒ ERROR: Application notarization failed" echo "" echo "Notarization output:" echo "$NOTARIZE_OUTPUT" exit 1 fi # Clean up temporary ZIP rm -f "$APP_ZIP" # Step 7: Staple notarization ticket to app echo "" echo "๐Ÿ“Ž Stapling notarization ticket to application..." xcrun stapler staple "$EXPORT_PATH/Gaze.app" if [ $? -eq 0 ]; then echo "โœ… Notarization ticket stapled to application" else echo "โŒ ERROR: Failed to staple notarization ticket" exit 1 fi # Step 8: Verify with Gatekeeper echo "" echo "๐Ÿ” Verifying Gatekeeper acceptance..." set +e # Temporarily disable exit on error spctl --assess --type execute --verbose=4 "$EXPORT_PATH/Gaze.app" 2>&1 SPCTL_RESULT=$? set -e # Re-enable exit on error if [ $SPCTL_RESULT -eq 0 ]; then echo "โœ… Application passes Gatekeeper verification" else echo "โš ๏ธ Warning: Gatekeeper assessment returned non-zero status" echo " This may be expected for some configurations" fi # Step 9: Create DMG from notarized app echo "" echo "๐Ÿ’ฟ Creating DMG..." create-dmg \ --volname "Gaze Installer" \ --eula "./LICENSE" \ --window-pos 200 120 \ --window-size 600 400 \ --icon-size 100 \ --background "./dmg_background.png" \ --icon "Gaze.app" 160 200 \ --app-drop-link 440 200 \ "$DMG_NAME" \ "$EXPORT_PATH/Gaze.app" if [ ! -f "$DMG_NAME" ]; then echo "โŒ ERROR: DMG creation failed" exit 1 fi echo "โœ… DMG created successfully" # Step 10: Sign the DMG echo "" echo "๐Ÿ” Signing DMG..." codesign --sign "$DEVELOPER_ID_APPLICATION" \ --timestamp \ --options runtime \ --force \ "$DMG_NAME" if [ $? -eq 0 ]; then echo "โœ… DMG signed successfully" else echo "โŒ ERROR: DMG signing failed" exit 1 fi # Verify DMG signature codesign --verify --deep --strict --verbose=2 "$DMG_NAME" if [ $? -eq 0 ]; then echo "โœ… DMG signature valid" else echo "โŒ ERROR: DMG signature verification failed" exit 1 fi # Step 11: Notarize the DMG echo "" echo "๐Ÿ” Submitting DMG for notarization..." DMG_NOTARIZE_OUTPUT=$(xcrun notarytool submit "$DMG_NAME" \ --keychain-profile "$NOTARY_KEYCHAIN_PROFILE" \ --wait \ --timeout 30m 2>&1) echo "$DMG_NOTARIZE_OUTPUT" if echo "$DMG_NOTARIZE_OUTPUT" | grep -q "status: Accepted"; then echo "โœ… DMG notarization accepted" # Extract submission ID for logging DMG_SUBMISSION_ID=$(echo "$DMG_NOTARIZE_OUTPUT" | grep "id:" | head -1 | awk '{print $2}') echo " Submission ID: $DMG_SUBMISSION_ID" else echo "โŒ ERROR: DMG notarization failed" echo "" echo "Notarization output:" echo "$DMG_NOTARIZE_OUTPUT" exit 1 fi # Step 12: Staple notarization ticket to DMG echo "" echo "๐Ÿ“Ž Stapling notarization ticket to DMG..." xcrun stapler staple "$DMG_NAME" if [ $? -eq 0 ]; then echo "โœ… Notarization ticket stapled to DMG" else echo "โŒ ERROR: Failed to staple notarization ticket to DMG" exit 1 fi # Step 13: Final verification echo "" echo "๐Ÿ” Final DMG verification..." set +e # Temporarily disable exit on error spctl --assess --type open --context context:primary-signature --verbose=4 "$DMG_NAME" 2>&1 DMG_SPCTL_RESULT=$? set -e # Re-enable exit on error if [ $DMG_SPCTL_RESULT -eq 0 ]; then echo "โœ… DMG passes all Gatekeeper checks" else echo "โš ๏ธ Warning: Gatekeeper assessment returned non-zero status" echo " This may be expected for disk images" fi # Copy DMG to releases directory echo "Moving DMG to releases directory..." mv "$DMG_NAME" "$RELEASES_DIR/" # Generate appcast if Sparkle tools are available if [ -n "$SPARKLE_BIN" ] && [ -d "$SPARKLE_BIN" ]; then echo "" echo "Generating appcast..." # Check for private key (Keychain or file) PRIVATE_KEY_FILE="$HOME/sparkle_private_key_backup.pem" KEY_OPTION="" if [ -f "$PRIVATE_KEY_FILE" ]; then echo "Using private key from: $PRIVATE_KEY_FILE" KEY_OPTION="--ed-key-file $PRIVATE_KEY_FILE" else echo "Using private key from Keychain (account: ed25519)" KEY_OPTION="--account ed25519" fi # Generate appcast with download URL prefix and key "$SPARKLE_BIN/generate_appcast" \ --download-url-prefix "$DOWNLOAD_URL_PREFIX" \ $KEY_OPTION \ "$RELEASES_DIR" # Verify appcast was generated if [ -f "$APPCAST_OUTPUT" ]; then echo "โœ… Appcast generated successfully" echo "๐Ÿ“‹ Appcast location: $APPCAST_OUTPUT" # Check for signature - if missing, add it manually if grep -q "edSignature" "$APPCAST_OUTPUT"; then echo "โœ… EdDSA signature verified in appcast" else echo "โš ๏ธ No signature found, generating manually with sign_update..." # Get signature for the DMG SIGNATURE_OUTPUT=$("$SPARKLE_BIN/sign_update" "$RELEASES_DIR/$DMG_NAME" 2>&1) if echo "$SIGNATURE_OUTPUT" | grep -q "edSignature"; then # Extract the signature ED_SIGNATURE=$(echo "$SIGNATURE_OUTPUT" | grep -o 'sparkle:edSignature="[^"]*"' | sed 's/sparkle:edSignature="\([^"]*\)"/\1/') FILE_LENGTH=$(echo "$SIGNATURE_OUTPUT" | grep -o 'length="[^"]*"' | sed 's/length="\([^"]*\)"/\1/') echo "โœ… Generated signature: ${ED_SIGNATURE:0:20}..." # Add signature to appcast XML # Find the enclosure line and add sparkle:edSignature attribute sed -i '' "s|