Salta al contenuto principale

Build e deploy di app iOS con Gitea Actions

Build, firma e upload di app iOS su TestFlight da Gitea Actions: un Mac runner self-hosted con act_runner, oppure build in cloud da un runner Linux.

Workflow Gitea Actions che compila un'app iOS firmata per TestFlight

Gitea Actions può fare build di app iOS, ma solo su un Mac che registri tu, perché Gitea non ha runner ospitati e Xcode gira solo su macOS. Le due configurazioni pratiche sono un Mac che esegue act_runner in modalità host, oppure un normale runner Linux che invia la build iOS a un servizio di build cloud. Questa guida copre entrambi gli approcci per un'app Capacitor, con file di workflow che puoi inserire in .gitea/workflows/.

Le team scelgono Gitea perché vogliono il loro code su un'infrastruttura che controllano. Questo non dovrebbe cambiare solo perché l'app richiede Xcode. Entrambe le configurazioni qui sotto mantengono il codice sorgente sul vostro server.

Come Gitea Actions esegue i job

Gitea Actions usa una sintassi compatibile con GitHub Actions, e i job vengono eseguiti su act_runner istanze che registrate sul vostro server Gitea. Ogni runner pubblica etichette come ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest. La parte dopo i due punti determina come vengono eseguiti i job:

  • docker://image esegue il job in un container. È il comportamento predefinito per i runner Linux.
  • host esegue il job direttamente sulla macchina, con quello che c'è installato.

macOS non può girare in un container Docker, quindi un runner Mac deve usare una host label. Questo ha conseguenze che vedremo più avanti.

Due note di compatibilità prima di copiare un workflow da GitHub:

  • Gitea legge .gitea/workflows/ e fa fallback su .github/workflows/ se la prima cartella non esiste.
  • uses: actions/checkout@v4 funziona perché Gitea risolve le action da GitHub per impostazione predefinita (l'impostazione DEFAULT_ACTIONS_URL ). Le istanze air-gapped devono fare il mirror delle action che usano e referenziarle tramite URL completo.

Opzione 1: un runner Mac con act_runner

Registra il Mac

Scarica l'app act_runner binario per macOS (arm64 per Apple Silicon) dalla pagina di release di Gitea, quindi ottieni un token di registrazione da Amministrazione sito > Actions > Runners (a livello di istanza), oppure dalle impostazioni dell'organizzazione o del repository.

./act_runner register --no-interactive \
  --instance https://gitea.example.com \
  --token <registration_token> \
  --name mac-mini-01 \
  --labels macos-arm64:host

./act_runner daemon

Esegui il daemon come launch agent sotto un utente dedicato, così che si riavvii dopo i riavvii e abbia la propria keychain di login.

Installa la toolchain sull'host

La modalità host significa che il job usa gli strumenti del Mac stesso. Installa:

  • Xcode 26 (obbligatorio per gli upload su App Store Connect dal 28 aprile 2026) ed esegui sudo xcodebuild -license accept
  • Node.js 22 o superiore e Bun (Capacitor 8 richiede Node 22)
  • Ruby e fastlane, oppure usa un Gemfile con bundle exec
  • CocoaPods, solo se il tuo progetto iOS lo usa ancora. I nuovi progetti Capacitor 8 usano Swift Package Manager.

Il workflow

# .gitea/workflows/ios-mac.yaml
name: iOS release (Mac runner)

on:
  push:
    tags:
      - 'v*'

jobs:
  ios:
    runs-on: macos-arm64
    env:
      APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
      APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
      APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}
      P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
      APP_STORE_CONNECT_TEAM_ID: ${{ vars.APP_STORE_CONNECT_TEAM_ID }}
      PROFILE_NAME: ${{ vars.IOS_PROFILE_NAME }}
    steps:
      - uses: actions/checkout@v4
      - run: bun install --frozen-lockfile
      - run: bun run build
      - run: bunx cap sync ios
      - name: Decode signing files
        run: |
          echo "${{ secrets.IOS_CERTIFICATE_BASE64 }}" | base64 --decode > dist.p12
          echo "${{ secrets.IOS_PROFILE_BASE64 }}" | base64 --decode > profile.mobileprovision
      - run: bundle exec fastlane ios beta
      - name: Clean up
        if: always()
        run: rm -f dist.p12 profile.mobileprovision

Il beta lane importa il certificato in un keychain temporaneo, passa il progetto alla firma manuale, incrementa il numero di build da TestFlight, archivia e carica. Un Fastfile completo per un progetto Capacitor è in Build e deploy di app iOS con Bitbucket Pipelines; funziona senza modifiche su Gitea. Per il contesto di fastlane, consulta guida alle GitHub Actions per iOS.

Il costo della modalità host

Il act_runner la documentazione è chiara a riguardo: i job sull'host non sono isolati. In pratica:

  • Ogni job condivide lo stesso utente, disco e keychain. Un job che si interrompe a metà può lasciare un keychain temporaneo o un provisioning profile obsoleto che fa fallire il successivo.
  • Qualsiasi workflow che punta a quel label esegue code su quel Mac con gli accessi di quell'utente. Registra runner host solo per i repository di cui ti fidi ed evita di eseguirli su pull request da fork.
  • DerivedData, archivi e simulator runtimes crescono finché il disco non è pieno.
  • Gli aggiornamenti di Xcode sono a tuo carico, e Apple alza il minimo SDK ogni primavera.
  • Un Mac builda un job alla volta. Code il giorno della release.

Se il tuo team ha già dei Mac e qualcuno se ne occupa, questa soluzione funziona. Altrimenti, l'opzione successiva rimuove il Mac dalla tua infrastruttura.

Opzione 2: Build iOS da un runner Linux con Capgo Build

Build Capgo compila e firma app Capacitor su Mac gestiti da Capgo. Il tuo job Gitea resta sul runner Linux che hai già. Fa il checkout del code, builda il layer web, esegue cap sync ios, e chiama build request. Il Capgo CLI carica il progetto nativo preparato, trasmette i log di Xcode nel log del job di Gitea ed esce con codice diverso da zero in caso di errore.

Questo modello si adatta bene a Gitea self-hosted:

  • Nessun accesso in entrata. Capgo non clona il tuo repository. Il tuo runner esegue il push del progetto preparato via HTTPS, quindi un server Gitea dietro firewall o VPN funziona senza modifiche.
  • Nessun token del repository. Non c'è nessuna connessione Git da configurare lato Capgo.
  • Il tuo codice sorgente web resta dov'è. Vengono caricati solo la cartella ios/ nativa, la config di Capacitor e le parti native dei plugin. src/, .git/, e .env i file non vengono caricati.

Configurazione una tantum

Su una macchina di sviluppo:

bunx @capgo/cli@latest login
bunx @capgo/cli@latest app add
bunx @capgo/cli@latest build init --platform ios

build init crea o riutilizza il tuo certificato di distribuzione e il provisioning profile dell'App Store e salva la chiave API di App Store Connect. Esegui una build locale per verificare che la firma funzioni:

bun run build && bunx cap sync ios
bunx @capgo/cli@latest build request com.example.app --platform ios --build-mode release

Poi esporta le credenziali:

bunx @capgo/cli@latest build credentials manage --appId com.example.app --platform ios
# select "Export to .env"

Aggiungi i secret a Gitea

Apri Impostazioni repository > Actions > Secrets e aggiungi ogni chiave del file esportato, più la tua chiave Capgo API:

Segreto Scopo
CAPGO_TOKEN Chiave Capgo API con permesso di upload
BUILD_CERTIFICATE_BASE64 Certificato di distribuzione (.p12)
P12_PASSWORD Password del certificato
CAPGO_IOS_PROVISIONING_MAP_BASE64 Mappatura del provisioning profile
APPLE_KEY_ID ID della chiave API di App Store Connect
APPLE_ISSUER_ID Issuer ID di App Store Connect
APPLE_KEY_CONTENT Base64 .p8 chiave
APP_STORE_CONNECT_TEAM_ID ID team Apple

Gitea rifiuta nomi di secret e variabili che iniziano con GITEA_ oppure GITHUB_, quindi mantieni questi nomi invariati. Inserisci i valori non segreti come l'app ID in Variabili e leggili tramite ${{ vars.NAME }}. I secret impostati a livello di organizzazione sono condivisi da ogni repository, il che è comodo se distribuisci diverse app con lo stesso team Apple. Elimina il file esportato una volta terminato.

Rilascio al push del tag

# .gitea/workflows/ios-release.yaml
name: iOS release

on:
  push:
    tags:
      - 'v*'

jobs:
  release-ios:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
      - run: bun install --frozen-lockfile
      - run: bun run build
      - run: bunx cap sync ios
      - name: Build, sign, and upload to TestFlight
        env:
          CAPGO_TOKEN: ${{ secrets.CAPGO_TOKEN }}
          BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
          P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
          CAPGO_IOS_PROVISIONING_MAP_BASE64: ${{ secrets.CAPGO_IOS_PROVISIONING_MAP_BASE64 }}
          APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
          APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
          APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}
          APP_STORE_CONNECT_TEAM_ID: ${{ secrets.APP_STORE_CONNECT_TEAM_ID }}
        run: |
          bunx @capgo/cli@latest build request ${{ vars.APP_ID }} \
            --platform ios \
            --build-mode release \
            --ios-distribution app_store \
            --store-release-name "${{ gitea.ref_name }}"

Note:

  • ${{ gitea.ref_name }} è il nome del tag. Il github.* context funziona anche come alias, utile durante il porting dei workflow.
  • Gitea imposta CI=true in ogni job, così Capgo CLI non aspetta mai input interattivo.
  • Capgo incrementa il build number dall'ultima build su TestFlight, così due tag di fila non collidono mai.
  • Senza --submit-to-store-review, la build arriva su TestFlight e si ferma. Aggiungi il flag se vuoi che la CI invii anche per l'App Review.

Il rilascio ora è git tag v2.3.0 && git push origin v2.3.0. Chiunque possa fare il push di un v* tag can ship, so add a protected tag rule for v* sotto Impostazioni > Tag e limitalo ai release manager.

Build ad hoc per i tester

Per le build QA da installare direttamente su dispositivi registrati, compila in modalità ad hoc e ottieni un link di download:

# .gitea/workflows/ios-adhoc.yaml
name: iOS ad hoc build

on:
  workflow_dispatch:

jobs:
  adhoc:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: oven-sh/setup-bun@v2
      - run: bun install --frozen-lockfile && bun run build
      - run: bunx cap sync ios
      - name: Ad hoc build
        env:
          CAPGO_TOKEN: ${{ secrets.CAPGO_TOKEN }}
          BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
          P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
          APPLE_KEY_ID: ${{ secrets.APPLE_KEY_ID }}
          APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }}
          APPLE_KEY_CONTENT: ${{ secrets.APPLE_KEY_CONTENT }}
          APP_STORE_CONNECT_TEAM_ID: ${{ secrets.APP_STORE_CONNECT_TEAM_ID }}
        run: |
          echo "${{ secrets.ADHOC_PROFILE_BASE64 }}" | base64 --decode > adhoc.mobileprovision
          bunx @capgo/cli@latest build request ${{ vars.APP_ID }} \
            --platform ios \
            --ios-distribution ad_hoc \
            --ios-provisioning-profile ./adhoc.mobileprovision \
            --output-upload --output-retention 3d \
            --output-record /tmp/build.json
          bunx @capgo/cli@latest build last-output --path /tmp/build.json --field outputUrl

Le build ad hoc richiedono un profilo di provisioning ad hoc che elenchi i dispositivi di test. Conserva quel profilo codificato in base64 come segreto (ADHOC_PROFILE_BASE64 sopra), decodificalo nel job e passalo con --ios-provisioning-profile. I flag CLI hanno la precedenza sulle variabili d'ambiente, quindi questo sovrascrive il profilo App Store solo per questa build. Raccogli gli UDID con Trova UDID iOS. Per la maggior parte dei tester, il testing interno su TestFlight è più semplice, perché non richiede UDID.

Approvazione manuale del rilascio

GitHub ti consente di richiedere dei revisori su un ambiente. Gitea Actions non implementa le regole di protezione degli ambienti, quindi un job che specifica un ambiente non si fermerà in attesa di approvazione. In alternativa funzionano due pattern:

  1. Tag protetti. Solo i release manager possono fare il push v* dei tag, e il workflow dei tag parte.
  2. Build, poi promuovi. Build ad ogni push su main con --ios-distribution ad_hoc o fai upload su TestFlight solo per i test interni, poi usa un workflow_dispatch workflow (disponibile nelle release Gitea attuali) per ricostruire il commit approvato con --submit-to-store-review.

Distribuisci modifiche web senza un nuovo binario

La maggior parte dei commit a un'app Capacitor cambia solo JavaScript, CSS e HTML. Ricompilare iOS per quelli spreca minuti e tempo di App Review. Con Live updates di Capgo, un solo job può scegliere il percorso giusto:

      - name: Live update or native build
        env:
          CAPGO_TOKEN: ${{ secrets.CAPGO_TOKEN }}
          # plus the iOS signing secrets from above
        run: |
          if bunx @capgo/cli@latest build needed ${{ vars.APP_ID }} --channel production; then
            bunx @capgo/cli@latest bundle upload ${{ vars.APP_ID }} --channel production
          else
            bunx cap sync ios
            bunx @capgo/cli@latest build request ${{ vars.APP_ID }} --platform ios --build-mode release
          fi

build needed esce con 0 quando le dipendenze native corrispondono a quelle attive sul canale, e con 1 quando serve un nuovo binario. Forza inoltre il percorso nativo quando i file sotto ios/ oppure capacitor.config.* cambiano, poiché non rientrano nel confronto delle dipendenze. Vedi Scegli automaticamente live update o Native Build.

Runner Mac vs cloud build

act_runner sul tuo Mac Build di Capgo da Linux
Hardware Lo acquisti e lo ospiti tu Nessuna
Aggiornamenti di Xcode Manuale, per singola macchina Gestito da Capgo
Isolamento tra i job Nessuno in modalità host Ambiente di build fresco per ogni job
Build parallele Uno per Mac Multipli, da qualsiasi runner
File di firma Nei segreti Gitea, decodificati sul Mac Nei segreti Gitea, inviati per build
Funziona dietro un firewall Sì Sì, solo HTTPS in uscita
App native Swift (senza Capacitor) Sì No, solo app Capacitor
Tempo massimo di build Configurabile da te 10 minuti per build

Risoluzione problemi

Sintomo Soluzione
Il job resta in coda all'infinito Nessun runner online ha la label runs-on. Controlla l'elenco dei runner e l'ortografia delle etichette.
uses: actions/checkout@v4 il download non riesce Il runner non riesce a raggiungere GitHub. Replica l'action sul tuo Gitea e richiamala tramite URL completo.
Il valore del segreto è vuoto nel job Il nome inizia con GITEA_/GITHUB_, oppure il segreto è definito su un altro repository.
security: SecKeychainItemImport: MAC verification failed Errato .p12 password, oppure un .p12 creato da OpenSSL 3 senza -legacy. Riesportalo.
No profiles for 'com.example.app' were found Il profilo nei secrets non corrisponde al bundle ID o al certificato. Riesegui build init e riesporta.
Upload rifiutato per la versione SDK Il runner Mac ha ancora un Xcode precedente alla versione 26.
cap sync ios avvisa riguardo a CocoaPods su Linux Atteso. La macchina di build di Capgo installa i pod.

Per ulteriori modalità di errore, consulta CI/CD per Capacitor: errori comuni e il Capgo Build troubleshooting guide.

Riassunto

Gitea Actions gestisce iOS senza problemi una volta deciso dove eseguire Xcode. Un runner Mac in modalità host offre pieno controllo e piena responsabilità. Un job Linux che chiama Capgo Build mantiene invariata la configurazione di Gitea, non richiede accesso in ingresso e trasforma un rilascio TestFlight in un push di tag. Indipendentemente dalla scelta, conserva i materiali di firma nei segreti di Gitea, proteggi i tag di rilascio e pubblica le modifiche solo web come live updates, in modo che la pipeline nativa si esegua solo quando necessario.

Live updates per le app Capacitor

Quando un bug nel web-layer è live, rilascia la fix tramite Capgo invece di aspettare giorni l'approvazione degli store. Gli utenti ricevono l'update in background, mentre le modifiche native restano nel normale processo di review.

Ultimi articoli dal blog

Capgo ti offre gli insight migliori che ti servono per creare un'app mobile davvero professionale.