Zašto je CI/CD neophodan za .NET MAUI projekte
Ako ste ikad ručno potpisivali Android keystore, generirali IPA datoteku i onda se borili s App Store Connect uploadom u ponedjeljak ujutro — znate zašto je CI/CD tako važan. Razvoj s .NET MAUI donosi odličnu produktivnost jer jedan kôd pokriva Android, iOS, macOS i Windows. Ali kad dođe do izgradnje, potpisivanja i distribucije na trgovine, stvari se brzo zakompliciraju.
Ručno potpisivanje certifikatima, generiranje AAB ili IPA datoteka, prijenos na Google Play Console ili App Store Connect — sve to oduzima sate i otvara vrata greškama. A greške u potpisivanju znače da vaša aplikacija jednostavno neće stići do korisnika.
CI/CD (Continuous Integration / Continuous Delivery) cjevovodi rješavaju upravo to. Automatizacijom cijelog procesa — od kompilacije do isporuke — dobivate:
- Dosljednost — svaka izgradnja slijedi identične korake, nema više "ali na mom stroju radi"
- Brzinu — gurnite kôd u repozitorij i pipeline obavi ostatak
- Sigurnost — certifikati i lozinke pohranjeni su u zaštićenim spremištima tajni, ne u nečijem Slacku
- Skalabilnost — kako tim raste, automatizacija osigurava jednoobrazan proces za sve
U ovom vodiču proći ćemo kroz postavljanje CI/CD cjevovoda za .NET MAUI koristeći GitHub Actions i Azure DevOps. Pokriti ćemo izgradnju za Android i iOS, upravljanje certifikatima, potpisivanje i automatsku distribuciju na Google Play i App Store. Dakle, krenimo redom.
Preduvjeti i priprema projekta
Prije nego uopće krenemo s cjevovodima, trebate osigurati da vaš .NET MAUI projekt ispunjava određene zahtjeve. Ovo je korak koji mnogi preskoče — i onda se čude zašto im pipeline puca.
Struktura projekta i .NET verzija
Google Play od 2025. zahtijeva da aplikacije ciljaju Android 15 (API razina 35) ili novije. To znači da vam treba minimalno .NET 9 s net9.0-android ili net9.0-android35.0 ciljnim okvirom. Imajte na umu da je Microsoftova podrška za .NET MAUI 8 završila u svibnju 2025., pa je nadogradnja ionako neizbježna.
Provjerite da vaša .csproj datoteka izgleda otprilike ovako:
<PropertyGroup>
<TargetFrameworks>net9.0-android;net9.0-ios;net9.0-maccatalyst</TargetFrameworks>
<OutputType>Exe</OutputType>
<UseMaui>true</UseMaui>
<ApplicationId>com.vasafirma.mojaaplikacija</ApplicationId>
<ApplicationDisplayVersion>1.0.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
</PropertyGroup>
Svojstvo ApplicationVersion zaslužuje posebnu pažnju — svaki prijenos na trgovine zahtijeva inkrementalni broj. Kasnije ćemo vidjeti kako to automatizirati.
Priprema Android potpisnog materijala
Za Android distribuciju potreban vam je keystore — datoteka s privatnim ključem za potpisivanje aplikacije. Keystore generirate s keytool:
keytool -genkey -v -keystore mojaaplikacija.keystore \
-alias mojaaplikacija-key \
-keyalg RSA -keysize 2048 \
-validity 20000 \
-storepass VasaJakaLozinka \
-keypass VasaJakaLozinka
Parametar -validity 20000 postavlja rok valjanosti na približno 54 godine. Ovo je kritično — ako ključ istekne, nećete moći ažurirati aplikaciju na Google Playu. Ozbiljno, spremite keystore na sigurno mjesto i nikad ga ne commitajte u repozitorij.
Priprema iOS potpisnog materijala
iOS potpisivanje je, iskreno govoreći, malo kompleksnije. Trebaju vam dva resursa s Apple Developer portala:
- Distribucijski certifikat (.p12) — potvrđuje vaš identitet kao razvijatelja
- Provisioning profil (.mobileprovision) — povezuje aplikaciju, certifikat i ciljne uređaje
Certifikat izvozite iz Keychain Access na macOS-u u .p12 formatu s lozinkom. Provisioning profil preuzmite s Apple Developer portala — za App Store distribuciju odaberite "App Store" tip profila.
Evo postupka izvoza certifikata iz Keychain Accessa:
- Otvorite Keychain Access na macOS-u
- U kategoriji "My Certificates" pronađite distribucijski certifikat (obično nešto poput "Apple Distribution: Vaša Firma")
- Desnom tipkom kliknite na certifikat i odaberite "Export..."
- Odaberite format .p12 (Personal Information Exchange)
- Postavite jaku lozinku — trebat će vam u CI/CD konfiguraciji
Za provisioning profil, idite na Apple Developer Portal → Certificates, Identifiers & Profiles → Profiles. Kreirajte novi profil tipa "App Store" povezan s vašim App ID-jem i distribucijskim certifikatom, te preuzmite generiranu .mobileprovision datoteku.
AAB nasuprot APK — što odabrati za distribuciju
Kratki odgovor: AAB. Google Play od 2021. zahtijeva Android App Bundle (AAB) format za sve nove aplikacije. I zapravo ima smisla — AAB donosi značajne prednosti:
- Manja veličina preuzimanja — Google Play generira optimizirane APK-ove za svaki uređaj
- Dynamic delivery — mogućnost isporuke značajki na zahtjev
- Play App Signing — Google upravlja potpisnim ključem za distribuciju, vi zadržavate upload ključ
U CI/CD cjevovodu to znači da ćete u dotnet publish uvijek koristiti /p:AndroidPackageFormats=aab. Ako vam za interno testiranje treba i APK (npr. za izravnu instalaciju na uređaj), možete generirati oboje:
dotnet publish -f net9.0-android /p:AndroidPackageFormats="aab;apk"
GitHub Actions: Postavljanje CI/CD cjevovoda
GitHub Actions je CI/CD platforma integrirana izravno u GitHub. Automatizira radne tokove na temelju događaja u repozitoriju — push, pull request, kreiranje taga. Za .NET MAUI projekte tipično koristimo dva odvojena posla: jedan za Android (na Windows runneru) i jedan za iOS (na macOS runneru).
Upravljanje tajnama u GitHub Actions
Prvo, pohranite osjetljive podatke kao GitHub Secrets. Idite na Settings → Secrets and variables → Actions i dodajte sljedeće:
ANDROID_KEYSTORE_BASE64— Base64 kodirana keystore datotekaANDROID_KEYSTORE_PASSWORD— lozinka keystoreaANDROID_KEY_ALIAS— alias ključaANDROID_KEY_PASSWORD— lozinka ključaIOS_P12_BASE64— Base64 kodirani .p12 certifikatIOS_P12_PASSWORD— lozinka certifikataIOS_PROVISION_PROFILE_BASE64— Base64 kodirani provisioning profilAPPSTORE_API_KEY_ID— ID API ključa za App Store ConnectAPPSTORE_ISSUER_ID— Issuer ID za App Store ConnectAPPSTORE_PRIVATE_KEY— sadržaj .p8 privatnog ključa
Za kodiranje datoteka u Base64:
# Na macOS-u
base64 -i mojaaplikacija.keystore | pbcopy
# Na Windowsu
certutil -encode mojaaplikacija.keystore encoded.txt
Android workflow s GitHub Actions
Kreirajte datoteku .github/workflows/android-build.yml u repozitoriju:
name: Android Build & Deploy
on:
push:
tags:
- 'v*'
workflow_dispatch:
env:
DOTNET_VERSION: '9.0.x'
CSPROJ_PATH: 'src/MojaAplikacija/MojaAplikacija.csproj'
jobs:
build-android:
runs-on: windows-latest
timeout-minutes: 30
steps:
- name: Checkout koda
uses: actions/checkout@v4
- name: Postavljanje .NET SDK-a
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Instalacija MAUI workloada
run: dotnet workload install maui-android
- name: Dekodiranje keystorea
run: |
echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" > keystore.b64
certutil -decode keystore.b64 mojaaplikacija.keystore
shell: cmd
- name: Objava Android AAB-a
run: |
dotnet publish ${{ env.CSPROJ_PATH }} ^
-c Release ^
-f net9.0-android ^
/p:AndroidPackageFormats=aab ^
/p:AndroidKeyStore=true ^
/p:AndroidSigningKeyStore=mojaaplikacija.keystore ^
/p:AndroidSigningKeyAlias=${{ secrets.ANDROID_KEY_ALIAS }} ^
/p:AndroidSigningKeyPass="${{ secrets.ANDROID_KEY_PASSWORD }}" ^
/p:AndroidSigningStorePass="${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" ^
/p:ApplicationVersion=${{ github.run_number }}
shell: cmd
- name: Prijenos AAB artefakta
uses: actions/upload-artifact@v4
with:
name: android-aab
path: '**/*.aab'
- name: Objava na Google Play
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT }}
packageName: com.vasafirma.mojaaplikacija
releaseFiles: '**/*.aab'
track: internal
status: completed
Workflow se pokreće pushom taga koji počinje s v (npr. v1.0.0) ili ručnim pokretanjem. Obratite pažnju na github.run_number za automatsko inkrementiranje ApplicationVersion — svaka izgradnja dobiva jedinstveni, rastući broj. Jednostavno i elegantno.
iOS workflow s GitHub Actions
iOS izgradnja zahtijeva macOS runner i složeniji postupak s certifikatima. Ovo je dio koji zna zadaviti glavobolje, ali kad jednom profunkcionira, radi pouzdano. Kreirajte .github/workflows/ios-build.yml:
name: iOS Build & Deploy
on:
push:
tags:
- 'v*'
workflow_dispatch:
env:
DOTNET_VERSION: '9.0.x'
CSPROJ_PATH: 'src/MojaAplikacija/MojaAplikacija.csproj'
XCODE_VERSION: '16.2'
jobs:
build-ios:
runs-on: macos-15
timeout-minutes: 45
steps:
- name: Checkout koda
uses: actions/checkout@v4
- name: Odabir Xcode verzije
run: sudo xcode-select -switch /Applications/Xcode_${{ env.XCODE_VERSION }}.app
- name: Postavljanje .NET SDK-a
uses: actions/setup-dotnet@v4
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Instalacija MAUI workloada
run: dotnet workload install maui-ios
- name: Instalacija certifikata i profila
env:
P12_BASE64: ${{ secrets.IOS_P12_BASE64 }}
P12_PASSWORD: ${{ secrets.IOS_P12_PASSWORD }}
PROVISION_BASE64: ${{ secrets.IOS_PROVISION_PROFILE_BASE64 }}
run: |
# Kreiranje privremenog keychaina
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
KEYCHAIN_PASSWORD=$(openssl rand -hex 16)
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Dekodiranje i uvoz certifikata
echo "$P12_BASE64" | base64 --decode > $RUNNER_TEMP/certificate.p12
security import $RUNNER_TEMP/certificate.p12 \
-P "$P12_PASSWORD" \
-A -t cert -f pkcs12 \
-k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
# Dekodiranje provisioning profila
echo "$PROVISION_BASE64" | base64 --decode > $RUNNER_TEMP/profile.mobileprovision
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $RUNNER_TEMP/profile.mobileprovision \
~/Library/MobileDevice/Provisioning\ Profiles/
- name: Objava iOS IPA
run: |
dotnet publish ${{ env.CSPROJ_PATH }} \
-c Release \
-f net9.0-ios \
/p:ArchiveOnBuild=true \
/p:RuntimeIdentifier=ios-arm64 \
/p:CodesignKey="Apple Distribution" \
/p:CodesignProvision="MojDistribucijskiProfil" \
/p:ApplicationVersion=${{ github.run_number }}
- name: Prijenos IPA artefakta
uses: actions/upload-artifact@v4
with:
name: ios-ipa
path: '**/*.ipa'
- name: Prijenos na TestFlight
uses: apple-actions/upload-testflight-build@v1
with:
app-path: '**/*.ipa'
issuer-id: ${{ secrets.APPSTORE_ISSUER_ID }}
api-key-id: ${{ secrets.APPSTORE_API_KEY_ID }}
api-private-key: ${{ secrets.APPSTORE_PRIVATE_KEY }}
- name: Čišćenje keychaina
if: always()
run: security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
Par ključnih detalja o ovom workflowu:
- Timeout od 45 minuta — macOS poslovi su znatno sporiji (i skuplji) od Windows poslova. Limit sprječava beskonačno izvršavanje i neplanirane troškove
- Privremeni keychain — kreiramo izolirani keychain za potpisivanje, čime izbjegavamo konflikte s postojećim certifikatima na runneru
- ArchiveOnBuild=true — ova zastavica osigurava generiranje IPA datoteke
- Čišćenje keychaina — korak s
if: always()briše osjetljive podatke čak i ako prethodni koraci ne uspiju
Kombinirani workflow s matričnom strategijom
Za naprednije scenarije, možete koristiti krovni workflow koji poziva ugnježđene workflowe za svaku platformu. Kreirajte .github/workflows/release.yml:
name: Release Pipeline
on:
push:
tags:
- 'v*'
jobs:
android:
uses: ./.github/workflows/android-build.yml
secrets: inherit
ios:
uses: ./.github/workflows/ios-build.yml
secrets: inherit
create-release:
needs: [android, ios]
runs-on: ubuntu-latest
steps:
- name: Preuzimanje artefakata
uses: actions/download-artifact@v4
- name: Kreiranje GitHub Release
uses: softprops/action-gh-release@v2
with:
files: |
android-aab/**/*.aab
ios-ipa/**/*.ipa
generate_release_notes: true
Ovaj pristup omogućuje paralelnu izgradnju za obje platforme i automatsko kreiranje GitHub Releasea s generiranim bilješkama. Čisto i pregledno.
Azure DevOps: Postavljanje CI/CD cjevovoda
Azure DevOps nudi robustan sustav cjevovoda koji se jako dobro uklapa u Microsoftov ekosustav. Za .NET MAUI koristimo YAML definicije sa zasebnim fazama za svaku platformu.
Upravljanje tajnama u Azure DevOps
Azure DevOps ima dva mehanizma za pohranu osjetljivih podataka:
- Secure Files — za binarne datoteke poput keystorea, .p12 certifikata i provisioning profila. Pronađite ih pod Pipelines → Library → Secure files
- Variable Groups — za tekstualne tajne kao što su lozinke i API ključevi. Idite na Pipelines → Library → Variable groups
Prenesite potpisne datoteke u Secure Files i kreirajte Variable Group s nazivom MauiSigningVariables. Dodajte varijable poput AndroidKeystorePassword, AndroidKeyAlias, iOSCertificatePassword i označite ih kao tajne klikom na ikonu lokota.
Android cjevovod u Azure DevOps
Kreirajte azure-pipelines-android.yml u korijenu repozitorija:
trigger:
tags:
include:
- 'v*'
pool:
vmImage: 'windows-latest'
variables:
- group: MauiSigningVariables
- name: buildConfiguration
value: 'Release'
- name: dotnetVersion
value: '9.0.x'
steps:
- task: UseDotNet@2
displayName: 'Instalacija .NET SDK'
inputs:
packageType: 'sdk'
version: '$(dotnetVersion)'
- task: CmdLine@2
displayName: 'Instalacija MAUI workloada'
inputs:
script: 'dotnet workload install maui-android'
- task: DownloadSecureFile@1
name: androidKeystore
displayName: 'Preuzimanje keystorea'
inputs:
secureFile: 'mojaaplikacija.keystore'
- task: CmdLine@2
displayName: 'Objava Android aplikacije'
inputs:
script: |
dotnet publish src/MojaAplikacija/MojaAplikacija.csproj ^
-c $(buildConfiguration) ^
-f net9.0-android ^
/p:AndroidPackageFormats=aab ^
/p:AndroidKeyStore=true ^
/p:AndroidSigningKeyStore=$(androidKeystore.secureFilePath) ^
/p:AndroidSigningKeyAlias=$(AndroidKeyAlias) ^
/p:AndroidSigningKeyPass="$(AndroidKeyPassword)" ^
/p:AndroidSigningStorePass="$(AndroidKeystorePassword)" ^
/p:ApplicationVersion=$(Build.BuildId)
- task: CopyFiles@2
displayName: 'Kopiranje AAB u staging direktorij'
inputs:
contents: '**/*.aab'
targetFolder: '$(Build.ArtifactStagingDirectory)'
flattenFolders: true
- task: PublishPipelineArtifact@1
displayName: 'Objava artefakta'
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)'
artifactName: 'android-build'
Za razliku od GitHub Actions, Azure DevOps koristi zadatak DownloadSecureFile@1 za pristup sigurnim datotekama. Varijabla $(androidKeystore.secureFilePath) automatski pokazuje na putanju preuzete datoteke na agentovom disku — nema ručnog Base64 dekodiranja.
iOS cjevovod u Azure DevOps
Ovdje Azure DevOps stvarno blista. iOS cjevovod koristi macOS pool i ugrađene zadatke za Apple certifikate koji dramatično pojednostavljuju konfiguraciju:
trigger:
tags:
include:
- 'v*'
pool:
vmImage: 'macos-15'
variables:
- group: MauiSigningVariables
- name: buildConfiguration
value: 'Release'
- name: dotnetVersion
value: '9.0.x'
steps:
- task: CmdLine@2
displayName: 'Odabir Xcode 16'
inputs:
script: 'sudo xcode-select -switch /Applications/Xcode_16.2.app/Contents/Developer'
- task: UseDotNet@2
displayName: 'Instalacija .NET SDK'
inputs:
packageType: 'sdk'
version: '$(dotnetVersion)'
- task: CmdLine@2
displayName: 'Instalacija MAUI workloada'
inputs:
script: 'dotnet workload install maui-ios'
- task: InstallAppleCertificate@2
displayName: 'Instalacija distribucijskog certifikata'
inputs:
certSecureFile: 'distribution.p12'
certPwd: '$(iOSCertificatePassword)'
keychain: 'temp'
- task: InstallAppleProvisioningProfile@1
displayName: 'Instalacija provisioning profila'
inputs:
provisioningProfileLocation: 'secureFiles'
provProfileSecureFile: 'appstore.mobileprovision'
- task: CmdLine@2
displayName: 'Objava iOS aplikacije'
inputs:
script: |
dotnet publish src/MojaAplikacija/MojaAplikacija.csproj \
-c $(buildConfiguration) \
-f net9.0-ios \
/p:ArchiveOnBuild=true \
/p:RuntimeIdentifier=ios-arm64 \
/p:CodesignKey="$(APPLE_CERTIFICATE_SIGNING_IDENTITY)" \
/p:CodesignProvision="$(APPLE_PROV_PROFILE_NAME)" \
/p:ApplicationVersion=$(Build.BuildId)
- task: CopyFiles@2
displayName: 'Kopiranje IPA datoteke'
inputs:
contents: '**/*.ipa'
targetFolder: '$(Build.ArtifactStagingDirectory)'
flattenFolders: true
- task: PublishPipelineArtifact@1
displayName: 'Objava artefakta'
inputs:
targetPath: '$(Build.ArtifactStagingDirectory)'
artifactName: 'ios-build'
- task: AppStoreRelease@1
displayName: 'Prijenos na App Store Connect'
inputs:
serviceEndpoint: 'AppleAppStoreConnection'
appIdentifier: 'com.vasafirma.mojaaplikacija'
appType: 'iOS'
releaseTrack: 'TestFlight'
shouldSkipWaitingForProcessing: true
Prednost ovog pristupa su ugrađeni zadaci InstallAppleCertificate@2 i InstallAppleProvisioningProfile@1. Oni automatski upravljaju keychainima i čišćenjem, što znatno pojednostavljuje workflow u usporedbi s ručnim skriptiranjem u GitHub Actions. Manje koda, manje grešaka.
Višefazni cjevovod s odobrenjima u Azure DevOps
Azure DevOps podržava okruženja (environments) s odobrenjima i provjerama. To je fantastično za kontrolu distribucije — automatski izgradite aplikaciju, ali zahtijevajte ručno odobrenje prije prijenosa na trgovinu:
stages:
- stage: Build
displayName: 'Izgradnja i testiranje'
jobs:
- job: BuildAndroid
pool:
vmImage: 'windows-latest'
steps:
- task: UseDotNet@2
inputs:
version: '9.0.x'
- script: dotnet workload install maui-android
- script: dotnet build -c Release -f net9.0-android
- script: dotnet test tests/ --configuration Release
- stage: Deploy
displayName: 'Distribucija na Google Play'
dependsOn: Build
condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/v'))
jobs:
- deployment: DeployToGooglePlay
environment: 'Production'
strategy:
runOnce:
deploy:
steps:
- task: GooglePlayRelease@4
inputs:
serviceConnection: 'GooglePlayConnection'
applicationId: 'com.vasafirma.mojaaplikacija'
action: 'SingleBundle'
bundleFile: '$(Pipeline.Workspace)/**/*.aab'
track: 'internal'
S deployment poslom i environment: 'Production', Azure DevOps će automatski zatražiti odobrenje prije distribucije. Odobrenja se konfiguriraju u Pipelines → Environments → Production → Approvals and checks. Ovo je osobito korisno u većim timovima gdje ne želite da svaki push automatski ode u produkciju.
Automatsko verzioniranje aplikacije
Ovo je jedno od najčešćih pitanja kad se postavljaju CI/CD cjevovodi za mobilne aplikacije. Svaki prijenos na Google Play ili App Store zahtijeva veći broj verzije od prethodnog — i nitko ne želi to ručno pratiti.
Korištenje broja izgradnje
Najjednostavniji pristup — koristite ugrađeni broj izgradnje CI/CD sustava:
<!-- U .csproj datoteci -->
<PropertyGroup>
<ApplicationDisplayVersion>1.2.0</ApplicationDisplayVersion>
<!-- ApplicationVersion se postavlja iz CI/CD-a -->
</PropertyGroup>
# GitHub Actions
/p:ApplicationVersion=${{ github.run_number }}
# Azure DevOps
/p:ApplicationVersion=$(Build.BuildId)
Semantičko verzioniranje s Git oznakama
Za sofisticiraniji pristup, izvucite verziju iz Git oznake:
# U GitHub Actions workflowu
- name: Izvlačenje verzije iz oznake
id: version
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
- name: Objava
run: |
dotnet publish ${{ env.CSPROJ_PATH }} \
/p:ApplicationDisplayVersion=${{ steps.version.outputs.VERSION }} \
/p:ApplicationVersion=${{ github.run_number }}
Ovaj pristup odvaja ApplicationDisplayVersion (vidljivu korisnicima, npr. "1.2.0") od ApplicationVersion (interni inkrementirajući broj). Moj savjet? Krenite s ovim pristupom od početka — puno je lakše nego naknadno migrirati.
Distribucija na trgovine aplikacija
Google Play distribucija
Za automatsku distribuciju na Google Play potrebno je:
- Servisni račun — kreirajte ga u Google Cloud Console s pristupom Google Play Developer API-ju
- Dozvole — dodijelite servisnom računu ulogu "Release manager" u Google Play Console
- Prva ručna objava — prvi AAB mora biti ručno prenesen putem Google Play Console. Ovo se ne može zaobići jer se njime uspostavlja veza između potpisnog ključa i aplikacije
Za GitHub Actions koristite akciju r0adkll/upload-google-play s JSON ključem servisnog računa u GitHub Secrets. Za Azure DevOps postoji Google Play Extension iz Visual Studio Marketplace.
Google Play nudi različite trake za distribuciju:
- internal — za interno testiranje (do 100 testera)
- alpha — za zatvoreno testiranje
- beta — za otvoreno testiranje
- production — za javnu objavu
Preporuka: automatski objavljivajte na internal traku, pa ručno promovirajte na više trake nakon testiranja. Ne žurite s produkcijom.
App Store distribucija
Za automatsku distribuciju na App Store koristite App Store Connect API:
- Kreirajte API ključ u App Store Connect → Users and Access → Integrations → App Store Connect API
- Preuzmite .p8 privatni ključ (pažnja: dostupan je samo jednom za preuzimanje!)
- Zabilježite Key ID i Issuer ID
Za GitHub Actions, akcija apple-actions/upload-testflight-build koristi te podatke za prijenos IPA na TestFlight. Za Azure DevOps, istu funkciju obavlja zadatak AppStoreRelease@1.
Važno: kao i kod Google Playa, prva verzija aplikacije mora biti ručno objavljena putem App Store Connect. Tek nakon toga automatizacija može preuzeti daljnje objave.
Codemagic kao alternativni CI/CD alat
Osim GitHub Actions i Azure DevOps, vrijedi spomenuti Codemagic — specijalizirani CI/CD alat za mobilne aplikacije s odličnom podrškom za .NET MAUI. Osobno mi se sviđa za manje projekte jer smanjuje količinu konfiguracije koja vam je potrebna.
Glavne prednosti:
- macOS runneri bez dodatnog troška — macOS strojevi uključeni su u sve planove, dok su kod GitHub Actions značajno skuplji
- Ugrađeno upravljanje certifikatima — automatsko dohvaćanje i obnavljanje iOS certifikata putem App Store Connect API-ja
- Automatsko verzioniranje — bez dodatne konfiguracije
- Jedan YAML za obje platforme — Android i iOS izgradnja u jednoj datoteci
Primjer Codemagic konfiguracije:
workflows:
maui-release:
name: MAUI Release
max_build_duration: 60
instance_type: mac_mini_m2
environment:
groups:
- signing_credentials
vars:
DOTNET_VERSION: "9.0"
CSPROJ_PATH: "src/MojaAplikacija/MojaAplikacija.csproj"
scripts:
- name: Instalacija .NET SDK-a
script: |
curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin \
--version $DOTNET_VERSION
echo 'export PATH="$HOME/.dotnet:$PATH"' >> ~/.zprofile
source ~/.zprofile
- name: Instalacija MAUI workloada
script: dotnet workload install maui
- name: Izgradnja i objava
script: |
dotnet publish $CSPROJ_PATH \
-c Release \
-f net9.0-ios \
/p:ArchiveOnBuild=true
artifacts:
- '**/*.ipa'
- '**/*.aab'
publishing:
app_store_connect:
auth: integration
submit_to_testflight: true
google_play:
credentials: $GCLOUD_SERVICE_ACCOUNT_CREDENTIALS
track: internal
Codemagic je posebno privlačan za manje timove i solo developere. Besplatni plan nudi 500 minuta mjesečno, što je dovoljno za većinu manjih projekata.
Testiranje u CI/CD cjevovodu
Automatsko testiranje nije opcija — trebalo bi biti integralni dio svakog cjevovoda. Dodajte korake za testiranje prije izgradnje:
# Izvršavanje unit testova
- name: Pokretanje unit testova
run: dotnet test tests/MojaAplikacija.Tests/MojaAplikacija.Tests.csproj --configuration Release --logger trx --results-directory TestResults
# Objava rezultata testova
- name: Objava rezultata testova
uses: actions/upload-artifact@v4
if: always()
with:
name: test-results
path: TestResults/*.trx
Kako strukturirati testove u cjevovodu:
- Unit testovi — na svakom pull requestu, brzi su i ne zahtijevaju emulator
- Integracijski testovi — prije objave, mogu zahtijevati simulatore
- UI testovi s Appiumom — na nightly buildovima ili prije produkcijskih objava (najsporiji, ali najsveobuhvatniji)
Za Azure DevOps, ekvivalentni koraci koriste DotNetCoreCLI@2:
- task: DotNetCoreCLI@2
displayName: 'Pokretanje testova'
inputs:
command: 'test'
projects: 'tests/**/*.csproj'
arguments: '--configuration Release --logger trx --results-directory $(Agent.TempDirectory)/TestResults'
- task: PublishTestResults@2
displayName: 'Objava rezultata testova'
condition: always()
inputs:
testResultsFormat: 'VSTest'
testResultsFiles: '$(Agent.TempDirectory)/TestResults/*.trx'
Preporučeni redoslijed koraka u cjevovodu: restore → build → test → publish → deploy. Ako testovi ne prođu, pipeline se prekida i neispravni kôd nikada ne dospijeva do korisnika. Upravo tako treba biti.
Napredne strategije i best practice
Keširanje za ubrzavanje izgradnje
NuGet paketi i MAUI workloadovi mogu se keširati za značajno ubrzanje:
- name: Keširanje NuGet paketa
uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }}
restore-keys: |
${{ runner.os }}-nuget-
Ovo može uštedjeti nekoliko minuta po izgradnji. Na macOS runnerima, gdje se minute broje (doslovno — s multiplikatorom 10x), svaka ušteđena minuta se isplati.
Uvjetna izgradnja prema grani
Različite grane mogu pokretati različite razine izgradnje:
on:
push:
branches:
- main # Puna izgradnja + distribucija
- develop # Samo izgradnja + testovi
pull_request:
branches:
- main # Samo testovi
Obavijesti o statusu izgradnje
Nitko ne želi saznati da je build pao tek kad pokušava napraviti release. Integrirajte obavijesti putem Slacka, Teamsa ili e-pošte:
- name: Obavijest na Slack
if: failure()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: 'Izgradnja za ${{ matrix.platform }} nije uspjela!'
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Zaštita tajni i sigurnosne preporuke
Ovo je tema koja se ne smije olako shvatiti. Evo ključnih praksi:
- Nikada ne commitajte potpisne materijale u repozitorij — koristite isključivo mehanizme tajni CI/CD sustava
- Rotirajte API ključeve periodično — posebno App Store Connect API ključeve
- Ograničite pristup tajnama — koristite environment protection rules ili odobrenja
- Auditirajte pristup — redovito pregledavajte tko ima pristup potpisnim materijalima
- Koristite privremene keychainove — za iOS izgradnje uvijek kreirajte izolirani keychain koji se briše nakon izgradnje
Usporedba GitHub Actions i Azure DevOps
Oba alata su sposobna za izgradnju .NET MAUI cjevovoda, ali imaju različite prednosti. Evo detaljne usporedbe koja bi vam trebala pomoći s odlukom.
GitHub Actions
- Integracija s GitHubom — ako vaš kôd već živi na GitHubu, ne trebate ništa dodatno
- Bogat ekosustav — tisuće gotovih akcija za specifične zadatke poput prijenosa na Google Play ili TestFlight
- Besplatne minute — 2.000 minuta mjesečno za javne repozitorije (macOS s multiplikatorom 10x)
- Jednostavna sintaksa — YAML format je intuitivan i dobro dokumentiran
- Ograničenja — iOS certifikati zahtijevaju ručno skriptiranje, napredna odobrenja traže GitHub Enterprise
Azure DevOps
- Microsoftov ekosustav — integracija s Azure Boards, Azure Repos i Visual Studio
- Ugrađeni zadaci za Apple —
InstallAppleCertificate@2iInstallAppleProvisioningProfile@1eliminiraju ručno skriptiranje - Napredna odobrenja — sustav okruženja s odobrenjima dostupan u svim planovima
- Variable Groups — centralizirano upravljanje varijablama između više cjevovoda
- Ograničenja — manja zajednica, složenija početna konfiguracija
Troškovi u 2026.
Trošak je značajan faktor, posebno za macOS runnere:
- GitHub Actions — macOS minute s multiplikatorom 10x. Privatni repozitoriji dobivaju 2.000 minuta na besplatnom planu
- Azure DevOps — besplatni plan s 1 paralelnim poslom i 1.800 minuta mjesečno. Dodatni poslovi od ~40 USD/mjesečno
- Codemagic — 500 besplatnih minuta s macOS M2 strojevima, premium od ~50 USD/mjesečno
Koncepti su gotovo identični — svi koriste YAML, paralelne poslove, tajne i artefakte. Odaberite alat koji se uklapa u vaš postojeći radni tok i proračun.
Rješavanje čestih problema
Hajmo biti iskreni — tijekom postavljanja CI/CD za .NET MAUI naletjet ćete na probleme. Evo najčešćih i kako ih riješiti:
- "Could not find any available provisioning profiles" — automatsko potpisivanje ne radi pouzdano u CI okruženju. Eksplicitno navedite certifikat i profil putem
CodesignKeyiCodesignProvisionparametara - Greška s verzijom Xcode-a — macOS runneri imaju više predinstaliranih verzija. Uvijek eksplicitno odaberite verziju s
xcode-select - Android potpisivanje ne uspijeva — provjerite da lozinke ne sadrže posebne znakove koji mogu uzrokovati probleme s parsiranjem u shellu. Koristite navodnike oko varijabli lozinki
- Spore macOS izgradnje — macOS runneri su 3-10x skuplji. Minimizirajte iOS izgradnje na nužne grane i agresivno keširajte
- Workload nije pronađen — instalirajte specifičan workload (
maui-androidilimaui-ios) umjesto punogmauiworkloada — brže je - Build timeout na macOS-u — iOS izgradnje mogu trajati 15-30 minuta. Postavite eksplicitni timeout (45 minuta) da izbjegnete iznenađenja na računu
- "The target framework is not supported" — MAUI workload nije ispravno instaliran. Dodajte
dotnet workload restorekao korak prije izgradnje - Problemi s paralelnim izgradnjama — ako Android i iOS dijele resurse (npr. NuGet keš), mogu nastati konflikti. Svaka platforma treba zasebni posao
Praktični kontrolni popis za postavljanje
Prije nego se bacite na konfiguraciju, provjerite jeste li sve pripremili. Ovaj popis pokriva oba okruženja:
- Projektna konfiguracija — .NET 9 ili noviji, ispravni ciljni okviri u .csproj, ApplicationId postavljen
- Android potpisivanje — keystore generiran, alias i lozinke dokumentirani, keystore pohranjen izvan repozitorija
- iOS potpisivanje — distribucijski certifikat izvezen u .p12, provisioning profil kreiran i preuzet, App Store Connect API ključ generiran
- CI/CD tajne — svi osjetljivi podaci pohranjeni u mehanizmu tajni odabranog alata
- Prva ručna objava — inicijalna verzija ručno prenesena na Google Play Console i App Store Connect
- Servisni računi — Google Cloud servisni račun i Apple App Store Connect API ključ s odgovarajućim dozvolama
- Testovi — barem osnovni unit testovi koji se mogu pokrenuti u CI okruženju bez emulatora
Zaključak
Postavljanje CI/CD cjevovoda za .NET MAUI aplikacije zahtijeva inicijalan ulog vremena — posebno oko konfiguracije potpisnih materijala i tajni. Ali jednom kad pipeline profunkcionira, svaka sljedeća objava postaje jednostavna poput guranja Git oznake. I to je osjećaj koji se teško opisuje dok ga ne iskusite.
Evo ključnih preporuka:
- Počnite s jednom platformom (Android je jednostavniji) i postupno dodajte ostale
- Uvijek koristite mehanizme tajni za osjetljive podatke — nikad hardkodirane vrijednosti
- Automatizirajte verzioniranje koristeći brojeve izgradnje
- Integrirajte testove u cjevovod prije koraka objave
- Počnite s distribucijom na interne testne trake, produkciju ostavite za ručnu promociju
S pravilno postavljenim CI/CD cjevovodom, vaš tim se može fokusirati na ono što je najvažnije — pisanje kvalitetnog koda — dok automatizacija brine o svemu ostalom. Timovi koji rano usvoje CI/CD prakse drastično smanjuju broj proizvodnih incidenata i ubrzavaju cikluse izdanja.
Bez obzira odaberete li GitHub Actions, Azure DevOps ili Codemagic, krenite s jednostavnim cjevovodom i iterativno ga nadograđujte. Svaki dodani korak automatizacije smanjuje rizik od pogrešaka i približava vas potpuno automatiziranom procesu isporuke.