Saltar al contenido principal
CI/CD iOS Tutorial

Compila y despliega apps iOS con Gitea Actions

Compila, firma y sube apps iOS a TestFlight desde Gitea Actions: con un Mac runner self-hosted usando act_runner, o con builds en la nube desde un runner Linux.

Workflow de Gitea Actions para compilar app iOS firmada para TestFlight

Gitea Actions puede compilar apps iOS, pero solo en un Mac que tú mismo registres, porque Gitea no tiene runners alojados y Xcode solo corre en macOS. Las dos configuraciones prácticas son un Mac ejecutando act_runner en modo host, o un runner Linux normal que envía el build de iOS a un servicio de cloud build. Esta guía explica ambas opciones para una app Capacitor, con archivos de workflow que puedes añadir a .gitea/workflows/.

Los equipos eligen Gitea porque quieren tener su code en una infraestructura que controlan. Eso no debería cambiar solo porque la app necesite Xcode. Ambos setups mantienen tu código fuente en tu servidor.

Cómo Gitea Actions ejecuta los jobs

Gitea Actions usa una sintaxis compatible con GitHub Actions, y los jobs se ejecutan en act_runner instancias que registras en tu servidor Gitea. Cada runner anuncia labels como ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest. Lo que sigue a los dos puntos determina cómo se ejecutan los jobs:

  • docker://image ejecuta el job en un contenedor. Este es el valor por defecto para runners de Linux.
  • host ejecuta el job directamente en la máquina, con lo que haya instalado allí.

macOS no puede ejecutarse en un contenedor Docker, por lo que un runner de Mac debe usar un host label. Esto tiene consecuencias que veremos más adelante.

Dos notas de compatibilidad antes de copiar un workflow de GitHub:

  • Gitea lee .gitea/workflows/ and falls back to .github/workflows/ cuando la primera carpeta no existe.
  • uses: actions/checkout@v4 funciona porque Gitea resuelve las actions desde GitHub por defecto (el DEFAULT_ACTIONS_URL ajuste). Las instancias air-gapped deben replicar las actions que usan y referenciarlas con la URL completa.

Opción 1: Un runner de Mac con act_runner

Registrar el Mac

Descarga el act_runner binario para macOS (arm64 para Apple Silicon) desde la página de releases de Gitea, luego obtén un token de registro en Administración del sitio > Actions > Runners (a nivel de instancia), o desde la configuración de la organización o del repositorio.

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

./act_runner daemon

Ejecuta el daemon como un launch agent bajo un usuario dedicado para que se reinicie tras los reboots y tenga su propio login keychain.

Instala el toolchain en el host

El modo Host significa que el job usa las herramientas del propio Mac. Instala:

  • Xcode 26 (requerido para subir a App Store Connect desde el 28 de abril de 2026) y ejecuta sudo xcodebuild -license accept
  • Node.js 22 o superior y Bun (Capacitor 8 requiere Node 22)
  • Ruby y fastlane, o utiliza un Gemfile con bundle exec
  • CocoaPods, solo si tu proyecto iOS aún lo usa. Los nuevos proyectos Capacitor 8 usan Swift Package Manager.

El 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

El beta lane importa el certificado en un keychain temporal, cambia el proyecto a firma manual, incrementa el build number desde TestFlight, archiva y sube. Un Fastfile completo para un proyecto Capacitor está en Build y deploy de apps iOS con Bitbucket Pipelines; funciona igual en Gitea. Para saber más de fastlane, consulta la guía de GitHub Actions para iOS.

El coste del host mode

El act_runner la documentación es clara al respecto: los host jobs no están aislados. En la práctica:

  • Cada job comparte el mismo usuario, disco y keychain. Un job que muere a medio camino puede dejar un keychain temporal o un provisioning profile obsoleto que rompe el siguiente.
  • Cualquier workflow que apunte a ese label ejecuta code en ese Mac con el acceso de ese usuario. Registra host runners solo para repositorios en los que confíes y evita ejecutarlos en pull requests de forks.
  • DerivedData, archives y los runtimes del simulador crecen hasta llenar el disco.
  • Tú te encargas de las actualizaciones de Xcode, y Apple sube el SDK mínimo cada primavera.
  • Un Mac compila un job a la vez. Colas el día del release.

Si tu equipo ya tiene Macs y alguien se encarga de ellos, esto funciona. Si no, la siguiente opción saca el Mac de tu infraestructura.

Opción 2: compilar iOS desde un runner de Linux con Capgo Build

Build de Capgo compila y firma apps de Capacitor en Macs gestionados por Capgo. Tu job de Gitea se queda en el runner de Linux que ya tienes. Hace checkout del code, compila la capa web, ejecuta cap sync ios, y llama a build request. El Capgo CLI sube el proyecto nativo preparado, envía los logs de Xcode al log del job de Gitea y devuelve un error si falla.

Este modelo encaja bien con Gitea self-hosted:

  • Sin acceso entrante. Capgo no clona tu repositorio. Tu runner envía el proyecto preparado por HTTPS, así que un servidor Gitea detrás de un firewall o VPN funciona sin cambios.
  • Sin token de repositorio. No hay que configurar ninguna conexión Git del lado de Capgo.
  • Tu código fuente web se queda en casa. Solo el nativo ios/ carpeta nativa, la config de Capacitor y las partes nativas de los plugins se suben. src/, .git/, y .env los archivos no.

Configuración única

En una máquina de desarrollo:

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

build init crea o reutiliza tu certificado de distribución y el provisioning profile de App Store, y guarda la clave API de App Store Connect. Ejecuta un build localmente para confirmar que todo se firma correctamente:

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

Después, exporta las credenciales:

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

Añade los secrets a Gitea

Abrir Configuración del repositorio > Actions > Secrets y añade cada clave del archivo exportado, además de tu clave API de Capgo:

Secreto Propósito
CAPGO_TOKEN Clave API de Capgo con permiso de upload
BUILD_CERTIFICATE_BASE64 Certificado de distribución (.p12)
P12_PASSWORD Contraseña del certificado
CAPGO_IOS_PROVISIONING_MAP_BASE64 Mapeo de perfiles de aprovisionamiento
APPLE_KEY_ID ID de la clave API de App Store Connect
APPLE_ISSUER_ID Issuer ID de App Store Connect
APPLE_KEY_CONTENT Base64 .p8 clave
APP_STORE_CONNECT_TEAM_ID ID de equipo de Apple

Gitea rechaza nombres de secretos y variables que comienzan con GITEA_ o GITHUB_, así que mantén estos nombres tal cual. Coloca valores no secretos, como el ID de la app, bajo Variables y leerlas con ${{ vars.NAME }}. Los secrets definidos a nivel de organización se comparten entre todos los repositorios, lo cual es útil cuando publicas varias apps desde un mismo equipo de Apple. Elimina el archivo exportado cuando termines.

Release al hacer push de un 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 }}"

Notas:

  • ${{ gitea.ref_name }} es el nombre del tag. El github.* context también funciona como alias, lo que ayuda cuando portas workflows.
  • Gitea establece CI=true en cada job, así que el Capgo CLI nunca espera input interactivo.
  • Capgo incrementa el build number del último build de TestFlight, para que dos tags seguidos nunca colisionen.
  • Sin --submit-to-store-review, el build llega a TestFlight y se detiene. Añade el flag si quieres que el CI también envíe a App Review.

Hacer release ahora es git tag v2.3.0 && git push origin v2.3.0. Cualquiera que pueda hacer push de un v* tag puede publicarse, así que añade una regla de tag protegida para v* en Ajustes &gt; Etiquetas y limítalo a los release managers.

Builds ad hoc para testers

Para builds de QA que se instalan directamente en dispositivos registrados, haz el build en modo ad hoc y obtén un link de descarga:

# .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

Los builds ad hoc necesitan un provisioning profile ad hoc que enumere los dispositivos de prueba. Guarda ese perfil codificado en base64 como un secret (ADHOC_PROFILE_BASE64 arriba), decodifícalo en el job y pásalo con --ios-provisioning-profile. Los flags de CLI tienen prioridad sobre las variables de entorno, por lo que esto sobrescribe el perfil de App Store solo para este build. Recopila los UDIDs con el Buscador de UDID de iOS. Para la mayoría de los testers, el testing interno de TestFlight es más simple, ya que no requiere UDIDs.

Aprobación manual de releases

GitHub te permite exigir revisores en un entorno. Gitea Actions no implementa reglas de protección de entornos, por lo que un job que haga referencia a un entorno no se detendrá a esperar aprobación. En su lugar, funcionan estos dos patrones:

  1. Tags protegidos. Solo los release managers pueden hacer push v* de tags, y el workflow del tag se ejecuta.
  2. Build y luego promote. Build en cada push a main con --ios-distribution ad_hoc o sube solo a TestFlight para pruebas internas, y luego usa un workflow_dispatch workflow (disponible en las versiones actuales de Gitea) para reconstruir el commit aprobado con --submit-to-store-review.

Publica cambios web sin un nuevo binario

La mayoría de los commits a una app de Capacitor solo modifican JavaScript, CSS y HTML. Reconstruir iOS para eso desperdicia minutos y tiempo de revisión de la App. Con actualizaciones Live Update de Capgo, un solo job puede elegir el camino correcto:

      - 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 sale con 0 cuando las dependencias nativas coinciden con lo que está en vivo en el canal, y con 1 cuando se requiere un nuevo binario. Fuerza también el camino nativo cuando cambian archivos bajo ios/ o capacitor.config.* , ya que esos no forman parte de la comparación de dependencias. Consulta Elige automáticamente live update o Native Build.

Runner de Mac vs build en la nube

act_runner en tu Mac Capgo Build desde Linux
Hardware Lo compras y lo alojas tú Ninguno
Actualizaciones de Xcode Manual, por máquina Gestionado por Capgo
Aislamiento entre jobs Ninguno en modo host Entorno de build limpio por job
Builds en paralelo Uno por Mac Múltiples, desde cualquier runner
Archivos de firma En los secrets de Gitea, decodificados en el Mac En los secrets de Gitea, enviados por build
Funciona detrás de un firewall Sí Sí, solo HTTPS saliente
Apps nativas en Swift (sin Capacitor) Sí No, solo apps con Capacitor
Límite de tiempo de build Tú lo defines 10 minutos por build

Solución de problemas

Síntoma Solución
El job se queda en cola para siempre Ningún runner online tiene ese label runs-on. Revisa la lista de runners y la ortografía de las etiquetas.
uses: actions/checkout@v4 error al descargar El runner no puede acceder a GitHub. Replica la acción en tu Gitea y referénciala con la URL completa.
El valor del secret está vacío en el job El nombre comienza con GITEA_/GITHUB_, o el secret está definido en otro repositorio.
security: SecKeychainItemImport: MAC verification failed Incorrecto .p12 contraseña, o un .p12 creado por OpenSSL 3 sin -legacy. Expórtalo de nuevo.
No profiles for 'com.example.app' were found El perfil en secrets no coincide con el bundle ID o el certificado. Ejecuta de nuevo build init y vuelve a exportar.
Upload rechazado para la versión SDK El runner de Mac aún tiene una versión de Xcode anterior a la 26.
cap sync ios avisa sobre CocoaPods en Linux Esperado. La máquina de build de Capgo instala los pods.

Para ver más modos de fallo, consulta CI/CD para Capacitor: errores comunes y la guía de resolución de problemas de Capgo Build.

Resumen

Gitea Actions gestiona iOS sin problemas una vez que decides dónde se ejecuta Xcode. Un runner Mac en modo host te da control total y toda la responsabilidad. Un job de Linux que llama a Capgo Build mantiene tu configuración de Gitea intacta, no necesita acceso entrante y convierte un release de TestFlight en un push de tag. Elijas lo que elijas, guarda el material de firma en los secrets de Gitea, protege tus tags de release y publica los cambios solo de web como live updates para que el pipeline nativo solo se ejecute cuando haga falta.

Live updates para apps Capacitor

Cuando un bug de la capa web está en producción, publica el fix a través de Capgo en lugar de esperar días a la aprobación de la app store. Los usuarios reciben la actualización en segundo plano mientras los cambios nativos siguen el flujo de revisión habitual.

Lo último de nuestro blog

Capgo te da la información clave que necesitas para crear una app móvil realmente profesional.