Publier des versions d'APK
Envoyez un APK Android signé directement depuis votre pipeline de publication et suivez-le jusqu'à son apparition dans la bibliothèque de votre entreprise.
Comment fonctionne la publication
Créez une session d'envoi multipart avec l'API Nomid, envoyez les octets de l'APK directement vers S3 à l'aide d'URL de parties de courte durée, finalisez la session, puis interrogez l'API pour obtenir le résultat final du traitement.
LIBRARY_MANAGE couvre désormais les envois d'APK et la gestion des applications, y compris la lecture, la modification, le masquage et la restauration des applications et des versions. Les API publiques pour les fichiers, les contacts et les autres ressources de la bibliothèque ne sont toujours pas disponibles. Il n'autorise ni l'affectation de stratégies, ni les commandes d'appareil, ni le provisionnement, ni l'administration du compte.
Lire et gérer les applications de la bibliothèque de l'entreprise et publier des versions d'APK signées. LIBRARY_MANAGE est une autorisation sensible, à l'échelle de l'entreprise, pour les envois d'APK et la gestion des applications. Elle n'autorise ni l'affectation de stratégies, ni les commandes d'appareil, ni le provisionnement, ni les opérations sur le compte.
Une publication terminée place la version dans la bibliothèque et peut déclencher la distribution existante pour les stratégies qui utilisent son application et son track. Elle ne confirme pas qu'un appareil a installé la version.
Avant de commencer
- Créez une clé d'API dédiée avec LIBRARY_MANAGE. Un utilisateur du portail qui crée cette autorisation doit disposer de API_KEY_MANAGE, d'une portée de stratégie d'entreprise sans restriction, de LIBRARY_VIEW et de LIBRARY_EDIT. L'autorisation sensible est décochée par défaut ; les clés existantes ne l'obtiennent pas automatiquement.
- N'accordez cette autorisation qu'à une automatisation de confiance qui a besoin de ces opérations de bibliothèque à l'échelle de l'entreprise. LIBRARY_MANAGE permet les envois d'APK et la gestion des applications, y compris l'accès aux sessions d'envoi démarrées par d'autres clés ou utilisateurs autorisés de l'entreprise ; conservez la clé dans le magasin de secrets de votre CI, et effectuez sa rotation ou révoquez-la lorsque les accès changent.
- Utilisez Python 3.11 ou une version plus récente pour l'exemple d'outil d'envoi. Compilez et signez l'APK avec l'identité de signature prévue et un code de version Android adapté avant l'envoi. L'API ne signe pas les APK et ne publie pas sur Google Play.
- Envoyez X-API-Key à l'API Nomid. La clé validée détermine l'entreprise ; les requêtes n'ont aucun paramètre de locataire ni d'entreprise. Aucune autorisation DEVICES_READ ou POLICIES_READ n'est nécessaire pour publier un APK.
Points de terminaison
https://api.nomid.tech/emm/api/v1
Les huit routes ci-dessous exigent X-API-Key avec LIBRARY_MANAGE ; une session d'envoi appartient à l'entreprise de la clé.
| Méthode | Point d'accès | Description |
|---|---|---|
| POST | /library/apps/uploads/check | Vérifiez l'éligibilité, le contenu en double et une éventuelle session reprenable existante. Un doublon est purement informatif. |
| POST | /library/apps/uploads/start | Démarrez un envoi et recevez sessionId, partSize et expiresAt. |
| POST | /library/apps/uploads/parts/sign | Obtenez une URL PUT S3 directe de courte durée et les requiredHeaders pour une partie. |
| GET | /library/apps/uploads/parts/{sessionId} | Listez les parties déjà envoyées et réutilisables, avec leurs ETag exacts, pour la même session. |
| POST | /library/apps/uploads/complete | Soumettez la liste ordonnée des parties et, en option, les tracks pour assembler et traiter l'APK. |
| GET | /library/apps/uploads/status/{sessionId} | Interrogez la session jusqu'à ce que status soit completed ou failed. |
| DELETE | /library/apps/uploads/abort/{sessionId} | Annulez une session inachevée ; les sessions en cours de traitement ou terminées ne peuvent pas être annulées. |
| GET | /library/apps/{packageName}/versions/{versionCode}/check-report | Lisez le dernier rapport de vérification d'envoi d'une version. Il nécessite le packageName et le versionCode de la version, ainsi que son environnement lorsqu'il n'est pas vide. |
Séquence d'envoi
- POST /check avec le nom de fichier, la taille réelle en octets (strictement positive), le mime accepté et le SHA-256 de l'APK. Examinez allowed et reason ; duplicateOf et existingSessionId peuvent être présents.
- POST /start avec les mêmes champs. Conservez de façon sécurisée le sessionId et le partSize renvoyés afin que cette exécution puisse reprendre en cas d'interruption.
- Pour chaque partie manquante, POST /parts/sign avec sessionId et un partNumber de 1 à 10000.
- Découpez le fichier en blocs séquentiels de partSize octets ; seul le dernier bloc peut être plus court. Envoyez chaque bloc en PUT à son URL signée avec requiredHeaders, puis conservez l'en-tête de réponse ETag exactement tel qu'il a été renvoyé.
- POST /complete avec sessionId, chaque paire partNumber/etag et, en option, tracks. La finalisation lance le traitement de l'APK en arrière-plan ; sa réponse immédiate peut encore indiquer processing.
- Effectuez un GET sur le point de terminaison status avec l'ID de session jusqu'à ce que le status, en minuscules, soit completed ou failed. Les réponses completed incluent app ; les réponses failed incluent un errorMessage. Les autres états ne signifient pas que la publication a réussi.
La taille doit être supérieure à zéro et respecter la limite d'envoi configurée. mime vaut application/vnd.android.package-archive ou application/octet-stream ; sha256 est le condensé hexadécimal de 64 caractères, en minuscules, des octets de l'APK. Les délais d'expiration de la session et de l'URL signée s'appliquent.
L'URL signée est destinée à S3, pas à l'API Nomid : envoyez uniquement requiredHeaders avec le PUT, jamais X-API-Key. N'enregistrez pas l'URL signée dans les journaux. Si elle expire, signez à nouveau cette partie.
Exemples de requêtes et de réponses
Ces exemples montrent les noms de champs et les valeurs de status en minuscules. Les identifiants, le condensé, l'heure, la taille des parties, les ETag et l'URL sont donnés à titre d'illustration ; utilisez les valeurs renvoyées pour votre propre session.
Requête POST /check ou /start
{
"filename": "app-release.apk",
"size": 33554432,
"mime": "application/vnd.android.package-archive",
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
} Réponse POST /check (éligible)
{
"allowed": true,
"recommendedPartSize": 16777216
} Réponse POST /start
{
"sessionId": "123e4567-e89b-12d3-a456-426614174000",
"partSize": 16777216,
"expiresAt": "2026-10-28T12:00:00Z"
} Requête POST /parts/sign
{
"sessionId": "123e4567-e89b-12d3-a456-426614174000",
"partNumber": 1
} Réponse POST /parts/sign
{
"url": "https://<presigned-part-url>",
"requiredHeaders": {}
} Réponse GET des parties envoyées
{
"status": "uploading",
"partSize": 16777216,
"parts": [{ "partNumber": 1, "etag": "\"example-etag\"" }]
} Requête POST /complete
{
"sessionId": "123e4567-e89b-12d3-a456-426614174000",
"parts": [
{ "partNumber": 1, "etag": "\"example-etag-1\"" },
{ "partNumber": 2, "etag": "\"example-etag-2\"" }
],
"tracks": ["prod"]
} Réponse POST /complete (en traitement)
{
"status": "processing",
"sessionId": "123e4567-e89b-12d3-a456-426614174000"
} requiredHeaders peut contenir des en-têtes exigés par la signature S3. Appliquez la table renvoyée telle quelle, valeurs comprises, et récupérez l'en-tête de réponse ETag de chaque PUT réussi.
Vérifications d'envoi
Chaque envoi est vérifié avant la création de sa version : la signature et le manifeste de l'APK, les versions précédentes du même paquet dans votre entreprise, et les appareils de votre entreprise (leurs niveaux d'API Android, ainsi que les applications installées dessus avec leurs certificats de signature). Le résultat est un rapport de vérification que vous pouvez lire depuis l'API.
Les vérifications signalent des constats. Les constats sont enregistrés et renvoyés, et l'envoi aboutit quand même ; seul VERSION_CODE_CONFLICT rejette un envoi. Un constat de sévérité ERROR décrit un APK que les appareils peuvent refuser d'installer ou de mettre à jour ; un WARNING décrit un APK qui s'installe avec un risque qu'il vaut la peine de connaître. Aucun des deux n'arrête l'envoi.
Un envoi terminé inclut checkReport dans la réponse GET /status, et dans la réponse POST /complete lorsqu'elle renvoie une session terminée (lors d'un appel répété, par exemple). Un envoi rejeté par une vérification a le status failed et inclut lui aussi checkReport. Le rapport est absent pendant le traitement de l'envoi, et lorsque l'envoi a échoué pour une autre raison. L'errorMessage d'un envoi en échec est générique ; lisez donc les constats avec blocking true dans checkReport pour savoir pourquoi une vérification l'a rejeté.
Les versions renvoyées par les routes de versions de la bibliothèque (la liste des versions et une version isolée, documentées dans le guide des applications de la bibliothèque) incluent checkSummary : le status du dernier rapport, errorCount (constats ERROR) et warningCount (constats WARNING). Il vaut null pour les versions envoyées avant l'existence des vérifications. La route check-report du tableau ci-dessus renvoie le rapport complet d'une version, dans la même forme que checkReport, et répond 404 lorsque la version n'en a pas. Comme les routes d'envoi, elle exige X-API-Key avec LIBRARY_MANAGE.
Champs du rapport
- status : PASSED (aucun constat), WARNINGS (la version a été créée et comporte des constats, y compris des constats ERROR qui n'ont pas bloqué) ou BLOCKED (un constat a arrêté l'envoi et aucune version n'a été créée).
- findings : les constats ERROR en premier. Chacun comporte code, severity (ERROR ou WARNING), params, blocking, et affectedDeviceCount lorsque le constat concerne des appareils. blocking vaut true uniquement pour un constat qui a arrêté l'envoi.
- params : valeurs lues dans l'APK envoyé ou dans les propres versions de votre entreprise, comme les versions, les niveaux de SDK, les noms d'autorisations et d'ABI, et les empreintes de certificats. Ses clés dépendent du code.
- affectedDeviceCount : le nombre d'appareils de votre entreprise auxquels le constat s'applique. Pour MIN_SDK_ABOVE_FLEET et TARGET_SDK_TOO_LOW, params.scope vaut INSTALLED lorsqu'il compte les appareils sur lesquels le paquet est installé, ou COMPANY lorsqu'aucun appareil ne l'a et qu'il compte tous les appareils de l'entreprise.
- checkerVersion identifie l'ensemble de vérifications qui a produit le rapport. createdAt est l'heure d'enregistrement du rapport, en UTC.
Codes de vérification
Les codes sont stables : de nouveaux peuvent être ajoutés, et les codes existants ne sont pas renommés. Lisez severity et blocking dans chaque constat plutôt que dans ce tableau.
| Code | Sévérité | Signification |
|---|---|---|
| SIGNATURE_INVALID | ERROR | L'APK n'est pas signé, ou sa signature ne se vérifie pas. params.issues liste les problèmes trouvés. |
| SIGNATURE_SCHEME_V1_ONLY | ERROR | L'APK cible Android 11 (API 30) ou supérieur mais n'a qu'une signature v1 (JAR), que refusent Android 11 et les versions ultérieures. Signez-le avec APK Signature Scheme v2 ou ultérieur. params.targetSdk. |
| SIGNING_KEY_CHANGED | ERROR | Le certificat de signature diffère de celui de la version précédente et aucune preuve de rotation de clé v3 ne les relie (params.previousVersionCode, previousCertSha256, newCertSha256), ou il diffère des certificats installés sur affectedDeviceCount appareils (params.installedCertSha1, newCertSha1, en hexadécimal SHA-1, tel que les appareils le signalent). Les appareils refusent de mettre à jour une application vers une build signée avec une autre clé. |
| TEST_ONLY | WARNING | Le manifeste définit android:testOnly="true", ce qui désigne une build de développement. Aucun params. |
| VERSION_CODE_CONFLICT | ERROR | Une version avec le même versionCode et le même environnement a déjà été envoyée avec un binaire différent. C'est le seul constat qui rejette un envoi. params.versionCode. |
| VERSION_DOWNGRADE | WARNING | Le versionCode est inférieur à celui de votre dernière version publiée (params.latestVersionCode), ou inférieur à celui installé sur affectedDeviceCount appareils (params.installedVersionCode, le plus élevé des versions installées). Les appareils ne passent normalement pas à un code de version inférieur. |
| MIN_SDK_ABOVE_FLEET | WARNING | Le minSdk de l'APK est supérieur au niveau d'API Android de affectedDeviceCount appareils, qui ne peuvent donc pas l'installer. params.minSdk, scope. |
| TARGET_SDK_TOO_LOW | WARNING | Le targetSdk de l'APK est inférieur à 23, et Android 14 et les versions ultérieures refusent de l'installer. affectedDeviceCount compte les appareils concernés qui exécutent Android 14 ou une version ultérieure. params.targetSdk, minimumTargetSdk, scope. |
| MISSING_ARM64 | WARNING | L'APK contient des bibliothèques natives mais aucune pour arm64-v8a. params.abis liste les ABI qu'il contient. |
| DEBUGGABLE | WARNING | Le manifeste définit android:debuggable="true". Aucun params. |
| NEW_DANGEROUS_PERMISSIONS | WARNING | L'APK demande des autorisations d'exécution (dangereuses) que la version précédente ne demandait pas. params.permissions, previousVersionCode. |
Exemples de rapport de vérification
Le premier exemple est la réponse GET status d'un envoi terminé qui comporte des constats ; les autres champs, comme app, sont omis. Le second est un envoi en échec rejeté par une vérification. Les identifiants, les compteurs, les empreintes et les heures sont donnés à titre d'illustration.
Envoi terminé avec des constats
{
"status": "completed",
"sessionId": "123e4567-e89b-12d3-a456-426614174000",
"checkReport": {
"status": "WARNINGS",
"findings": [
{
"code": "SIGNING_KEY_CHANGED",
"severity": "ERROR",
"params": {
"installedCertSha1": ["0123456789abcdef0123456789abcdef01234567"],
"newCertSha1": ["89abcdef0123456789abcdef0123456789abcdef"]
},
"affectedDeviceCount": 2,
"blocking": false
},
{
"code": "MIN_SDK_ABOVE_FLEET",
"severity": "WARNING",
"params": { "minSdk": 28, "scope": "INSTALLED" },
"affectedDeviceCount": 3,
"blocking": false
},
{
"code": "MISSING_ARM64",
"severity": "WARNING",
"params": { "abis": ["armeabi-v7a"] },
"blocking": false
}
],
"checkerVersion": "2",
"createdAt": "2026-11-03T09:30:00Z"
}
} Envoi rejeté par une vérification
{
"status": "failed",
"sessionId": "123e4567-e89b-12d3-a456-426614174000",
"errorMessage": "Upload processing failed",
"checkReport": {
"status": "BLOCKED",
"findings": [
{
"code": "VERSION_CODE_CONFLICT",
"severity": "ERROR",
"params": { "versionCode": 42 },
"blocking": true
}
],
"checkerVersion": "2",
"createdAt": "2026-11-03T09:30:00Z"
}
} checkSummary sur une version dans une liste de versions
{
"versionCode": 42,
"environment": "",
"status": "PUBLISHED",
"displayName": "Kiosk",
"displayVersion": "1.4.2",
"createdAt": "2026-11-03T09:30:00Z",
"sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"fileSizeBytes": 33554432,
"tracks": ["prod"],
"checkSummary": { "status": "WARNINGS", "errorCount": 1, "warningCount": 2 }
} Tracks et nouvelles tentatives
Si tracks est omis, prod est utilisé. La première version d'une nouvelle application inclut toujours prod, même si un autre track est demandé. Les versions suivantes peuvent cibler des noms de track explicites d'au plus 10 caractères. Cette API ne comporte aucune étape de brouillon ni d'approbation.
Une somme de contrôle identique peut identifier une application ou une version existante, mais elle n'affecte pas cette version au track demandé par cette exécution. Ne considérez pas un résultat de doublon de /check comme une publication réussie pour un nouveau track.
Ne reprenez que l'ID de session enregistré lors de cette même exécution de publication, avec le même APK et les mêmes tracks. Effectuez un GET sur le point de terminaison des parties envoyées avec cet ID de session pour réutiliser les parties. Après un résultat incertain de /complete, interrogez cette session avant de réessayer la finalisation. Une session annulée ou expirée nécessite un nouvel envoi.
Publier depuis la CI
Sur un runner CI Linux disposant de Python 3.11 ou d'une version plus récente et de sha256sum, téléchargez l'outil d'envoi Python (bibliothèque standard uniquement) et vérifiez sa somme de contrôle épinglée avant de l'exécuter. Examinez la copie vérifiée et versionnez-la dans le dépôt de votre pipeline pour les exécutions futures. Fournissez NOMID_API_KEY par l'injection d'environnement secrète de votre plateforme CI. L'outil d'envoi se termine avec un code non nul en cas d'échec ou de dépassement du délai.
Télécharger l'outil d'envoi Python
curl --fail --silent --show-error --output publish_apk.py \
https://www.nomidmdm.com/developers/publish_apk.py &&
printf '%s %s\n' '8d9ba2a4432e6e6db05ba2411bfb6bfa85c85a79d1ce480d97e0014f965f12d2' publish_apk.py | sha256sum --check --status &&
python3 publish_apk.py app-release.apk --track prod Pour reprendre une exécution interrompue, transmettez explicitement son ID de session enregistré, avec le même APK et les mêmes tracks :
python3 publish_apk.py app-release.apk --track prod \
--session-id SESSION_ID_FROM_PREVIOUS_RUN Ne mettez pas la clé d'API dans la commande, le dépôt ou les journaux ; n'activez pas le suivi du shell. Gardez les URL présignées et leurs en-têtes requis hors des journaux de CI. Le succès de la publication et l'installation sur les appareils sont des vérifications distinctes.
GitHub Actions : publier un APK lorsqu'une release est publiée
Ce workflow complet télécharge un APK déjà signé, joint à la release GitHub qui l'a déclenché, vérifie l'outil d'envoi Nomid et attend la publication dans la bibliothèque de votre entreprise. Il utilise l'asset de la release, et non un artifact de build d'Actions, et ne compile ni ne signe votre application.
- Créez une clé d'API Nomid dédiée avec LIBRARY_MANAGE. Dans votre dépôt GitHub, ouvrez Settings → Secrets and variables → Actions et ajoutez un secret de dépôt nommé NOMID_API_KEY contenant la clé. Aucune autorisation sur les appareils ou les stratégies n'est nécessaire.
- Enregistrez le YAML ci-dessous sous .github/workflows/publish-to-nomid.yml dans votre dépôt avant de créer l'étiquette de la release, afin que le commit étiqueté contienne le workflow. Remplacez APK_ASSET_NAME par le nom exact de l'APK signé que vous joignez à chaque release.
- Compilez et signez l'APK dans votre pipeline existant. Joignez-le à une release GitHub en brouillon sous le nom app-release.apk (ou le nom d'asset que vous avez configuré), puis publiez la release. L'exemple prend aussi en charge les dépôts privés grâce au jeton GitHub intégré avec contents: read. L'APK doit déjà être joint lors de la publication de la release ; sinon l'étape de téléchargement échoue.
name: Publish APK to Nomid
on:
release:
types: [published]
permissions:
contents: read
concurrency:
group: nomid-apk-${{ github.event.release.id }}
cancel-in-progress: false
jobs:
publish:
runs-on: ubuntu-24.04
timeout-minutes: 30
env:
APK_ASSET_NAME: app-release.apk
NOMID_TRACK: ${{ github.event.release.prerelease && 'beta' || 'prod' }}
steps:
- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: '3.12'
- name: Download the signed APK from this release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
shell: bash
run: |
set -euo pipefail
gh release download --repo "$GH_REPO" \
--pattern "$APK_ASSET_NAME" --output app-release.apk -- "$RELEASE_TAG"
test -s app-release.apk
- name: Download and verify the Nomid uploader
shell: bash
run: |
set -euo pipefail
curl --fail --silent --show-error --output publish_apk.py \
https://www.nomidmdm.com/developers/publish_apk.py
printf '%s %s\n' '8d9ba2a4432e6e6db05ba2411bfb6bfa85c85a79d1ce480d97e0014f965f12d2' publish_apk.py \
| sha256sum --check --status
- name: Publish to the Nomid company library
env:
NOMID_API_KEY: ${{ secrets.NOMID_API_KEY }}
shell: bash
run: |
set -euo pipefail
if [[ -z "$NOMID_API_KEY" ]]; then
echo "Configure the NOMID_API_KEY repository secret before publishing." >&2
exit 1
fi
python3 publish_apk.py app-release.apk --track "$NOMID_TRACK" --timeout 1200 Les releases stables sont publiées sur prod ; les prereleases GitHub sont publiées sur beta. Pour une nouvelle application, Nomid inclut toujours prod lors de sa première publication, même lorsque beta est demandé. Une prerelease d'un nouveau paquet n'est donc pas isolée de prod. Les versions suivantes conservent la sélection de track explicite. Choisissez le type de release et le track avant de publier.
La clé Nomid n'est fournie qu'à l'étape de publication, par son environnement. Une clé absente, un APK absent, une somme de contrôle différente, une publication en échec ou un dépassement du délai de publication font échouer le job. Un job réussi confirme la publication dans la bibliothèque, pas l'installation sur les appareils. En cas d'interruption, utilisez le sessionId enregistré avec le même APK et le même track, comme décrit plus haut ; relancer le workflow entier démarre un nouvel envoi.
Si un autre workflow publie la release GitHub avec son GITHUB_TOKEN intégré, GitHub ne déclenche pas ce workflow de release. Dans ce cas, exécutez l'outil d'envoi vérifié directement dans le job de release existant après avoir joint l'APK, ou publiez la release avec un jeton d'application GitHub ou un jeton d'accès personnel dont la portée est appropriée. Gardez le GH_TOKEN de l'étape de téléchargement séparé de la clé d'API Nomid.
Documentation GitHub : déclencher un workflow depuis un workflow
Erreurs et reprise
- 401 : la clé est absente, invalide, expirée ou révoquée. Vérifiez l'injection des secrets de la CI sans afficher la valeur.
- 403 : la clé n'a pas LIBRARY_MANAGE. Créez une clé correctement autorisée ; des autorisations de lecture ou d'écriture sans rapport ne suffisent pas.
- 404 pour un ID de session inconnu dans l'entreprise de la clé. Confirmez l'ID et la clé liée à l'entreprise ; ne basculez pas vers une autre entreprise pour le chercher.
- 400 pour des champs de requête invalides, y compris la taille en octets ou le MIME. Corrigez la requête ; ne réessayez pas une entrée invalide inchangée.
- 410 : la session d'envoi a expiré. Démarrez un nouvel envoi ; les URL de parties signées précédemment ne peuvent pas être réutilisées.
- 409 depuis /abort signifie que l'envoi est déjà en cours de traitement ou terminé. Vérifiez son statut au lieu d'essayer de l'annuler.
- 422 depuis /complete signifie que les parties fournies ne peuvent pas former le fichier. Comparez la liste complète des parties et les ETag exacts avec le point de terminaison des parties envoyées.
- En cas d'erreur réseau ou S3 transitoire, signez à nouveau l'URL d'une partie expirée et réutilisez les parties réussies. Après un résultat incertain de /complete, interrogez d'abord la même session. Un envoi finalisé en échec peut être réessayé par le même chemin de finalisation tant que sa session est valide.