Archivar y subir: la compilación de release, los dos números y App Store Connect
Entre el proyecto que compila en tu máquina y el binario que Apple acepta hay un artefacto intermedio con reglas propias: el archivo. Esta lección explica en qué se diferencia de una compilación normal y por qué aparecen fallos que en depuración no existían, desmonta la confusión permanente entre número de versión y número de compilación estableciendo qué contrato cumple cada uno, y recorre la subida como lo que es, una transacción irreversible con validaciones que conviene ejecutar antes y no después.
Archivar parece una orden de menú y es en realidad el momento en que tu proyecto se convierte por primera vez en el objeto que verán los usuarios: compilado con optimizaciones distintas, sin las aserciones que te protegían, con los recursos adelgazados por dispositivo y con los símbolos de depuración extraídos a un fichero aparte que necesitarás para entender los fallos que ocurran a miles de kilómetros. Casi todos los sustos de última hora —el bloqueo que solo pasa en producción, el número de compilación rechazado, el correo automático que aparece veinte minutos después de subir— son consecuencias de tratar esta etapa como un botón en lugar de como una fase con requisitos propios.
- Explicar qué hace un archivo que una compilación de depuración no hace, y qué clase de defectos destapa.
- Fijar una política para el número de versión y el de compilación acorde con el contrato que cumple cada uno.
- Exportar y validar el paquete antes de subirlo, en lugar de descubrir los errores por correo.
- Entender el procesamiento posterior a la subida y por qué una compilación subida es inmutable.
El archivo es otro binario, no el mismo con otro nombre
La configuración de lanzamiento cambia lo suficiente como para que el código se comporte distinto. El optimizador reordena y elimina, las aserciones desaparecen, las comprobaciones que dependen de la bandera de depuración dejan de ejecutarse, la optimización de módulo completo permite decisiones que atraviesan ficheros y el enlazador retira lo que cree muerto. Un fallo que solo aparece en lanzamiento casi nunca es un defecto del optimizador: es una dependencia oculta del comportamiento de depuración, casi siempre un valor no inicializado, una carrera que la lentitud de la depuración escondía o un objeto que sobrevivía por accidente.
El resultado es un paquete que contiene la aplicación, los ficheros de símbolos y los metadatos de distribución. Los símbolos merecen atención: sin ellos, un informe de fallo es una lista de direcciones sin significado, y como se generan en cada compilación y no se pueden reconstruir después, perderlos equivale a renunciar a diagnosticar esa versión para siempre.
# Archivar de forma reproducible, sin depender del estado de la interfaz.
xcodebuild archive \
-scheme MiApp \
-configuration Release \
-destination 'generic/platform=iOS' \
-archivePath build/MiApp.xcarchive
# Exportar el paquete distribuible segun las opciones declaradas.
xcodebuild -exportArchive \
-archivePath build/MiApp.xcarchive \
-exportOptionsPlist ExportOptions.plist \
-exportPath build/export
El fichero de opciones merece verse por dentro, porque es donde se toman decisiones que después resultan difíciles de rastrear: el método de distribución, si se suben los símbolos, si se permite el adelgazamiento por dispositivo y con qué identidad se firma.
# ExportOptions.plist en forma legible: cada clave es una decision de distribucion.
plutil -p ExportOptions.plist
# Comprobar que los simbolos existen antes de necesitarlos.
ls build/MiApp.xcarchive/dSYMs
dwarfdump --uuid build/MiApp.xcarchive/dSYMs/MiApp.app.dSYM
El identificador único que imprime ese último comando es el que enlaza un informe de fallo con el fichero de símbolos correcto. Cuando llega un informe ilegible, la pregunta no es si guardaste los símbolos sino si guardaste los de esa compilación exacta, y ese identificador es la única forma de responderla sin conjeturas.
El destino genérico no es un detalle cosmético: archivar contra un simulador produce un artefacto que jamás podrá distribuirse, y es la causa de que la opción de distribución aparezca deshabilitada sin explicación. El fichero de opciones de exportación, por su parte, es el que decide el método de distribución, si se sube automáticamente y si se incluyen los símbolos, y tenerlo bajo control de versiones convierte una secuencia de diálogos en una decisión revisable.
Si la primera vez que ejecutas la configuración de lanzamiento es la víspera de publicar, habrás concentrado todos los defectos que esa configuración destapa en el peor momento posible. Ejecutar la suite de pruebas también en lanzamiento, aunque sea una vez al día, reparte ese descubrimiento a lo largo del ciclo y elimina la categoría entera de sorpresas de última hora.
Dos números con dos contratos distintos
La confusión entre ambos números es universal y se disuelve en cuanto se identifica a quién responde cada uno. La cadena de versión corta es para el usuario: aparece en la ficha de la tienda, describe un cambio funcional y su progresión es una promesa de significado. El número de compilación es para el sistema: identifica un artefacto concreto, debe crecer y su único deber es ser único dentro de su tren de versión.
Versión corta
Lo que el usuario ve, del tipo 2.4.1. Debe aumentar en cada publicación en la tienda y no puede repetirse en un tren ya publicado.
Número de compilación
Identifica el artefacto. Debe crecer dentro de la misma versión corta y puede reiniciarse al cambiar de versión, aunque no reiniciarlo nunca es más seguro.
Derivado, no escrito a mano
Un contador de la integración continua o el número de commits produce un valor único sin intervención humana y elimina la colisión más común.
Trazable al código
Guardar en algún sitio la correspondencia entre número de compilación y revisión exacta es lo que permite, meses después, saber qué código produjo un fallo concreto.
# Fijar ambos numeros desde la linea de comandos, sin tocar el proyecto a mano.
xcodebuild archive -scheme MiApp \
MARKETING_VERSION=2.4.1 \
CURRENT_PROJECT_VERSION=$CI_BUILD_NUMBER \
-archivePath build/MiApp.xcarchive
La regla que más disgustos evita se enuncia así: una compilación subida es inmutable. No se puede reemplazar el contenido de un número ya aceptado, ni reutilizarlo después de eliminar la compilación. Si detectas un defecto diez minutos después de subir, la única salida es subir otra compilación con un número mayor. Por eso derivar el número de un contador automático no es una comodidad sino una defensa: el error humano que repite un número obliga a repetir todo el proceso, y siempre ocurre con prisa.
Subir es una transacción: valida antes
La subida transfiere el paquete y arranca un procesamiento asíncrono en el que se comprueban firma, iconos, arquitecturas, cadenas de propósito de los permisos y declaraciones de privacidad. Ese procesamiento tarda entre unos minutos y bastante más, y sus errores llegan por correo, lo que produce la peor de las combinaciones: un fallo trivial descubierto tarde. La validación previa ejecuta buena parte de esas comprobaciones en tu máquina y cuesta un minuto.
# Validar contra los servidores antes de comprometer la subida.
xcrun altool --validate-app \
-f build/export/MiApp.ipa -t ios \
--apiKey "$CLAVE_API" --apiIssuer "$EMISOR_API"
# Subir con clave de la API, sin sesion interactiva ni contrasenas de persona.
xcrun altool --upload-app \
-f build/export/MiApp.ipa -t ios \
--apiKey "$CLAVE_API" --apiIssuer "$EMISOR_API"
flowchart TB a[Archivar en configuracion de lanzamiento] --> b[Exportar el paquete distribuible] b --> c[Validar contra los servidores] c --> d[Subir con clave de la API] d --> e[Procesamiento asincrono en la tienda] e --> f[Comprobacion de firma y arquitecturas] e --> g[Cadenas de proposito de los permisos] e --> h[Declaracion de privacidad y APIs de motivo obligado] f --> ok[Compilacion disponible] g --> ok h --> ok e --> err[Rechazo automatico por correo] err --> n[Subir una compilacion nueva con numero mayor]
Conviene además saber que lo que subes no es lo que se descarga. La tienda adelgaza el paquete por dispositivo, de modo que cada usuario recibe solo la arquitectura y los recursos gráficos que le corresponden. Esto tiene dos implicaciones prácticas: el tamaño que ves al exportar no es el que verá el usuario, y por tanto discutir sobre el peso de la app mirando el paquete completo lleva a conclusiones equivocadas; y los catálogos de recursos no son una comodidad organizativa sino el mecanismo que habilita ese adelgazamiento, razón por la cual las imágenes sueltas fuera de un catálogo viajan a todos los dispositivos aunque solo sirvan para uno.
Dos familias de rechazo automático concentran la mayoría de los casos. La primera son las cadenas de propósito ausentes: cualquier permiso que la app pueda solicitar necesita una explicación declarada, y basta con que una biblioteca de terceros enlace una capacidad para que el requisito aparezca aunque tu código no la use. La segunda, más reciente y peor entendida, es la declaración de privacidad: la app y sus dependencias deben declarar los datos que recogen y justificar el uso de ciertas interfaces del sistema con un motivo de una lista cerrada. Ninguna de las dos se detecta compilando, y ambas se detectan validando.
La declaración de privacidad tiene además una propiedad que descoloca a los equipos la primera vez: es compuesta. No basta con la tuya, porque cada dependencia binaria debe aportar la suya y firmarla, y el resultado que evalúa la tienda es la unión de todas. Eso convierte una actualización rutinaria de una biblioteca de terceros en un cambio con consecuencias regulatorias, y explica por qué conviene revisar ese apartado cada vez que se toca el fichero de dependencias y no solo cuando se toca el código propio.
# Localizar las declaraciones de privacidad incluidas en el paquete final.
find build/export -name "PrivacyInfo.xcprivacy"
Dos trámites pequeños que atascan entregas. Si no subes los ficheros de símbolos, los informes de fallo llegarán ilegibles y no hay forma de repararlo a posteriori para esa versión. Y la pregunta sobre uso de cifrado bloquea la compilación hasta que alguien la responde a mano, salvo que declares la respuesta en el Info.plist de una vez y para siempre.
El cambio de mentalidad que separa a quien publica con ansiedad de quien publica con rutina consiste en dejar de ver la subida como el final de un proceso creativo y verla como el acta de un hecho. Una vez transferido, ese paquete es la única definición de lo que los usuarios ejecutan: no lo es tu rama, ni tu etiqueta de git, ni lo que recuerdas haber compilado. De esa observación se derivan casi todas las prácticas que parecen ceremonia y no lo son. El número de compilación debe ser único e irrepetible porque es el identificador primario de un hecho ocurrido, y renumerar hechos es corromper el registro. Los símbolos deben archivarse junto al artefacto porque son la clave de lectura de ese registro y se destruyen en cuanto la máquina que compiló haga limpieza. La compilación debe ser reproducible desde una revisión concreta porque, cuando llegue un informe de fallo dentro de cinco meses, la pregunta no será qué hace el código hoy sino qué hacía exactamente el binario que falló. Y por eso archivar desde un portátil con cambios sin confirmar no es una informalidad simpática: es publicar un hecho que ya nadie podrá reconstruir. La disciplina aquí no protege la compilación de hoy, protege tu capacidad futura de explicar lo que pasó.
- Archiva tu app desde la línea de comandos con destino genérico y exporta el paquete con un fichero de opciones guardado en el repositorio.
- Ejecuta la suite de pruebas en configuración de lanzamiento y anota cuántos fallos aparecen que no existían en depuración.
- Sustituye los dos números escritos a mano por ajustes de compilación alimentados desde variables, y deriva el de compilación de un contador automático.
- Valida el paquete antes de subir y provoca a propósito un fallo de cadena de propósito para ver el mensaje real que produce.
- Guarda el archivo completo con sus símbolos en un almacén duradero y escribe cómo recuperar, a partir de un número de compilación, la revisión exacta que lo generó.