Le parcours du combattant App Store : la préparation
« Je croyais que le plus dur, c’était de coder l’application. »
Le binaire était prêt : je me suis dit que le gros du travail était fait.
Tous les développeurs se font cette illusion. Une seule fois.
Ce qui suit n’est pas du code : c’est de la paperasse, de l’empaquetage, et un pipeline qui s’est révélé aussi complexe qu’une petite application à part entière.
Ce qu’Apple exige vraiment
Soumettre sur le Mac App Store, ce n’est pas envoyer un zip : il faut des métadonnées, des captures, des fichiers de conformité, de la signature, de l’empaquetage, et un ou deux pièges qui n’apparaissent que quand le chrono tourne.
Le manifeste de confidentialité. Renala avait besoin d’un fichier PrivacyInfo.xcprivacy parce qu’elle touche à l’espace disque, aux horodatages de fichiers et aux user defaults.1 Même une application qui ne collecte aucune donnée utilisateur doit déclarer quelles API système elle appelle, et pourquoi.
Le fichier est un plist structuré (le format property-list d’Apple) : quelles données on collecte, quelles catégories d’API protégées on utilise, et quels codes de justification Apple les couvrent. Dans le cas de Renala, le manifeste était simple : aucune donnée collectée, aucun suivi, trois catégories d’API, trois codes.2
%%{init: {"theme": "default"}}%%
accTitle: Privacy manifest structure
accDescr: PrivacyInfo.xcprivacy declares three sections: Required Reason APIs (file access, user defaults) with Apple-defined reason codes, Collected Data Types (none collected), and Third-Party SDK Policies (StoreKit with no data collected).
graph TD
PRIV["PrivacyInfo.xcprivacy"] --> APIS["API à justification obligatoire<br/>(accès fichiers, user defaults)"]
PRIV --> DATA["Types de données collectées<br/>(aucune collecte)"]
PRIV --> SDK["Politiques SDK tiers<br/>(StoreKit : aucune donnée collectée)"]
APIS --> CODES["Codes de justification Apple<br/>ex. 3B52.1, E174.1, CA92.1"]
Les captures d’écran. Le Mac App Store en veut entre une et dix, et 2880x1800 fait partie des tailles Mac acceptées.3 J’en ai utilisé cinq. Sans écran 5K, impossible de les capturer nativement : j’ai utilisé un outil de composition à la bonne résolution. Chaque capture raconte un morceau de l’histoire : lancement d’un scan, treemap en détail, navigation dans la barre latérale, détection de doublons, parcours de nettoyage. Les captures d’écran, c’est un argumentaire commercial, pas de la documentation.
Les métadonnées. Description App Store, sous-titre, mots-clés, URL de politique de confidentialité, URL de support, catégorie, tranche d’âge. L’essentiel relève du formulaire. Le champ mots-clés, lui, non. Apple donne 100 octets, mieux vaut les dépenser sur des termes qui ne sont pas déjà couverts par le nom de l’application ou de l’éditeur.4
La boîte à pourboires
Renala est gratuite ; si quelqu’un veut payer, autant lui en donner la possibilité.
StoreKit 2 rend la chose simple si les produits sont des consommables.5 Les consommables sont le bon type de produit : un pourboire est un achat ponctuel, sans droit d’accès à suivre ni à restaurer. On les charge via Product.products(for:) avec un jeu d’identifiants produit. L’utilisateur voit les montants proposés, en choisit un, StoreKit gère la transaction. Ni serveur, ni restauration, ni état d’achat à resynchroniser ensuite.
L’implémentation tient en peu de lignes : une classe observable TipJarManager, une TipJarView qui charge les produits à l’affichage et les présente dans une liste. Sans abonnement, sans bouton de restauration, sans interface d’achat maison.6
Configurer les produits dans App Store Connect est une étape manuelle à part. Chaque produit a besoin d’un identifiant, d’un nom interne, d’un texte visible côté client et d’un palier de prix. Et chacun a aussi son propre état de validation.7 Retenez bien cette phrase : elle reviendra.
Voilà pour la configuration produit. Reste la question de l’expédition : comment livrer tout ça, de manière fiable, plus d’une fois.
Le pipeline CI/CD
Faire tout ça à la main une fois, ça passe ; le refaire après une correction de bug, c’est là que le processus manuel s’effondre.
Le pipeline se scinde en deux parce qu’on parle du même outil, mais pas du même canal de distribution. Hors store, il faut une signature Developer ID et une notarisation. Dans le store, il faut une signature App Store, un paquet installateur et un envoi à App Store Connect.8
graph LR
accTitle: CI/CD pipeline: direct distribution and App Store
accDescr: A git version tag triggers GitHub Actions. The left branch builds with Developer ID, notarizes, creates DMGs for both architectures, and publishes a GitHub Release. The right branch archives with Apple Distribution signing, packages as .pkg, uploads to App Store Connect, and enters App Store Review.
TAG[Tag de version Git] --> GH[GitHub Actions]
GH --> BUILD[xcodebuild build<br/>Developer ID]
BUILD --> NOTA[xcrun notarytool<br/>soumission + agrafage]
NOTA --> DMG[Création DMG (image disque)<br/>arm64 + x86_64]
DMG --> REL[GitHub Release]
GH --> ABUILD[xcodebuild archive<br/>Apple Distribution]
ABUILD --> PKG[productbuild .pkg]
PKG --> SUBMIT[xcrun altool<br/>téléversement]
SUBMIT --> REVIEW[Validation App Store]
La branche de gauche sert à la distribution directe : build signé Developer ID, notarisation, création des DMG, publication en tant que GitHub Release. La branche de droite vise le Mac App Store : archive signée pour le store, empaquetage en .pkg via productbuild (l’outil Apple de packaging installateur), téléversement vers App Store Connect avec xcrun altool.
En local, le Makefile expose les opérations de distribution directe : make dmg-arm64, make dmg-x86_64, make notarize. Le chemin de soumission App Store vit dans un workflow séparé, parce que les identités de signature et les règles d’empaquetage ne sont pas les mêmes.9
En zoomant sur la branche App Store :
%%{init: {"theme": "default"}}%%
accTitle: App Store submission workflow
accDescr: xcodebuild produces an archive signed for Apple Distribution. productbuild packages it as a .pkg with an Installer certificate. xcrun altool uploads it. App Store Review either approves (Ready for Sale) or rejects with a guideline violation and crash log.
graph LR
BUILD["xcodebuild archive<br/>Apple Distribution + profil"] --> PKG["productbuild .pkg<br/>certificat installateur"]
PKG --> UPLOAD["xcrun altool<br/>téléversement"]
UPLOAD --> REVIEW["Validation App Store"]
REVIEW --> APPROVED["Approuvé :<br/>Prêt à la vente"]
REVIEW --> REJECTED["Rejeté :<br/>Violation des directives<br/>+ journal de plantage"]
La notarisation en bref
Quand un utilisateur télécharge un build signé Developer ID et double-clique dessus, Gatekeeper vérifie qu’Apple l’a bien notarisé.10 La notarisation, ce n’est pas l’App Review. C’est le contrôle automatisé d’Apple contre le malware et les problèmes de signature pour les logiciels distribués hors du store.
Si la soumission passe, Apple renvoie un ticket. On l’agrafe au bundle de l’application, au DMG ou au paquet installateur, et Gatekeeper peut alors faire sa vérification même hors ligne.11
Le chemin Mac App Store est différent. Apple exécute des contrôles de sécurité équivalents pendant la soumission au store, donc il n’y a pas d’étape notarytool séparée dans ce pipeline.12
Les pièges
Deux pièges sur le chemin de l’empaquetage App Store.
Premièrement : xcodebuild -exportArchive sur le chemin App Store m’a surtout fait perdre du temps. Dans ma configuration, l’archive contenait déjà l’application correctement signée. productbuild --component <app> /Applications --sign "3rd Party Mac Developer Installer: ..." produisait le .pkg que voulait réellement App Store Connect, donc j’ai pris cette voie.13
Deuxièmement : l’authentification xcrun altool. Pour la commande de téléversement, la valeur dont j’avais besoin était mon identifiant Apple de connexion, pas l’identifiant numérique de l’application.14 Le message d’erreur quand on se trompe n’aide pas beaucoup. C’est le genre de détail qui coûte vingt minutes la première fois et zéro ensuite, donc personne ne le note, et donc ça coûte toujours vingt minutes la première fois.
Ce que le pipeline apporte
Une fois les workflows en place, un tag de version déclenche tout : builds Developer ID pour le téléchargement direct, empaquetage App Store pour la validation, et toute la colle pénible entre les deux.
C’est surtout précieux après un rejet. Corriger le problème, incrémenter la version, taguer, laisser le pipeline tourner. Faire confiance au pipeline, c’est savoir que le correctif a bien été livré, pas « je crois avoir suivi toutes les étapes ».
Ce pipeline évoqué en introduction, celui qui est aussi complexe qu’une petite application ? Il faut l’automatiser avant d’en avoir besoin. On en aura besoin.
Références
- Apple : Directives de validation App Store
- Apple : Fichiers de manifeste de confidentialité
- Apple : Notarisation des logiciels macOS avant distribution
- Apple : Achats intégrés avec StoreKit
- Apple : Spécifications des captures d’écran
- Apple : Informations de version par plateforme
- WWDC 2022 : Intégration et migration des achats intégrés
- WWDC 2023 : Découvrir StoreKit pour SwiftUI
Footnotes
-
Le manifeste du projet Renala déclare trois catégories d’API consultées : horodatages de fichiers, espace disque et user defaults. ↩
-
Les codes exacts du projet sont
3B52.1pour les horodatages de fichiers,E174.1pour l’espace disque etCA92.1pour les user defaults. ↩ -
La documentation actuelle d’App Store Connect autorise entre une et dix captures pour une app Mac, et 2880x1800 n’est qu’une taille 16:10 acceptée parmi d’autres. ↩
-
Apple documente une limite de 100 octets, pas de 100 caractères. La documentation recommande aussi d’éviter de répéter des termes déjà présents dans le nom de l’application ou de l’éditeur. ↩
-
Les produits de pourboire dans la configuration StoreKit de Renala sont des consommables. « Non récurrent » est vrai, mais trop flou pour être utile. ↩
-
StoreKit affiche toujours sa propre feuille d’achat. La partie utile ici, c’est l’absence d’interface d’achat personnalisée. ↩
-
En pratique, il faut un product ID, un nom de référence, du texte localisé côté client et un prix. Le détail qui compte pour l’histoire, c’est que chaque achat intégré a aussi son propre état de validation. ↩
-
Même application, règles différentes. La distribution Developer ID, c’est une relation directe entre vous, la notarisation Apple et l’utilisateur. La distribution Mac App Store passe par App Store Connect, la review et les règles de packaging du store. ↩
-
Dans le dépôt de l’application, le workflow de distribution directe et le workflow App Store sont séparés pour cette raison précise : certificats différents, empaquetage différent, destination finale différente. ↩
-
Gatekeeper est le contrôle de sécurité lancé par macOS quand un logiciel téléchargé essaie de démarrer. ↩
-
Plus précisément, le ticket s’agrafe à un bundle ou à un conteneur pris en charge. Le point pratique, c’est la vérification hors ligne, pas la surface exacte d’attache. ↩
-
Apple décrit la soumission Mac App Store comme incluant des contrôles de sécurité équivalents, ce qui explique l’absence d’étape de notarisation distincte dans ce pipeline. ↩
-
La terminologie Xcode actuelle préfère
app-store-connectà l’ancien nomapp-storepour la méthode d’export. Dans ce projet, le workflow saute cette étape d’export et empaquette directement l’application archivée. ↩ -
altoolaccepte aujourd’hui à la fois--usernameet--apple-id, mais ils ne désignent pas la même chose. Sur ce chemin de téléversement,--usernameétait la valeur pertinente. ↩
