Maintenir une grosse extension AL qui se met à jour proprement, deux fois par an
Une extension Business Central se met à jour proprement à trois conditions : elle compile contre la prochaine mineure et la prochaine majeure avant que Microsoft les livre, sa codeunit d’upgrade est trop petite pour échouer, et chaque dépréciation annoncée par Microsoft apparaît en avertissement dans votre propre build, avec justification écrite, une version avant de devenir une erreur. Le code d’upgrade en est la plus petite partie. L’essentiel, c’est un calendrier et un ruleset.
Nous maintenons ainsi plusieurs applications AL : une app AppSource de facturation électronique (40 codeunits de test, 433 tests), et une extension par tenant de feuilles de temps projet d’environ quatre-vingts objets. Voici ce que font réellement ces dépôts.
Qu’exige le rythme de mise à jour de Business Central d’une extension ?
Deux majeures par an, en avril et octobre, une mineure les mois intermédiaires. Une preview sort environ un mois avant chaque majeure. Une fois la majeure disponible, les administrateurs ont cinq mois pour la planifier, puis un mois de grâce — septembre ou mars — où elle ne peut plus être repoussée. Vient ensuite la période forcée : toute extension faisant échouer la mise à jour peut être désinstallée automatiquement pour que l’environnement avance.
Un détail change la planification : le contrôle de compatibilité de Microsoft avant une majeure est technique. Il vérifie que votre app compile et s’installe encore sur la nouvelle version ; qu’elle fasse encore la bonne chose reste votre responsabilité ; le contrôle ne le voit pas. La question n’est donc pas si vous survivrez à la vague, mais si vous découvrez en février ce qui casse en avril, ou le jour où l’environnement d’un client se met à jour.
Pourquoi la codeunit d’upgrade est-elle le code le plus dangereux que vous possédez ?
Parce qu’une erreur dans OnUpgradePerCompany annule la mise à jour de l’extension. Pas la migration : la version entière. Chaque tenant contenant des données reste bloqué sur l’ancienne version jusqu’au correctif.
Un remplissage de libellés cosmétique, sur le chemin d’upgrade de notre app de facturation électronique, lisait la table E-Document de Microsoft pour réétiqueter quelques enregistrements. La plateforme a refusé la lecture avec une erreur de permission, alors que la codeunit déclarait cette permission ; la cause racine n’a jamais été établie. Une migration de libellés bloquait tous les tenants. Pire : notre pipeline était rouge depuis six jours sur un canal de déploiement inutilisé, et la vraie panne se cachait sous ce bruit. Un signal qui hurle en permanence ne signale rien.
La codeunit d’upgrade ne touche plus que nos propres tables et la file d’attente des travaux. Chaque migration lisant ou écrivant une table de base est sortie vers une tâche de fond, armée une fois par société et exécutée dans son propre Codeunit.Run : un échec ne peut ni bloquer les autres migrations, ni coûter une version. La déclaration Permissions de la codeunit d’upgrade a été supprimée. C’est une non-déclaration, pas un garde-fou : la codeunit n’accorde plus une permission devenue inutile, et le compilateur ne signalera pas une lecture de table de base remise sur ce chemin. Les tests l’attrapent : deux vérifient que le chemin d’upgrade laisse les enregistrements E-Document intacts, donc une lecture réintroduite échoue en CI, pas pendant la fenêtre d’upgrade d’un client.
Notre règle : tout ce qui est sur le chemin d’upgrade doit pouvoir échouer sans coûter une montée de version. Sinon, ça n’a rien à faire là.
Comment faire tourner une migration exactement une fois — et jamais zéro fois sans le savoir ?
Avec les upgrade tags, appliqués plus strictement que ne l’exige la documentation. La codeunit Upgrade Tag de Microsoft fournit HasUpgradeTag, SetUpgradeTag et un événement pour enregistrer vos tags, afin qu’une nouvelle société les reçoive déjà posés. Le schéma documenté : vérifier le tag, faire le travail, poser le tag. Nos ajouts :
- Un tag par migration, jamais partagé. Une migration terminée ne doit jamais passer pour une autre terminée.
- Le tag est daté, préfixé par l’espace de noms et écrit en littéral brut, jamais dans un label traduisible.
- Vérification, travail et pose du tag forment une unité atomique dans une seule procédure. Codeunit.Run ne valide la transaction qu’en l’absence d’erreur : un échec laisse le tag absent et la migration repart au prochain armement de la tâche.
- Il n’existe délibérément aucun assistant « lancer toutes les migrations ». Il en existait un, supprimé en revue : une procédure appelant chaque remplissage est l’endroit naturel pour en enregistrer un nouveau, et exactement ce que la production n’appelle pas. Une troisième migration câblée là seulement passerait tous les tests et ne tournerait sur aucun tenant.
Cette dernière règle vient d’un second incident. Une fois les migrations sorties du chemin d’upgrade, nous les avions accrochées à un travail récurrent, dont l’entrée de file d’attente est créée en attente, par conception. Sur un tenant qui se met à jour, exactement la population ayant des données à migrer, rien ne les déclenchait. Aucune erreur, aucune trace. Le silence déguisé en fonctionnement. Le correctif a suivi la lecture du code source de la file d’attente dans le dépôt public BCApps de Microsoft ; un test supprimant les tags via la Upgrade Tag Library l’épingle.
Comment apprendre un changement cassant avant que la vague ne vous l’apprenne ?
En compilant contre, selon un calendrier calé sur celui de Microsoft.
AL-Go for GitHub livre trois workflows de sonde — Test Current, Test Next Minor, Test Next Major — pointant chacun sur un artefact différent. Chez nous, les trois fichiers de paramètres ne diffèrent que d’une ligne : ////latest, ////nextminor, ////nextmajor. Notre cadence : Test Current et Test Next Minor chaque semaine, Test Next Major chaque semaine en février-mars et en août-septembre — les deux fenêtres les plus proches de la livraison réelle, avec encore du temps pour réagir — et Update AL-Go System Files chaque semaine pour que l’outillage lui-même ne pourrisse pas. L’app feuilles de temps planifie les quatre ainsi aujourd’hui. Sur l’app de facturation électronique, seuls Test Current et la mise à jour des fichiers système sont planifiés ; Test Next Minor et Test Next Major se lancent encore à la demande, l’écart que nous comblons ensuite.
L’autre moitié : rendre les dépréciations bruyantes. Le calendrier de Microsoft : un code marqué obsolète en version N reste présent en N+1 et N+2, et retiré au plus tôt en N+3 — au moins douze mois. Dans le ruleset de l’app de feuilles de temps, AL0432 (« vous référencez un objet de base que Microsoft a marqué en attente d’obsolescence ») est donc épinglé à Warning, avec justification écrite, pour que nul ne l’abaisse en silence. Une règle signalant un runtime en retard sur la plateforme est activée pour la même raison. AppSourceCop va plus loin avec obsoleteTagMinAllowedMajorMinor : réglez-le deux versions en arrière, et toute référence à ce que Microsoft pourrait retirer à la prochaine version devient une erreur dès aujourd’hui. Microsoft recommande de l’activer le temps d’une passe, vu son coût sur les gros projets.
Deux autres dépréciations sont tuées dans le manifeste plutôt qu’au grep : NoImplicitWith transforme un with ambigu en erreur de compilation au lieu d’une surprise à l’exécution quand Microsoft ajoute un champ à une table de base, et NoPromotedActionProperties interdit les anciennes propriétés d’actions promues.
Même discipline pour votre propre surface : en AppSource, on ne supprime pas, AppSourceCop refuse net les tables et champs retirés, retypés ou renommés. On marque ObsoleteState = Pending avec une raison et le tag de version, on livre l’alternative, puis on passe à Removed au moins une majeure plus tard.
Quelles règles de code rapportent vraiment au moment de la mise à jour ?
Celles qui partent du principe que Microsoft va modifier l’application de base — parce qu’il le fera.
Additif uniquement. Extensions de tables et de pages, abonnés aux événements sur les objets de base ; nos propres tables pour nos propres données, et le moins possible : certaines des extensions AL à connaître existent pour supprimer du code que vous posséderiez sinon. Les deux apps ont une racine d’espace de noms unique et un affixe d’objet obligatoire, et l’app de facturation électronique compte 22 abonnés aux événements pour zéro logique de base modifiée.
Exécuter les triggers par défaut. Insert(true), Modify(true), Validate plutôt que :=. Cela respecte la logique métier de la table, présente et future. Quand Microsoft ajoute un OnValidate à un champ que vous alimentez, le code qui validait tient ; celui qui affectait directement saute la nouvelle logique sans bruit. Un contournement porte un commentaire expliquant pourquoi ; convertir un vieux (false) en (true) est un changement de comportement couvert par des tests, jamais un balayage.
Access = Internal sur les codeunits d’installation et d’upgrade, imposé comme erreur par le ruleset, pas comme habitude.
Un seul ruleset pour l’organisation, hébergé à distance et hérité par chaque dépôt, avec des surcharges locales portant chacune une justification — soixante-huit dans l’app de feuilles de temps. La configuration des analyseurs vit dans git, pas dans l’éditeur d’un développeur : aucune dérive individuelle, une barrière locale au moins aussi stricte que la CI.
Une leçon de l’historique des versions : relever runtime dans app.json est une contrainte de déploiement, pas seulement de compilation. Nous l’avons passé à 17.0 neuf jours après la disponibilité générale de la majeure d’avril 2026 ; le bac à sable de recette n’avait pas encore pris la mise à jour, la publication a échoué, retour en arrière le jour même. Épinglez application sur le plancher de la majeure, pas sur une mineure.
L’approche Asio Services
L’hygiène de mise à jour n’est pas une phase, c’est l’état par défaut du dépôt. Chaque changement naît comme une issue, arrive avec un test, passe les analyseurs à zéro avertissement en local avant le push, et est compilé contre la prochaine vague avant sa livraison. L’alternative : une mise à jour forcée qui désinstalle votre app un mardi. Voilà aussi pourquoi nous repoussons les personnalisations qui n’auraient jamais dû être écrites : chaque objet possédé est une chose de plus à renouveler deux fois par an.
Si vous avez hérité d’une extension dont la dernière mise à jour a été une mauvaise semaine, ou si vous planifiez un développement Business Central devant encore tourner dans cinq vagues, commencez par notre formulaire de clarté. Nous vous dirons ce qui est sur le chemin d’upgrade et ce qui ne devrait pas y être.
FAQ
Qu’est-ce qu’une codeunit d’upgrade dans Business Central ?
Une codeunit avec Subtype = Upgrade, dont les triggers — OnUpgradePerCompany et OnUpgradePerDatabase — s’exécutent quand une nouvelle version de votre extension s’installe par-dessus l’ancienne. Une erreur dans l’un ou l’autre annule la mise à jour de l’extension : elle ne doit faire que ce qui ne peut pas attendre, ne toucher que ce qui vous appartient.
Comment fonctionnent les upgrade tags ?
La codeunit Upgrade Tag enregistre, par société, qu’un morceau de code d’upgrade nommé a tourné. On vérifie le tag, on fait le travail, on pose le tag, et on l’enregistre dans l’événement OnGetPerCompanyUpgradeTags pour que les nouvelles sociétés démarrent avec le tag déjà posé. Un tag par migration garde les migrations indépendantes.
Combien de temps avant que Microsoft retire du code obsolète ?
Au moins douze mois. Un code marqué obsolète en version N reste présent en N+1 et N+2, et retiré au plus tôt en N+3. Le paramètre obsoleteTagMinAllowedMajorMinor d’AppSourceCop trouve les références qui pourraient casser à la prochaine version.
Que se passe-t-il si mon extension bloque une mise à jour majeure ?
Pendant la période forcée, elle peut être désinstallée automatiquement pour que l’environnement se mette à jour. Ses données sont conservées et récupérables en installant ensuite une version compatible. Si elle bloque une mise à jour de sécurité critique, elle peut être désinstallée sous 14 jours.