GitHub Actions για .NET MAUI: Πλήρης Οδηγός CI/CD Pipeline με Signing και Store Deployment (2026)

Πλήρες CI/CD pipeline για .NET MAUI στο GitHub Actions: build, code signing, fastlane deploys σε Google Play και App Store, με έτοιμα YAML workflows για Android και iOS.

GitHub Actions .NET MAUI CI/CD Οδηγός 2026
On this page
  1. Γιατί GitHub Actions για .NET MAUI CI/CD
  2. Αρχιτεκτονική Pipeline: Runners, Jobs και Matrix
  3. Προετοιμασία repository: Secrets και Environments
  4. Android build και signing στο GitHub Actions
  5. iOS build και signing σε macOS runners
  6. Δημοσίευση σε Google Play και App Store με fastlane
  7. Αυτόματο versioning και build caching
  8. Best practices και συχνά λάθη
  9. Συχνές ερωτήσεις

Ενημερώθηκε: 16 Σεπτεμβρίου 2026

Το GitHub Actions για .NET MAUI CI/CD σου επιτρέπει να κάνεις build, sign και deploy την εφαρμογή σου σε Android και iOS από ένα ενιαίο workflow, χωρίς Azure DevOps ή Jenkins. Χρειάζεσαι έναν windows-latest ή ubuntu-latest runner για το Android APK/AAB, έναν macos-14 runner για το iOS .ipa, τα κατάλληλα secrets (keystore, provisioning profile, App Store Connect API key) και ένα YAML workflow που ενσωματώνει το dotnet publish με fastlane για upload στα stores.

Γιατί GitHub Actions για .NET MAUI CI/CD

Έχω στήσει pipelines για MAUI (και Xamarin.Forms πριν από αυτό) σε Azure DevOps, App Center, Bitrise και GitHub Actions. Οι περισσότερες ομάδες παραλείπουν αυτό το βήμα και μετά κολλάνε στο "στο μηχάνημά μου δουλεύει" την Παρασκευή στις 6μμ πριν από ένα release. Ήταν εκεί, το έζησα, δεν το προτείνω σε κανέναν.

Το GitHub Actions κερδίζει σήμερα για τρεις πρακτικούς λόγους. Πρώτον, ο κώδικας και το CI ζουν στο ίδιο repository. Δεύτερον, τα macOS 14 (Apple Silicon) runners υποστηρίζουν το dotnet publish -f net9.0-ios out-of-the-box. Και τρίτον, το ecosystem των actions (setup-dotnet, upload-artifact, fastlane) καλύπτει το 90% όσων χρειάζεσαι.

Το App Center έχει ήδη ανακοινώσει sunset για τον Μάρτιο του 2025, οπότε αν είσαι ακόμα εκεί, η μετάβαση είναι υποχρεωτική. Το Azure DevOps παραμένει καλή επιλογή για enterprise setups με πολλαπλά repos, αλλά για single-repo MAUI apps το GitHub Actions προσφέρει καλύτερη ενσωμάτωση με pull requests, environment protection rules και OIDC για cloud deployments. Επιπλέον, τα free minutes για public repos είναι απεριόριστα και για private repos έχεις 2.000 λεπτά/μήνα στο δωρεάν πλάνο, δηλαδή περίπου 40 πλήρη builds ενός μεσαίου MAUI project.

Για την πλήρη ροή release μετά το CI, δες και τη δημοσίευση .NET MAUI σε App Store και Google Play που καλύπτει τα manual review steps.

Αρχιτεκτονική Pipeline: Runners, Jobs και Matrix

Ο βασικός κανόνας είναι ένας: το iOS build απαιτεί υποχρεωτικά macOS runner, επειδή το Xcode toolchain (xcodebuild, codesign, security) υπάρχει μόνο εκεί. Το Android build μπορεί να τρέξει σε Ubuntu ή Windows runner. Τα Ubuntu runners είναι δύο φορές φτηνότερα από τα Windows και τέσσερις φορές φτηνότερα από τα macOS, οπότε η επιλογή είναι εύκολη.

Η αρχιτεκτονική βγαίνει ως εξής:

  • Job "build-android" σε ubuntu-latest
  • Job "build-ios" σε macos-14 (Apple Silicon, Xcode 16)
  • Job "deploy" που εξαρτάται (needs) και από τα δύο, για upload σε stores

Και τα δύο build jobs μπορούν να τρέξουν παράλληλα, μειώνοντας το wall-clock time. Δες τη βασική δομή του .github/workflows/release.yml:

name: MAUI Release Pipeline

on:
  push:
    tags: ['v*.*.*']
  workflow_dispatch:
    inputs:
      channel:
        description: 'Release channel'
        required: true
        default: 'internal'
        type: choice
        options: [internal, beta, production]

env:
  DOTNET_VERSION: '9.0.x'
  MAUI_VERSION: '9.0.0'

jobs:
  build-android:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}
      - name: Install MAUI workload
        run: dotnet workload install maui-android
      - name: Build & sign Android
        run: |
          dotnet publish src/MyApp/MyApp.csproj \
            -f net9.0-android \
            -c Release \
            -p:AndroidPackageFormat=aab \
            -p:AndroidKeyStore=true \
            -p:AndroidSigningKeyStore=$RUNNER_TEMP/upload.keystore \
            -p:AndroidSigningStorePass=${{ secrets.ANDROID_KEYSTORE_PASSWORD }} \
            -p:AndroidSigningKeyAlias=${{ secrets.ANDROID_KEY_ALIAS }} \
            -p:AndroidSigningKeyPass=${{ secrets.ANDROID_KEY_PASSWORD }}
      - uses: actions/upload-artifact@v4
        with:
          name: android-aab
          path: '**/*-Signed.aab'

  build-ios:
    runs-on: macos-14
    timeout-minutes: 45
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}
      - name: Install MAUI workload
        run: dotnet workload install maui-ios
      - name: Select Xcode 16.1
        run: sudo xcode-select -s /Applications/Xcode_16.1.app

  deploy:
    needs: [build-android, build-ios]
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploy step goes here"

Προετοιμασία repository: Secrets και Environments

Πριν γράψεις οποιοδήποτε workflow, χρειάζεσαι τα ακόλουθα ως GitHub Secrets (Settings → Secrets and variables → Actions). Δεν κάνεις commit ΤΙΠΟΤΑ από αυτά. Ούτε σαν "temp file", ούτε σαν "θα το βγάλω μετά". Ποτέ.

Android secrets

  • ANDROID_KEYSTORE_BASE64, δηλαδή το .keystore file κωδικοποιημένο σε base64 (base64 -i upload.keystore | pbcopy)
  • ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD
  • GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: service account JSON με πρόσβαση στο Google Play Developer API

iOS secrets

  • IOS_P12_CERTIFICATE_BASE64: το .p12 distribution certificate σε base64
  • IOS_P12_PASSWORD: το password του .p12
  • IOS_PROVISIONING_PROFILE_BASE64: το .mobileprovision σε base64
  • APP_STORE_CONNECT_API_KEY_ID, APP_STORE_CONNECT_ISSUER_ID, APP_STORE_CONNECT_API_KEY_BASE64: το p8 API key σε base64
  • KEYCHAIN_PASSWORD: random password για το προσωρινό keychain του runner

Στη συνέχεια δημιούργησε Environments (Settings → Environments): internal, beta, production. Στο production environment ενεργοποίησε "Required reviewers" ώστε να χρειάζεται manual approval πριν το upload στο App Store. Αυτή η επιπλέον πόρτα έχει σώσει release πολλές φορές στην καριέρα μου. Ένας συνάδελφος τρέχει το workflow, ο άλλος approves αφού δει το build number και τα release notes. Απλό, αλλά έχει γλυτώσει και εμένα και άλλους από πραγματικά ενοχλητικά rollbacks.

Android build και signing στο GitHub Actions

Για το Android, το .NET MAUI υποστηρίζει native signing μέσα από τα MSBuild properties. Δεν χρειάζεσαι fastlane για signing. Το dotnet publish κάνει τη δουλειά μόνο του. Το βασικό βήμα είναι να αποκωδικοποιήσεις το keystore από το secret και να το βάλεις σε ένα temporary path:

- name: Decode Android keystore
  env:
    KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
  run: |
    echo "$KEYSTORE_BASE64" | base64 --decode > $RUNNER_TEMP/upload.keystore
    ls -la $RUNNER_TEMP/upload.keystore

- name: Restore workloads
  run: dotnet workload restore src/MyApp/MyApp.csproj

- name: Publish Android AAB
  run: |
    dotnet publish src/MyApp/MyApp.csproj \
      -f net9.0-android \
      -c Release \
      -p:ApplicationDisplayVersion=1.4.0 \
      -p:ApplicationVersion=${{ github.run_number }} \
      -p:AndroidPackageFormat=aab \
      -p:AndroidKeyStore=true \
      -p:AndroidSigningKeyStore=$RUNNER_TEMP/upload.keystore \
      -p:AndroidSigningStorePass=${{ secrets.ANDROID_KEYSTORE_PASSWORD }} \
      -p:AndroidSigningKeyAlias=${{ secrets.ANDROID_KEY_ALIAS }} \
      -p:AndroidSigningKeyPass=${{ secrets.ANDROID_KEY_PASSWORD }} \
      -o ./artifacts/android

- name: Upload artifact
  uses: actions/upload-artifact@v4
  with:
    name: android-release
    path: ./artifacts/android/*-Signed.aab
    retention-days: 14

Ο συνδυασμός ApplicationDisplayVersion (semver) και ApplicationVersion (integer, το versionCode για την Google Play) είναι κρίσιμος. Η Google Play απορρίπτει κάθε upload όπου το versionCode δεν είναι αυστηρά μεγαλύτερο από το προηγούμενο. Χρησιμοποιώντας το github.run_number εξασφαλίζεις μονοτονική αύξηση χωρίς manual παρέμβαση.

Για ακόμα καλύτερα αποτελέσματα, ενεργοποίησε το AndroidLinkMode=SdkOnly για μικρότερο AAB και το AndroidPackageFormat=aab (όχι apk). Η Google Play απαιτεί App Bundle format από τον Αύγουστο του 2021 για κάθε νέο app. Το APK δουλεύει μόνο για internal distribution ή sideloading. Αν θες να δεις τα διαθέσιμα MSBuild properties, ο επίσημος πίνακας του .NET for Android είναι η πηγή αλήθειας.

iOS build και signing σε macOS runners

Το iOS είναι το σημείο όπου οι περισσότερες ομάδες παραλείπουν βήματα και μετά ξοδεύουν μια ολόκληρη μέρα στο debug. (Και ναι, το έχω κάνει μία Παρασκευή απόγευμα, δεν το συνιστώ.) Χρειάζεσαι τρία πράγματα σωστά για να δουλέψει το signing: ένα Distribution Certificate (.p12) στο keychain του runner, ένα Provisioning Profile (.mobileprovision) στο σωστό directory, και το Team ID/Bundle ID να ταιριάζει με ό,τι υπάρχει στο csproj.

Το βήμα setup δείχνει έτσι:

- name: Import Apple certificate & provisioning profile
  env:
    P12_BASE64: ${{ secrets.IOS_P12_CERTIFICATE_BASE64 }}
    P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }}
    PROFILE_BASE64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
    KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
  run: |
    # Δημιουργία temporary keychain
    KEYCHAIN_PATH=$RUNNER_TEMP/build.keychain
    security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
    security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
    security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH

    # Import του .p12 certificate
    echo "$P12_BASE64" | base64 --decode > $RUNNER_TEMP/cert.p12
    security import $RUNNER_TEMP/cert.p12 \
      -P "$P12_PASSWORD" \
      -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
    security list-keychain -d user -s $KEYCHAIN_PATH

    # Εγκατάσταση provisioning profile
    mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
    echo "$PROFILE_BASE64" | base64 --decode > \
      ~/Library/MobileDevice/Provisioning\ Profiles/build.mobileprovision

- name: Publish iOS IPA
  run: |
    dotnet publish src/MyApp/MyApp.csproj \
      -f net9.0-ios \
      -c Release \
      -p:ApplicationDisplayVersion=1.4.0 \
      -p:ApplicationVersion=${{ github.run_number }} \
      -p:RuntimeIdentifier=ios-arm64 \
      -p:ArchiveOnBuild=true \
      -p:CodesignKey="Apple Distribution: My Company (TEAM123ABC)" \
      -p:CodesignProvision="MyApp App Store" \
      -o ./artifacts/ios

Δύο τιμές πρέπει να ταιριάζουν ακριβώς. Το CodesignKey είναι το common name του certificate όπως εμφανίζεται στο Keychain Access (μπορείς να το επαληθεύσεις με security find-identity -v -p codesigning). Το CodesignProvision είναι το name του provisioning profile (όχι το UUID, όχι το filename). Αν κάτι δεν ταιριάζει, το xcodebuild αποτυγχάνει με ένα μπερδεμένο μήνυμα τύπου "No profiles matching 'X' found".

Για εναλλακτική προσέγγιση, δες τον επίσημο οδηγό της Microsoft για iOS deployment, ο οποίος τεκμηριώνει όλα τα MSBuild properties. Για αυτόματη διαχείριση certificates σε ομάδα (χωρίς manual export σε .p12), δες το fastlane match documentation. Γράφει τα certs σε ένα private git repo κρυπτογραφημένα και τα κάνει sync σε κάθε machine.

Δημοσίευση σε Google Play και App Store με fastlane

Αν και το dotnet publish παράγει το τελικό artifact, το upload στα stores είναι πολύ πιο εύκολο με fastlane. Το fastlane είναι Ruby-based, εγκαθίσταται σε 30 δευτερόλεπτα και έχει δύο βασικά actions για MAUI: supply (Google Play) και pilot (TestFlight).

Στο repo σου, δημιούργησε ένα fastlane/Fastfile:

default_platform(:android)

platform :android do
  desc "Upload AAB to Google Play internal track"
  lane :internal do
    upload_to_play_store(
      package_name: "com.mycompany.myapp",
      track: "internal",
      aab: "./artifacts/android/MyApp-Signed.aab",
      json_key_data: ENV["GOOGLE_PLAY_SERVICE_ACCOUNT_JSON"],
      skip_upload_metadata: true,
      skip_upload_images: true,
      skip_upload_screenshots: true
    )
  end
end

platform :ios do
  desc "Upload IPA to TestFlight"
  lane :beta do
    api_key = app_store_connect_api_key(
      key_id: ENV["APP_STORE_CONNECT_API_KEY_ID"],
      issuer_id: ENV["APP_STORE_CONNECT_ISSUER_ID"],
      key_content: ENV["APP_STORE_CONNECT_API_KEY_BASE64"],
      is_key_content_base64: true,
      duration: 1200
    )
    upload_to_testflight(
      api_key: api_key,
      ipa: "./artifacts/ios/MyApp.ipa",
      skip_waiting_for_build_processing: true
    )
  end
end

Και στο workflow, μετά το build:

- name: Setup Ruby & fastlane
  uses: ruby/setup-ruby@v1
  with:
    ruby-version: '3.3'
    bundler-cache: true

- name: Deploy to Google Play internal
  if: matrix.platform == 'android'
  env:
    GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
  run: bundle exec fastlane android internal

- name: Deploy to TestFlight
  if: matrix.platform == 'ios'
  env:
    APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
    APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
    APP_STORE_CONNECT_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }}
  run: bundle exec fastlane ios beta

Το App Store Connect API key αντικαθιστά τη χρήση Apple ID/password (που έχει καταργηθεί ούτως ή άλλως λόγω 2FA). Το ίδιο key μπορεί να ανακληθεί οποιαδήποτε στιγμή από το App Store Connect χωρίς να πειραχτεί ο λογαριασμός. Για ολοκληρωμένο workflow release που περιλαμβάνει και τα manual review steps, δες τον οδηγό δημοσίευσης σε App Store και Google Play.

Αυτόματο versioning και build caching

Το βασικό διακύβευμα σε ένα CI/CD pipeline είναι ο χρόνος. Ένα MAUI project από scratch μπορεί να χρειάζεται 15-25 λεπτά για full build και τα δύο platforms. Με σωστό caching, πέφτει στα 5-8 λεπτά. Δύο caches αξίζουν πραγματικά:

- name: Cache NuGet packages
  uses: actions/cache@v4
  with:
    path: ~/.nuget/packages
    key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/packages.lock.json') }}
    restore-keys: |
      ${{ runner.os }}-nuget-

- name: Cache .NET workloads
  uses: actions/cache@v4
  with:
    path: |
      ~/.dotnet/workloads
      ~/.dotnet/sdk-advertising
    key: ${{ runner.os }}-workloads-${{ env.DOTNET_VERSION }}-${{ env.MAUI_VERSION }}

Το NuGet cache γλυτώνει τα πιο πολλά λεπτά. Τα MAUI projects τραβάνε 200-400 packages. Το workload cache είναι μικρότερο κέρδος (~30 δευτερόλεπτα) αλλά αξίζει και αυτό.

Για versioning, εκτός από το github.run_number, μπορείς να χρησιμοποιήσεις git tags σαν πηγή αλήθειας. Ένα workflow που τρέχει μόνο σε v*.*.* tags μπορεί να παράγει την έκδοση από το tag name:

- name: Parse version from tag
  id: version
  run: |
    TAG=${GITHUB_REF#refs/tags/v}
    echo "display=$TAG" >> $GITHUB_OUTPUT
    echo "code=${{ github.run_number }}" >> $GITHUB_OUTPUT

- name: Publish
  run: |
    dotnet publish ... \
      -p:ApplicationDisplayVersion=${{ steps.version.outputs.display }} \
      -p:ApplicationVersion=${{ steps.version.outputs.code }}

Έτσι το tag v1.4.0 γίνεται το display version, ενώ ο μοναδικός run number γίνεται το internal build number. Και τα δύο stores δείχνουν το 1.4.0 στους χρήστες, αλλά εσωτερικά ξεχωρίζουν τα builds. Απλό και συνεπές.

Best practices και συχνά λάθη

Μερικά πράγματα που έχω μάθει με τον δύσκολο τρόπο:

1. Χώρισε PR builds από release builds

Στα PR runs δεν χρειάζεσαι signing. Ένα ξεχωριστό pr-check.yml workflow που κάνει μόνο dotnet build και dotnet test σε Ubuntu (χωρίς macOS) τρέχει σε 3 λεπτά αντί για 15. Κράτα το release workflow μόνο για tags ή manual dispatch. Για τη σχεδίαση των tests, δες τον οδηγό δοκιμών σε .NET MAUI.

2. Χρησιμοποίησε concurrency για να αποφύγεις race conditions

concurrency:
  group: release-${{ github.ref }}
  cancel-in-progress: false

Αν δύο pushes γίνουν σε γρήγορη διαδοχή, το δεύτερο θα περιμένει το πρώτο. Χωρίς αυτό, ενδέχεται δύο runs να προσπαθούν να ανεβάσουν το ίδιο version number. Και αυτό μοιάζει αστείο μέχρι να συμβεί σε πραγματικό release.

3. Μην κάνεις commit το bin/obj

Οι MAUI templates παράγουν συχνά ένα Platforms/Android/Resources folder που μπερδεύει το .gitignore. Επιβεβαίωσε ότι το obj/ και bin/ είναι πραγματικά ignored. Ένα stale obj folder στο repo προκαλεί mysterious build failures (κυριολεκτικά, "δουλεύει τοπικά αλλά όχι στο CI").

4. Σεβάσου το rate limit του App Store Connect API

Το API έχει όριο ~200 requests/hour. Το fastlane pilot κάνει ~10 requests ανά upload. Αν έχεις πάνω από 15 releases την ημέρα (π.χ. σε monorepo με πολλά apps), θα χτυπήσεις το limit. Χρησιμοποίησε skip_waiting_for_build_processing: true για να αποφύγεις άσκοπα polling calls.

5. Ποτέ μην κάνεις log τα secrets

Το GitHub Actions σκιάζει αυτόματα τα secrets στα logs, αλλά αν κάνεις echo ένα base64-decoded secret σε λάθος βήμα, θα βγει καθαρό. Πάντα χρησιμοποίησε redirect σε αρχείο (> file.p12), ποτέ σε stdout.

Τέλος, αν έρχεσαι από Xamarin.Forms, θα δεις ότι το MAUI CI είναι πολύ πιο απλό. Δεν χρειάζεσαι Windows agent για Android (το Ubuntu φτάνει) και δεν χρειάζεσαι Windows agent για UWP (αφού δεν υπάρχει πια). Ένα καθαρό migration από Xamarin.Forms σε .NET MAUI συχνά μειώνει τον χρόνο CI κατά 40%. Στο τελευταίο μου migration ήταν 42%, αλλά ας πούμε ότι το στρογγυλεύω.

Συχνές ερωτήσεις

Μπορώ να χτίσω .NET MAUI iOS app στο GitHub Actions χωρίς macOS runner;

Όχι. Το iOS toolchain (xcodebuild, codesign, actool) υπάρχει μόνο σε macOS. Χρειάζεσαι υποχρεωτικά macos-13, macos-14 ή macos-latest runner για το iOS build. Για το Android, μπορείς να χρησιμοποιήσεις ubuntu-latest, που είναι και φτηνότερο.

Πόσο κοστίζει ένα τυπικό MAUI CI/CD pipeline;

Στο GitHub free tier έχεις 2.000 λεπτά/μήνα για private repos. Το macOS runner καταναλώνει με πολλαπλασιαστή 10x, οπότε πραγματικά έχεις 200 λεπτά macOS. Ένα iOS build τρέχει ~10 λεπτά, άρα έχεις χώρο για περίπου 20 iOS builds/μήνα. Για μεγαλύτερη χρήση, χρειάζεσαι Team ($4/user/μήνα) ή self-hosted macOS runners.

Χρειάζομαι fastlane αν κάνω upload στα stores;

Δεν είναι υποχρεωτικό. Μπορείς να χρησιμοποιήσεις το επίσημο google-play-github-action για Android και τα Apple API endpoints κατευθείαν για TestFlight. Αλλά το fastlane είναι battle-tested, έχει documentation για κάθε edge case και το ίδιο script τρέχει και τοπικά (χρήσιμο για emergency releases όταν το CI έχει outage).

Πώς διαχειρίζομαι διαφορετικά bundle IDs για staging και production;

Χρησιμοποίησε MSBuild configurations. Στο csproj, όρισε <ApplicationId Condition="'$(Configuration)' == 'Staging'">com.mycompany.myapp.staging</ApplicationId>. Στο workflow, πέρασε -c Staging για ένα build και -c Release για το άλλο. Χρειάζεσαι ξεχωριστό provisioning profile για κάθε bundle ID.

Γιατί αποτυγχάνει το iOS build με "No profiles matching found";

Το πιο συχνό αίτιο είναι mismatch μεταξύ του CodesignProvision που δίνεις και του πραγματικού provisioning profile name. Τρέξε security cms -D -i profile.mobileprovision στο runner και έλεγξε το <key>Name</key>. Αυτή είναι η τιμή που πρέπει να δώσεις, όχι το filename ούτε το UUID. Επίσης βεβαιώσου ότι το bundle ID στο csproj ταιριάζει ακριβώς με αυτό στο profile.

Μπορώ να χρησιμοποιήσω self-hosted Mac για iOS builds;

Ναι, και αξίζει αν κάνεις πολλά releases. Ένα Mac mini M2 (~700€) πληρώνει τον εαυτό του σε περίπου 6 μήνες σε σχέση με τα macOS-hosted minutes. Πρόσεξε όμως τα security implications: το self-hosted runner τρέχει με τα δικαιώματα του user, οπότε δεν το βάζεις σε public repo και το απομονώνεις σε VLAN.