Introduction
Faites-vous encore Archive → Distribute... manuellement depuis Xcode à chaque release de votre app iOS ?
Ce processus est chronophage et tend à créer des dépendances environnementales — le fameux problème : "ça build sur mon Mac, mais pas sur le sien."
Dans cet article, nous allons construire un pipeline complet pour compiler, signer et uploader automatiquement une app iOS vers TestFlight avec GitHub Actions.
Beaucoup de tutoriels utilisent fastlane, mais nous adoptons ici une approche différente : utiliser uniquement xcodebuild et xcrun altool, les outils natifs d'Apple — sans fastlane. Inutile d'introduire un environnement Ruby, le workflow reste simple, et on échappe aux problèmes de compatibilité fastlane lors des mises à jour de Xcode.
Pour l'authentification, nous utilisons une clé API App Store Connect — plus sécurisée et non affectée par la double authentification — plutôt qu'un identifiant Apple ID + mot de passe.
Prérequis
- Inscription au Apple Developer Program (payant)
- Enregistrement de l'app dans App Store Connect
- Projet iOS poussé dans un dépôt GitHub
- Builds locaux fonctionnels sous macOS / Xcode confirmés
Vue d'ensemble
Le pipeline que nous allons construire fonctionne ainsi :
- Déclenchement du workflow par un push sur
mainou manuellement - Préparation de Xcode sur le runner macOS de GitHub Actions
- Import temporaire du certificat et du profil de provisionnement dans le trousseau
- Mise à jour du numéro de build
- Génération du
.xcarchiveavecxcodebuild archive - Export en
.ipaavecxcodebuild -exportArchive - Upload vers TestFlight avec
xcrun altool - Suppression du trousseau temporaire et nettoyage
push → Actions déclenché → Installation certificat → archive → export → altool → TestFlight
Étape 1 : Préparer les certificats et profil de provisionnement
Pour effectuer la signature de code en CI, encodez les deux fichiers suivants en Base64 et stockez-les dans GitHub Secrets.
1-1. Exporter le certificat de distribution (.p12)
Ouvrez "Trousseaux d'accès.app" sur votre Mac, faites un clic droit sur le certificat Apple Distribution ou iPhone Distribution et exportez-le au format .p12. Notez le mot de passe défini lors de l'export — vous en aurez besoin plus tard.
Si vous n'avez pas de certificat, créez-en un depuis la section Certificates du site Apple Developer.
1-2. Télécharger le profil de provisionnement (.mobileprovision)
Téléchargez le profil de provisionnement pour la distribution App Store depuis la section Profiles du site Apple Developer. Notez également le nom du profil (ex. : YourApp AppStore) — vous devrez l'indiquer dans ExportOptions.plist.
1-3. Encoder en Base64
Exécutez les commandes suivantes dans votre terminal pour convertir les fichiers en chaînes Base64.
# Certificat
base64 -i Certificates.p12 | pbcopy
# Profil de provisionnement
base64 -i YourApp_AppStore.mobileprovision | pbcopy
Le contenu est copié dans le presse-papiers via pbcopy et peut être collé directement dans GitHub Secrets.
Étape 2 : Générer une clé API App Store Connect
Pour éviter les complications liées à la double authentification, nous utilisons une clé API.
- Se connecter à App Store Connect
- Sélectionner "Utilisateurs et accès" → "Integrations" → "App Store Connect API"
- Générer une clé avec le bouton "+" (rôle App Manager ou supérieur requis)
- Sauvegarder le fichier
AuthKey_XXXXXXXXXX.p8téléchargé (téléchargement unique — à conserver précieusement) - Noter la Key ID et l'Issuer ID affichés lors de la création
Encoder également le fichier .p8 en Base64 :
base64 -i AuthKey_XXXXXXXXXX.p8 | pbcopy
Étape 3 : Enregistrer dans GitHub Secrets
Dans Settings → Secrets and variables → Actions du dépôt, enregistrez les Secrets suivants :
| Nom du Secret | Contenu |
|---|---|
BUILD_CERTIFICATE_BASE64 |
Chaîne Base64 du certificat .p12 |
P12_PASSWORD |
Mot de passe défini lors de l'export .p12 |
BUILD_PROVISION_PROFILE_BASE64 |
Chaîne Base64 du .mobileprovision |
KEYCHAIN_PASSWORD |
Mot de passe quelconque pour le trousseau temporaire |
APP_STORE_CONNECT_API_KEY_ID |
Key ID de la clé API |
APP_STORE_CONNECT_API_ISSUER_ID |
Issuer ID |
APP_STORE_CONNECT_API_KEY_BASE64 |
Chaîne Base64 du .p8 |
Étape 4 : Créer ExportOptions.plist
Ce fichier de configuration est nécessaire pour générer un .ipa à partir d'un .xcarchive. Commitez-le dans votre dépôt sous ios/ExportOptions.plist (aucune information sensible).
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>app-store</string>
<key>teamID</key>
<string>ABCD123456</string>
<key>signingStyle</key>
<string>manual</string>
<key>signingCertificate</key>
<string>Apple Distribution</string>
<key>provisioningProfiles</key>
<dict>
<key>com.example.yourapp</key>
<string>YourApp AppStore</string>
</dict>
<key>uploadSymbols</key>
<true/>
<key>uploadBitcode</key>
<false/>
</dict>
</plist>
Étape 5 : Créer le workflow GitHub Actions
Voici la partie principale. Créez .github/workflows/ios-testflight.yml.
name: iOS TestFlight Deploy
on:
push:
branches: [main]
workflow_dispatch:
env:
SCHEME: YourApp
WORKSPACE: YourApp.xcworkspace # Utiliser PROJECT pour .xcodeproj
CONFIGURATION: Release
EXPORT_OPTIONS_PLIST: ios/ExportOptions.plist
jobs:
deploy:
runs-on: macos-14
timeout-minutes: 60
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode_15.4.app
- name: Install the Apple certificate and provisioning profile
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
P12_PASSWORD: ${{ secrets.P12_PASSWORD }}
BUILD_PROVISION_PROFILE_BASE64: ${{ secrets.BUILD_PROVISION_PROFILE_BASE64 }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# Décoder le Base64
echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
echo -n "$BUILD_PROVISION_PROFILE_BASE64" | base64 --decode -o $PP_PATH
# Créer un trousseau temporaire
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# Importer le certificat
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" \
-A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: \
-k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
# Placer le profil de provisionnement
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
- name: Set build number
run: |
# Utiliser le numéro d'exécution GitHub Actions comme numéro de build (unicité garantie)
BUILD_NUMBER=${{ github.run_number }}
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_NUMBER" \
YourApp/Info.plist
echo "Build number set to $BUILD_NUMBER"
- name: Build archive
run: |
xcodebuild archive \
-workspace "$WORKSPACE" \
-scheme "$SCHEME" \
-configuration "$CONFIGURATION" \
-archivePath "$RUNNER_TEMP/YourApp.xcarchive" \
-destination "generic/platform=iOS" \
-allowProvisioningUpdates \
CODE_SIGNING_ALLOWED=YES \
| xcbeautify
- name: Export IPA
run: |
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/YourApp.xcarchive" \
-exportOptionsPlist "$EXPORT_OPTIONS_PLIST" \
-exportPath "$RUNNER_TEMP/export" \
| xcbeautify
- name: Prepare API key for altool
env:
APP_STORE_CONNECT_API_KEY_BASE64: ${{ secrets.APP_STORE_CONNECT_API_KEY_BASE64 }}
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
run: |
# altool lit depuis ~/.appstoreconnect/private_keys/ ou ./private_keys/
mkdir -p ~/.appstoreconnect/private_keys
echo -n "$APP_STORE_CONNECT_API_KEY_BASE64" | base64 --decode \
-o ~/.appstoreconnect/private_keys/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8
- name: Upload to TestFlight
env:
APP_STORE_CONNECT_API_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}
APP_STORE_CONNECT_API_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_ISSUER_ID }}
run: |
xcrun altool --upload-app \
--type ios \
--file "$RUNNER_TEMP/export/YourApp.ipa" \
--apiKey "$APP_STORE_CONNECT_API_KEY_ID" \
--apiIssuer "$APP_STORE_CONNECT_API_ISSUER_ID"
- name: Clean up keychain and profiles
if: always()
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true
rm -f ~/Library/MobileDevice/Provisioning\ Profiles/build_pp.mobileprovision
rm -rf ~/.appstoreconnect/private_keys
Points clés
Utiliser github.run_number comme numéro de build
Plutôt que d'utiliser increment_build_number de fastlane, on exploite le numéro d'exécution attribué automatiquement par GitHub Actions. Ce numéro est garanti croissant et satisfait l'exigence de TestFlight de "pas de numéro de build en doublon". Un numéro basé sur la date ($(date +%Y%m%d%H%M)) fonctionne aussi.
Formater les logs avec xcbeautify
Les logs bruts de xcodebuild sont très difficiles à lire ; on les redirige donc vers xcbeautify. Il est préinstallé sur les runners macos-14. S'il n'est pas disponible, ajoutez brew install xcbeautify comme étape.
Emplacement du fichier de clé API
xcrun altool détecte automatiquement la clé API depuis l'un des chemins suivants. Aucun flag ne permet de spécifier le chemin explicitement, donc la fichier doit être placé dans un répertoire désigné :
./private_keys/AuthKey_<KEY_ID>.p8~/private_keys/AuthKey_<KEY_ID>.p8~/.private_keys/AuthKey_<KEY_ID>.p8~/.appstoreconnect/private_keys/AuthKey_<KEY_ID>.p8
Pourquoi utiliser un trousseau temporaire
Importer des certificats dans le trousseau par défaut de l'environnement CI est difficile à nettoyer et pose des problèmes de sécurité. La meilleure pratique est de créer un trousseau par job et de le supprimer après.
Nettoyage avec if: always()
Le trousseau temporaire et les fichiers de clés sont toujours supprimés, même en cas d'échec du build. C'est particulièrement important avec les runners self-hosted.
Étape 6 : Vérification
Poussez sur la branche main ou exécutez manuellement depuis l'onglet Actions avec Run workflow.
L'opération se termine en 5 à 10 minutes, et un nouveau build apparaît dans l'onglet TestFlight d'App Store Connect. Le passage du statut "En traitement" (Processing) à "Terminé" peut prendre 10 à 30 minutes supplémentaires.
Options de gestion des certificats et configurations recommandées
Nous avons construit le pipeline en encodant .p12 en Base64 et en le stockant dans GitHub Secrets, mais GitHub Actions offre d'autres options. Choisissez celle qui convient le mieux à la taille et aux besoins de votre équipe.
Option 1 : Stockage en Base64 dans Secrets (méthode de cet article)
Principe : .p12 et .mobileprovision sont encodés en Base64 et stockés en tant que chaînes dans GitHub Secrets.
Avantages
- Aucune dépendance externe ; tout fonctionne dans GitHub
- Configuration initiale la plus simple
.p12fait généralement quelques Ko à quelques dizaines de Ko, bien en dessous de la limite de 64 Ko des Secrets
Inconvénients
- Mise à jour manuelle des Secrets lors de la rotation des certificats
- La gestion de plusieurs apps multiplie les Secrets et devient complexe
- Les Secrets ne peuvent pas être manipulés comme des fichiers (contrairement à Azure DevOps Secure Files)
Idéal pour : développeurs solo, petites équipes de 1 à 3 personnes, app unique
Option 2 : Committer des fichiers chiffrés dans le dépôt
Principe : .p12 est chiffré avec GPG ou OpenSSL et commité dans le dépôt ; seule la passphrase de déchiffrement est stockée dans Secrets.
# Chiffrer localement
gpg --symmetric --cipher-algo AES256 Certificates.p12
# → Commiter Certificates.p12.gpg dans le dépôt
Dans le workflow :
- name: Decrypt certificate
run: |
gpg --quiet --batch --yes --decrypt \
--passphrase="$CERT_PASSPHRASE" \
--output $RUNNER_TEMP/cert.p12 \
secrets/Certificates.p12.gpg
env:
CERT_PASSPHRASE: ${{ secrets.CERT_PASSPHRASE }}
Avantages
- Gestion en tant que fichier ; historique des rotations traçable via Git
- Pas de limite de taille des Secrets
- Approche la plus proche d'Azure DevOps Secure Files
Inconvénients
- En cas de compromission du dépôt, la force de la passphrase est la dernière ligne de défense
- La présence de fichiers
.gpgdans le dépôt peut déranger certains
Idéal pour : migration depuis Azure DevOps, gestion sous forme de fichiers souhaitée, dépôts privés
Option 3 : fastlane match
Principe : Les certificats chiffrés sont stockés dans un dépôt privé dédié (ou S3/GCS), et fastlane se charge automatiquement de les récupérer, installer et déchiffrer.
Avantages
- Gestion centralisée des certificats pour plusieurs apps et environnements (dev/adhoc/appstore)
- Les nouveaux membres configurent leur environnement local avec un seul
fastlane match - Régénération automatique des certificats supportée
Inconvénients
- Ruby et fastlane requis
- Risques de problèmes de compatibilité fastlane lors des mises à jour Xcode
- Va à l'encontre de l'approche sans fastlane de cet article
Idéal pour : gestion de plusieurs apps, équipes iOS de 5 personnes ou plus, rotation fréquente des certificats
Option 4 : AWS Parameter Store + Intégration OIDC
Principe : Les chaînes Base64 de .p12 et les mots de passe sont stockés en tant que SecureString dans AWS Systems Manager Parameter Store et récupérés depuis GitHub Actions via OIDC. L'absence de credentials à long terme est un avantage majeur.
Parameter Store vs. Secrets Manager
AWS propose deux services similaires ; voici une comparaison :
| Critère | Parameter Store | Secrets Manager |
|---|---|---|
| Coût (Standard) | Gratuit | 0,40 $/secret/mois + frais API |
| Coût (Advanced) | 0,05 $/10 000 appels API | Idem |
| Limite de taille | Standard 4 Ko / Advanced 8 Ko | 64 Ko |
| Rotation automatique | Non | Oui |
| Chiffrement KMS | Via le type SecureString | Natif |
Un .p12 encodé en Base64 fait généralement quelques dizaines de Ko à 30 Ko, ce qui est le point de décision. S'il tient en 8 Ko, Parameter Store est nettement moins cher ; sinon, Secrets Manager ou l'approche hybride S3 ci-dessous est plus appropriée.
Gestion des fichiers dépassant 8 Ko
Si la taille est trop grande, une configuration hybride consistant à stocker le .p12 chiffré dans S3 et à ne conserver que la clé S3 et le mot de passe de déchiffrement dans Parameter Store est pratiquement propre. Elle combine la simplicité de Parameter Store et la liberté de taille de S3.
Configuration côté AWS
Créez d'abord un fournisseur d'identité OIDC pour GitHub Actions dans IAM (ignorez si déjà créé) :
- Provider URL :
https://token.actions.githubusercontent.com - Audience :
sts.amazonaws.com
Stockez ensuite les paramètres dans Parameter Store (exemple AWS CLI) :
# Corps du certificat
aws ssm put-parameter \
--name /ios/yourapp/dist-cert-base64 \
--type SecureString \
--value "$(base64 -i Certificates.p12)"
# Mot de passe .p12
aws ssm put-parameter \
--name /ios/yourapp/p12-password \
--type SecureString \
--value "your-p12-password"
# Profil de provisionnement
aws ssm put-parameter \
--name /ios/yourapp/provisioning-profile-base64 \
--type SecureString \
--value "$(base64 -i YourApp_AppStore.mobileprovision)"
# Clé API App Store Connect (.p8)
aws ssm put-parameter \
--name /ios/yourapp/asc-api-key-base64 \
--type SecureString \
--value "$(base64 -i AuthKey_XXXXXXXXXX.p8)"
Créez un rôle IAM pour GitHub Actions et restreignez l'Assume Role à des dépôts et branches spécifiques dans la Trust Policy :
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:your-org/your-repo:ref:refs/heads/main"
}
}
}
]
}
Politique de permissions minimales — uniquement GetParameter sur le chemin cible :
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["ssm:GetParameter", "ssm:GetParameters"],
"Resource": "arn:aws:ssm:ap-northeast-1:123456789012:parameter/ios/yourapp/*"
},
{
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:ap-northeast-1:123456789012:key/your-key-id"
}
]
}
Workflow GitHub Actions
N'oubliez pas de définir id-token: write dans le bloc permissions. Sans cela, le token OIDC ne peut pas être récupéré.
permissions:
id-token: write # Requis pour l'acquisition du token OIDC
contents: read
jobs:
deploy:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-ios-deploy
aws-region: ap-northeast-1
- name: Fetch secrets from Parameter Store
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# Récupérer depuis Parameter Store (SecureString nécessite --with-decryption)
aws ssm get-parameter \
--name /ios/yourapp/dist-cert-base64 \
--with-decryption \
--query Parameter.Value --output text | base64 --decode > $CERTIFICATE_PATH
aws ssm get-parameter \
--name /ios/yourapp/provisioning-profile-base64 \
--with-decryption \
--query Parameter.Value --output text | base64 --decode > $PP_PATH
P12_PASSWORD=$(aws ssm get-parameter \
--name /ios/yourapp/p12-password \
--with-decryption \
--query Parameter.Value --output text)
# Créer le trousseau et importer le certificat
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" \
-A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: \
-k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
# Les étapes archive / export / altool suivantes sont identiques à l'Étape 5
Avantages
- Coût quasi nul : les paramètres standard sont entièrement gratuits ; Advanced est extrêmement bon marché
- Pas de credentials à long terme grâce à OIDC : inutile de stocker des clés d'accès dans GitHub (avantage majeur)
- Logs d'audit avec CloudTrail : enregistrement complet de qui a accédé au certificat et quand
- Contrôle des permissions granulaire avec IAM/KMS : rôles séparés pour les environnements dev et prod
- Partage entre plusieurs CI/CD : même certificat utilisable depuis GitHub Actions, CodeBuild, Jenkins et le développement local
- Historique des versions : rollback vers des versions précédentes possible
Inconvénients
- Compte AWS requis (coût initial élevé pour les équipes ne l'utilisant pas encore)
- Workaround nécessaire si
.p12dépasse 8 Ko (utiliser S3 en complément ou Secrets Manager) - Courbe d'apprentissage pour la configuration OIDC (une seule fois)
Idéal pour : équipes utilisant AWS, exigences de logs d'audit, référencement du même certificat depuis plusieurs outils CI/CD, management de qualité entreprise à faible coût
Option 5 : AWS Secrets Manager + Intégration OIDC
Principe : .p12 est stocké directement en binaire (SecretBinary) dans AWS Secrets Manager et récupéré depuis GitHub Actions via OIDC. Contrairement à Parameter Store, la caractéristique principale est de pouvoir manipuler le fichier directement sans se soucier de la conversion Base64.
Différences avec Parameter Store
| Critère | Parameter Store | Secrets Manager |
|---|---|---|
| Format de stockage | Chaînes uniquement (binaire nécessite Base64) | Binaire directement OK (type SecretBinary) |
| Limite de taille | 4 Ko (Standard) / 8 Ko (Advanced) | 64 Ko |
| Coût | Gratuit à très bon marché | 0,40 $/mois/secret + frais API |
| Rotation automatique | Non | Oui (intégration Lambda) |
| Cas d'usage | Valeurs de configuration, petits secrets | Secrets plus grands, exploitation en production |
Si .p12 dépasse 8 Ko ou si vous privilégiez la facilité de manipulation en tant que fichier, Secrets Manager est plus naturel. Pour 1 à 2 certificats, le coût mensuel est inférieur à un dollar.
Configuration côté AWS
La configuration du fournisseur d'identité OIDC est partagée avec l'Option 4, nous la passons ici.
Enregistrez dans Secrets Manager en binaire avec le préfixe fileb://, qui indique à l'AWS CLI d'envoyer le fichier tel quel en binaire :
# Corps du certificat (peut être enregistré directement en binaire)
aws secretsmanager create-secret \
--name ios/yourapp/dist-cert \
--secret-binary fileb://Certificates.p12
# Profil de provisionnement en binaire
aws secretsmanager create-secret \
--name ios/yourapp/provisioning-profile \
--secret-binary fileb://YourApp_AppStore.mobileprovision
# Clé API App Store Connect (.p8) en binaire
aws secretsmanager create-secret \
--name ios/yourapp/asc-api-key \
--secret-binary fileb://AuthKey_XXXXXXXXXX.p8
# Mots de passe en chaîne
aws secretsmanager create-secret \
--name ios/yourapp/p12-password \
--secret-string "your-p12-password"
Pour les mises à jour, utilisez update-secret :
aws secretsmanager update-secret \
--secret-id ios/yourapp/dist-cert \
--secret-binary fileb://Certificates_new.p12
La politique de permissions du rôle IAM autorise Secrets Manager au lieu de Parameter Store :
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue"],
"Resource": "arn:aws:secretsmanager:ap-northeast-1:123456789012:secret:ios/yourapp/*"
},
{
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:ap-northeast-1:123456789012:key/your-key-id"
}
]
}
Workflow GitHub Actions
Les valeurs récupérées via SecretBinary sont retournées encodées en Base64 dans la réponse API, donc décodez avec base64 --decode et écrivez dans un fichier :
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: macos-14
steps:
- uses: actions/checkout@v4
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-ios-deploy
aws-region: ap-northeast-1
- name: Fetch certificates from Secrets Manager
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
PP_PATH=$RUNNER_TEMP/build_pp.mobileprovision
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
# SecretBinary est retourné encodé en Base64 ; décoder et écrire dans un fichier
aws secretsmanager get-secret-value \
--secret-id ios/yourapp/dist-cert \
--query SecretBinary --output text | base64 --decode > $CERTIFICATE_PATH
aws secretsmanager get-secret-value \
--secret-id ios/yourapp/provisioning-profile \
--query SecretBinary --output text | base64 --decode > $PP_PATH
# SecretString peut être récupéré directement
P12_PASSWORD=$(aws secretsmanager get-secret-value \
--secret-id ios/yourapp/p12-password \
--query SecretString --output text)
# Placer la clé API (.p8) au chemin attendu par altool
API_KEY_ID="${{ secrets.APP_STORE_CONNECT_API_KEY_ID }}"
mkdir -p ~/.appstoreconnect/private_keys
aws secretsmanager get-secret-value \
--secret-id ios/yourapp/asc-api-key \
--query SecretBinary --output text | base64 --decode \
> ~/.appstoreconnect/private_keys/AuthKey_${API_KEY_ID}.p8
# Créer le trousseau et importer le certificat
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security import $CERTIFICATE_PATH -P "$P12_PASSWORD" \
-A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple: \
-k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles
# Les étapes archive / export / altool suivantes sont identiques à l'Étape 5
Avantages
.p12enregistrable et récupérable directement comme fichier (le plus proche d'Azure DevOps Secure Files)- Support jusqu'à 64 Ko : pas de souci de limite de taille
- Pas de credentials à long terme grâce à OIDC : mêmes avantages de sécurité que l'Option 4
- Logs d'audit avec CloudTrail disponibles nativement
- Rotation automatique : possible via Lambda (mais l'intégration côté Apple nécessite un effort de construction élevé)
- Contrôle des permissions granulaire avec IAM
Inconvénients
- Coût : ~0,40 $/secret/mois + frais API ; quelques dollars par mois pour 3 à 5 certificats
- Compte AWS requis
- Courbe d'apprentissage pour la configuration OIDC (partagée avec l'Option 4)
Idéal pour : priorité à la manipulation en tant que fichier, fichiers .p12 volumineux, nombreux fichiers de certificats pour plusieurs apps, rotation automatique envisagée, expérience équivalente à Azure DevOps Secure Files souhaitée
Option 6 : Autres gestionnaires de secrets cloud
Pour les équipes n'utilisant pas AWS, les options suivantes utilisent le même concept. Tous supportent l'intégration OIDC :
| Service | Coût | Limite de taille | Support binaire |
|---|---|---|---|
| Azure Key Vault | 0,03 $/10 000 opérations | 25 Ko | ○ (type Certificate) |
| Google Secret Manager | 0,06 $/mois/secret | 64 Ko | ○ |
| HashiCorp Vault (OSS) | Coûts serveur uniquement | Aucune limite | ○ |
Azure Key Vault propose notamment un type Certificate avec support officiel pour l'import/export direct au format .p12 — idéal pour les équipes utilisant l'écosystème Azure.
Option 7 : Protection supplémentaire avec GitHub Environments
Cette option peut être combinée avec n'importe laquelle des options ci-dessus. En configurant GitHub Environments, vous pouvez restreindre l'accès aux certificats aux déploiements depuis des branches spécifiques ou ajouter une étape d'approbation manuelle :
jobs:
deploy:
runs-on: macos-14
environment: production # ← Les Secrets liés à cet Environment sont utilisables
Comme les Secrets peuvent être isolés par Environment, vous évitez d'utiliser accidentellement des certificats de distribution production pour des builds de développement.
Configurations recommandées par taille d'équipe
| Taille | Configuration recommandée |
|---|---|
| Développeur solo / projet hobby | Option 1 (Base64 + Secrets) — la simplicité l'emporte |
| Startup / petite équipe (1 à 3 personnes) | Option 1 ou Option 2 (fichiers chiffrés) |
| Équipe moyenne (plusieurs apps ou 5+ personnes) | Option 3 (fastlane match) ou Option 5 (Secrets Manager) |
| Utilisateurs AWS, sensibles aux coûts | Option 4 (Parameter Store + OIDC) |
| Utilisateurs AWS, orientés exploitation | Option 5 (Secrets Manager + OIDC) — adapté aux fichiers |
| Enterprise / exigences d'audit | Option 4 ou 5 + Option 7 (Environments) combinées |
| Migration depuis Azure DevOps | Option 2 (fichiers chiffrés) ou Option 5 (Secrets Manager) — la plus proche de Secure Files |
Problèmes courants et solutions
No signing certificate "iOS Distribution" found
Survient souvent quand l'import du certificat dans le trousseau a échoué ou que set-key-partition-list est absent. Ajoutez security find-identity -v -p codesigning $KEYCHAIN_PATH comme étape pour vérifier que le certificat est visible — cela facilite l'isolation du problème.
error: exportArchive: "YourApp.app" requires a provisioning profile
Le profil de provisionnement n'a pas été placé correctement, ou le Bundle ID et le nom du profil dans provisioningProfiles d'ExportOptions.plist ne correspondent pas. Le nom du profil est le "nom affiché sur le site Apple Developer", pas le nom de fichier.
altool: Invalid API key
Souvent causé par des sauts de ligne ou des espaces supplémentaires dans le fichier .p8 encodé en Base64. Générez-le avec base64 -i file.p8 | pbcopy et collez directement. Si le nom de fichier ne suit pas la convention AuthKey_<KEY_ID>.p8, altool ne le reconnaîtra pas — construisez-le précisément via les variables d'environnement.
xcodebuild: error: The operation couldn't be completed. No such file or directory
Survient quand .xcworkspace est utilisé mais que -project est spécifié (ou vice versa). Si vous utilisez un workspace CocoaPods ou Swift Package Manager, spécifiez -workspace.
Parameter Store : ParameterNotFound ou AccessDenied
Erreurs lors de l'intégration AWS. Vérifiez dans cet ordre :
- Aucune faute de frappe dans le chemin du paramètre (hiérarchie comme
/ios/yourapp/...) ? ssm:GetParameterest-il autorisé pour le chemin cible dans la politique de permissions du rôle IAM ?- Pour les SecureString, le flag
--with-decryptionest-il inclus ? - La permission
kms:Decryptpour la clé KMS est-elle accordée au rôle IAM ? - La condition
subdans la Trust Policy correspond-elle à la branche d'exécution du workflow ?
Pour aller plus loin
Nous avons construit le pipeline avec une configuration minimale. Voici quelques extensions possibles :
- Notifications Slack : Intégrer
slackapi/slack-github-actionpour notifier automatiquement à la fin de l'upload. - Release notes automatiques : Extraire les changements depuis
git loget les saisir automatiquement dans le champ "What to Test" de TestFlight (difficile avec altool seul ; nécessite d'appeler directement l'App Store Connect API). - Déclencheur sur push de tag : Déclencher sur un push de tag
v*plutôt que surmainpour un flow de release plus intentionnel. - Exécuter les tests unitaires en amont : Lancer
xcodebuild testavant l'archive et arrêter le déploiement en cas d'échec des tests.
Conclusion
Une configuration sans fastlane présente les avantages d'être intuitive et facile à comprendre, de ne pas nécessiter d'environnement Ruby et d'être indépendante des problèmes de compatibilité fastlane lors des mises à jour Xcode. En contrepartie, des éléments comme la gestion des numéros de build doivent être gérés manuellement, et à mesure qu'on passe à la gestion de plusieurs apps, la charge de gestion augmente. Dans ce cas, migrer vers fastlane match ou AWS Secrets Manager / Parameter Store est un choix pragmatique.
Le principal obstacle lors de la configuration initiale est incontestablement la gestion des certificats — espérons que cet article et la section "Options de gestion des certificats" vous aideront à franchir cet obstacle.
