← Volver al blog
Giancarlos Gaona·Publicado el 17 de marzo de 2026 · Actualizado el 3 de agosto de 2026·8 min de lectura

Automatizar builds de Android e iOS con GitHub Actions

DevOpsMobileGitHub Actions

Por qué automatizar los builds móviles

Compilar una app móvil manualmente es un proceso tedioso. En Android necesitas abrir Android Studio, esperar que Gradle sincronice, firmar el APK con tu keystore y exportarlo. En iOS la situación es peor: abrir Xcode, gestionar certificados y provisioning profiles, compilar, archivar y firmar. Multiplicar esto por cada release o cada fix urgente convierte el proceso en un cuello de botella real.

Con GitHub Actions puedes automatizar todo esto. Cada push a una rama específica o cada tag nuevo dispara un pipeline que compila, firma y sube los artefactos listos para distribuir. El equipo deja de depender de "la máquina de Juan que tiene el keystore" y el proceso se vuelve reproducible y auditable.

En este artículo voy a cubrir la configuración completa para proyectos que usan Capacitor como bridge nativo, aunque los conceptos aplican a cualquier proyecto React Native o nativo puro con ajustes mínimos.

Preparación: sincronizar Capacitor antes del build

Antes de compilar el proyecto nativo, Capacitor necesita sincronizar el código web con las plataformas nativas. Esto significa que tu pipeline debe:

  1. Instalar dependencias de Node.js
  2. Ejecutar el build web (npm run build)
  3. Ejecutar npx cap sync para copiar el bundle a android/ e ios/

Este paso es crítico y muchos pipelines fallan por omitirlo. El cap sync no solo copia archivos, también actualiza plugins nativos y sus dependencias.

- name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'

- name: Install dependencies
  run: npm ci

- name: Build web assets
  run: npm run build

- name: Sync Capacitor
  run: npx cap sync

Build de Android: generando el APK firmado

Configuración del runner y Java

Android necesita el JDK y las herramientas del SDK. Los runners de GitHub (ubuntu-latest) ya traen Java preinstalado, pero conviene fijar la versión para evitar sorpresas:

- name: Setup Java JDK
  uses: actions/setup-java@v4
  with:
    distribution: 'temurin'
    java-version: '17'
    cache: 'gradle'

La opción cache: 'gradle' es importante. Gradle descarga decenas de dependencias en cada build, y sin cache un build que debería tomar 3 minutos puede tomar 12.

Firmando el APK con el keystore

Nunca debes subir tu keystore al repositorio. En su lugar, lo codificas en base64 y lo guardas como secret de GitHub:

# En tu maquina local
base64 -w 0 mi-app-release.keystore > keystore-base64.txt

Luego creas estos secrets en GitHub:

  • ANDROID_KEYSTORE_BASE64: el contenido del archivo base64
  • ANDROID_KEYSTORE_PASSWORD: password del keystore
  • ANDROID_KEY_ALIAS: alias de la key
  • ANDROID_KEY_PASSWORD: password de la key

En el workflow decodificas el keystore antes de compilar:

- name: Decode keystore
  run: |
    echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/app/release.keystore

- name: Build signed APK
  working-directory: android
  run: |
    ./gradlew assembleRelease \
      -Pandroid.injected.signing.store.file=$PWD/app/release.keystore \
      -Pandroid.injected.signing.store.password=${{ secrets.ANDROID_KEYSTORE_PASSWORD }} \
      -Pandroid.injected.signing.key.alias=${{ secrets.ANDROID_KEY_ALIAS }} \
      -Pandroid.injected.signing.key.password=${{ secrets.ANDROID_KEY_PASSWORD }}

Si prefieres generar un AAB (Android App Bundle) para Google Play en lugar de un APK, reemplaza assembleRelease por bundleRelease. El AAB genera paquetes optimizados por dispositivo pero no se puede instalar directamente — necesitas bundletool para testear localmente.

Cache de Gradle

Además del cache integrado de setup-java, puedes cachear el directorio .gradle del proyecto:

- name: Cache Gradle wrapper
  uses: actions/cache@v4
  with:
    path: |
      ~/.gradle/caches
      ~/.gradle/wrapper
    key: gradle-${{ hashFiles('android/gradle/wrapper/gradle-wrapper.properties') }}
    restore-keys: gradle-

Build de iOS: generando el IPA

El runner de macOS

iOS requiere un runner macOS. No hay forma de compilar con Xcode en Linux. Esto tiene implicaciones de costo: desde el cambio de precios de enero de 2026, GitHub factura los runners por minuto en dólares, y un runner macOS cuesta 0,062 USD por minuto frente a 0,006 USD de un Linux de 2 núcleos, es decir, alrededor de diez veces más caro.

build-ios:
  runs-on: macos-14

Usar macos-14 te da un runner con Apple Silicon (M1), que es significativamente más rápido para compilar que los runners Intel (macos-13). Además, las versiones recientes de Xcode solo están disponibles en runners ARM.

Certificados y provisioning profiles

Esta es la parte más compleja de todo el pipeline. Apple requiere un certificado de distribución (un archivo .p12) y un provisioning profile (.mobileprovision) para firmar la app.

Debes crear estos secrets:

  • IOS_CERTIFICATE_BASE64: certificado .p12 en base64
  • IOS_CERTIFICATE_PASSWORD: password del certificado
  • IOS_PROVISION_PROFILE_BASE64: provisioning profile en base64

La instalación en el runner requiere crear un keychain temporal:

- name: Install Apple certificate and provisioning profile
  env:
    CERTIFICATE_BASE64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
    CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
    PROVISION_PROFILE_BASE64: ${{ secrets.IOS_PROVISION_PROFILE_BASE64 }}
  run: |
    # Crear variables
    CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
    PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
    KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db

    # Decodificar archivos
    echo -n "$CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
    echo -n "$PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH

    # Crear keychain temporal
    security create-keychain -p "" $KEYCHAIN_PATH
    security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
    security unlock-keychain -p "" $KEYCHAIN_PATH

    # Importar certificado
    security import $CERTIFICATE_PATH -P "$CERTIFICATE_PASSWORD" \
      -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
    security set-key-partition-list -S apple-tool:,apple: \
      -k "" $KEYCHAIN_PATH
    security list-keychain -d user -s $KEYCHAIN_PATH

    # Instalar provisioning profile
    mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
    cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles

Compilar el IPA

Con los certificados instalados, el build usa xcodebuild:

- name: Install CocoaPods dependencies
  working-directory: ios/App
  run: pod install

- name: Build iOS archive
  working-directory: ios/App
  run: |
    xcodebuild archive \
      -workspace App.xcworkspace \
      -scheme App \
      -configuration Release \
      -archivePath $RUNNER_TEMP/App.xcarchive \
      -destination 'generic/platform=iOS' \
      CODE_SIGN_IDENTITY="iPhone Distribution" \
      PROVISIONING_PROFILE_SPECIFIER="Mi App Distribution"

- name: Export IPA
  run: |
    xcodebuild -exportArchive \
      -archivePath $RUNNER_TEMP/App.xcarchive \
      -exportOptionsPlist ios/App/ExportOptions.plist \
      -exportPath $RUNNER_TEMP/output

El archivo ExportOptions.plist define el método de exportación (app-store, ad-hoc, enterprise). Para distribución vía TestFlight necesitas app-store.

Cache de CocoaPods

CocoaPods puede tomar varios minutos en instalar. El cache reduce esto drásticamente:

- name: Cache CocoaPods
  uses: actions/cache@v4
  with:
    path: ios/App/Pods
    key: pods-${{ hashFiles('ios/App/Podfile.lock') }}
    restore-keys: pods-

Workflow completo con builds condicionales

En la práctica, no quieres compilar en cada push a cualquier rama. La estrategia más común es compilar solo cuando se crea un tag de versión:

name: Build Mobile Apps

on:
  push:
    tags:
      - 'v*'

jobs:
  build-web:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      - run: npx cap sync
      - uses: actions/upload-artifact@v4
        with:
          name: web-assets
          path: |
            android/
            ios/

  build-android:
    needs: build-web
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: web-assets
      - uses: actions/setup-java@v4
        with:
          distribution: 'temurin'
          java-version: '17'
          cache: 'gradle'
      - name: Decode keystore
        run: echo "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > android/app/release.keystore
      - name: Build APK
        working-directory: android
        run: |
          ./gradlew assembleRelease \
            -Pandroid.injected.signing.store.file=$PWD/app/release.keystore \
            -Pandroid.injected.signing.store.password=${{ secrets.ANDROID_KEYSTORE_PASSWORD }} \
            -Pandroid.injected.signing.key.alias=${{ secrets.ANDROID_KEY_ALIAS }} \
            -Pandroid.injected.signing.key.password=${{ secrets.ANDROID_KEY_PASSWORD }}
      - uses: actions/upload-artifact@v4
        with:
          name: android-apk
          path: android/app/build/outputs/apk/release/*.apk

  build-ios:
    needs: build-web
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: web-assets
      - name: Install certificates
        run: |
          # (bloque completo de certificados mostrado arriba)
          echo "Certificates installed"
      - name: Install CocoaPods
        working-directory: ios/App
        run: pod install
      - name: Build and archive
        working-directory: ios/App
        run: |
          xcodebuild archive \
            -workspace App.xcworkspace \
            -scheme App \
            -configuration Release \
            -archivePath $RUNNER_TEMP/App.xcarchive \
            -destination 'generic/platform=iOS'
      - name: Export IPA
        run: |
          xcodebuild -exportArchive \
            -archivePath $RUNNER_TEMP/App.xcarchive \
            -exportOptionsPlist ios/App/ExportOptions.plist \
            -exportPath $RUNNER_TEMP/output
      - uses: actions/upload-artifact@v4
        with:
          name: ios-ipa
          path: ${{ runner.temp }}/output/*.ipa

Estrategia de versionado con git tags

Una convención que funciona bien es usar Semantic Versioning con tags de git:

git tag v1.2.0
git push origin v1.2.0

Dentro del workflow, puedes extraer la versión del tag para inyectarla en la app:

- name: Extract version from tag
  id: version
  run: echo "VERSION=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT

Luego usas ${{ steps.version.outputs.VERSION }} para actualizar build.gradle o Info.plist antes de compilar. Esto garantiza que la versión del binario coincida con el tag de git.

Problemas comunes y cómo resolverlos

Firma de Android falla con "keystore was tampered with": Generalmente ocurre cuando el base64 se corrompió al copiar. Verifica que usaste base64 -w 0 (sin line wraps) al codificar.

Xcode version mismatch: El runner puede tener una versión de Xcode diferente a la que esperas. Fija la versión con:

- name: Select Xcode version
  run: sudo xcode-select -s /Applications/Xcode_15.4.app

Puedes ver las versiones disponibles en la documentación de GitHub sobre runners macOS.

CocoaPods falla con errores de CDN: A veces el CDN de CocoaPods tiene problemas. Agrega reintentos o usa --repo-update:

- run: pod install --repo-update || pod install --repo-update

El build de Android es lento: Además del cache de Gradle, agrega estas propiedades al gradle.properties del proyecto:

org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true

Provisioning profile expira: Los provisioning profiles tienen un año de vigencia. Configura un recordatorio o usa fastlane match para automatizar la renovación.

Conclusión

Automatizar builds móviles con GitHub Actions elimina fricción del proceso de release y reduce errores humanos. El costo de los runners macOS es el principal inconveniente, pero compilar solo en tags mantiene el gasto controlado. Una vez configurado, el flujo se vuelve tan simple como git tag v1.3.0 && git push origin v1.3.0 — y en minutos tienes APK e IPA listos para distribuir.

GG
Giancarlos Gaona

Ingeniero de Software y Consultor Senior especializado en Liferay DXP, con más de 8 años de experiencia liderando proyectos enterprise para banca, retail, energía e instituciones públicas.