macOSFastlaneCertificadosApp Store ConnectGitHub

CI/CD para macOS con Fastlane (parte 2): setup, certificados y credenciales

En la parte 1 puse el mapa: firmar, notarizar, adjuntar el ticket, distribuir. Antes de tocar Fastlane hay que preparar las credenciales que orquesta el pipeline: el certificado que firma, la API Key que autentica ante Apple para notarizar, y el token que publica en GitHub Releases. Este post junta las cuatro piezas y las deja listas para que las lanes las consuman.

Todo lo que sigue vive solo en mi Mac. Nada de esto se sube al repo. Lo único que sí queda en git es un .env.example con placeholders, para que quien clone el repo sepa qué necesita.

Requisitos previos

  • Cuenta de Apple Developer activa. Cuesta USD $99 al año y sin ella no hay Developer ID.
  • Xcode reciente (yo uso Xcode 26). No es Fastlane, es porque xcodebuild y notarytool viven dentro de Xcode.
  • Ruby 3.x y Bundler para correr Fastlane. Si tienes rbenv o el Ruby del sistema, cualquiera sirve.

En el repo pokedex-macos los dos primeros ya son visibles en el project.yml (deployment target macOS 14, Swift 6) y en el Gemfile (gem "fastlane", "~> 2.230").

El certificado — Developer ID Application vs Mac App Distribution

Este es el punto donde más gente se traba, así que va con detalle.

Apple emite varios tipos de certificados para desarrolladores de macOS. Los dos que se confunden son:

  • Mac App Distribution — firma apps que se van a subir al Mac App Store. Solo sirve para eso. Si intentas notarizar una .app firmada con este certificado, Apple te la rechaza.
  • Developer ID Application — firma apps que se distribuyen fuera del Mac App Store. Es el que necesitamos.

Se generan desde el mismo lugar en developer.apple.com → Certificates, pero son certificados distintos, con roles distintos, y no se sustituyen entre sí. También hay un tercero, Developer ID Installer, que sirve para firmar paquetes .pkg cuando distribuyes con un instalador en vez de un DMG. En esta serie el flujo es DMG con la .app adentro, así que no lo tocamos.

Para generar el Developer ID Application:

  1. En el portal de Apple Developer → Certificates → botón +.
  2. Bajo la sección Software, elige Developer ID Application.
  3. Sube un Certificate Signing Request (CSR) generado con Keychain Access. Apple documenta el paso a paso en Create a certificate signing request.
  4. Descarga el .cer que Apple genera, doble clic para importarlo al Keychain.
  5. En Keychain Access, exporta el par (certificado + private key) como un .p12 con contraseña. Guárdalo fuera del repo. Ese .p12 es tu backup.

Con eso en el Keychain, Xcode y Fastlane ya pueden firmar. En el project.yml del repo el modo de firma está en automático:

settings:
  base:
    CODE_SIGN_STYLE: Automatic

Con estilo automático, Xcode busca en el Keychain el certificado que corresponde al export_method: "developer-id" que pasa Fastlane y firma solo.

App Store Connect API Key para notarizar

Notarizar exige autenticarse contra Apple. Hay tres formas, en orden de peor a mejor:

  1. Apple ID + contraseña. Ya no se recomienda.
  2. Apple ID + app-specific password. Funciona pero expira y hay que renovarla.
  3. API Key de App Store Connect. Es un archivo .p8 que no expira mientras no lo revoques, y es lo que usa el Fastfile del repo.

Se genera desde App Store Connect → Users and Access → Integrations → Keys:

  1. Click en + para generar una nueva.
  2. Nombre descriptivo (p. ej. “notarize-local”).
  3. Rol: Developer es suficiente para notarizar. No hace falta Admin.
  4. Descarga el .p8. Apple solo permite descargarlo una vez —si lo pierdes tienes que revocar y regenerar. Guárdalo en un lugar fuera del repo. Yo uso ~/Documents/asc-keys/.

Además del archivo, App Store Connect te muestra dos identificadores:

  • Key ID — 10 caracteres, visible en la lista de keys.
  • Issuer ID — UUID que es el mismo para toda tu cuenta.

Estos tres —.p8, Key ID e Issuer ID— son los que consume notarytool. Fastlane los envuelve. En el Fastfile del repo la línea relevante es esta:

notarize(
  package:      app_path,
  bundle_id:    BUNDLE_ID,
  api_key_path: ENV["APP_STORE_CONNECT_API_KEY_PATH"],
  print_log:    true,
  verbose:      true
)

Fastlane lee el Key ID e Issuer ID de dentro del propio .p8 si el archivo tiene el nombre canónico AuthKey_<KEY_ID>.p8, que es como Apple lo entrega por defecto. Nada de hardcodear IDs.

GitHub Personal Access Token

El último paso del pipeline sube el DMG a GitHub Releases. Para eso hace falta un token con permiso de escritura en el repo.

Dos opciones:

  • PAT classic con scope repo. Simple, funciona en cualquier repo tuyo.
  • PAT fine-grained limitado al repo pokedex-macos, con Contents: Read and write. Más seguro, es el que uso.

Se generan en github.com/settings/tokens. Cópialo apenas lo generes —GitHub tampoco lo muestra dos veces— y lo guardas para meterlo al .env.

Cómo se acomoda todo en el .env

Fastlane carga automáticamente fastlane/.env si existe. Ese archivo está en .gitignore y nunca se sube. La plantilla .env.example sí, con placeholders:

APPLE_ID=you@example.com
APPLE_TEAM_ID=ABCDE12345
APP_STORE_CONNECT_API_KEY_PATH=/Users/tu-usuario/Documents/asc-keys/AuthKey_XXXXXXXXXX.p8
GITHUB_TOKEN=ghp_your_token_here

Cuatro variables. APPLE_ID la lee el Appfile para logging, APPLE_TEAM_ID para identificar el equipo al que pertenece el certificado, APP_STORE_CONNECT_API_KEY_PATH la usa el paso de notarización, y GITHUB_TOKEN la usa set_github_release.

El Team ID de 10 caracteres está en developer.apple.com/account bajo Membership.

El .gitignore que protege todo

Antes de commitear cualquier cosa, este es el bloque del .gitignore que evita accidentes:

# Secrets — NUNCA commitear
fastlane/.env
fastlane/.env.default
fastlane/AuthKey_*.p8
*.p12
*.mobileprovision

.p12 es el certificado exportado con private key, .p8 es la API Key de ASC, .mobileprovision no aplica a Developer ID puro pero lo dejo por si algún día se necesita provisioning explícito.

Verificación rápida

Con las cuatro variables puestas y bundle install corrido, un chequeo rápido:

bundle exec fastlane lanes

Debe listar las dos lanes disponibles:

------ mac ------
mac qa      Build QA firmado con Developer ID (sin notarizar). ...
mac release Release oficial: build → notarizar → staple → GitHub Release

Si eso funciona, el Fastfile parsea, el Ruby carga y las variables de entorno están al alcance. Todavía no hemos disparado nada real —eso llega en las partes 4 y 5.

Qué sigue

En la parte 3 abrimos la caja negra de la notarización: qué firma exactamente codesign, qué es el hardened runtime, qué entitlements exige Developer ID, cómo funciona notarytool con la API Key que acabamos de generar, y por qué el staple no es opcional.