Empaqueter les dépendances du modèle
- Leçon 2
- 40 min
- Module 1 : Définir le contrat d'inférence
Leçon 2 sur 15 · Déployer des modèles d'IA pour de vrais utilisateursEmpaqueter les dépendances du modèle
1. Définir le contrat d'inférence
2. Choisir un mode de service
3. Créer le service de prédiction
4. Choisir l'infrastructure et mesurer
5. Mettre en production, surveiller et rétablir
Ce que vous saurez faire
Placez tout ce qui permet d’exécuter et de vérifier le modèle dans un dossier au numéro de version immuable.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
- Le contrat d’inférence et
contract.py, présentés dans Entrées et sorties des prédictions. - Le contenu de l’artefact du modèle et pourquoi on sauvegarde tout le pipeline, présentés dans Empaqueter les artefacts du cours Apprentissage automatique : du problème à un modèle fiable.
Ce qu’il vous faut sur votre ordinateur
~/projects/escalation-serviceavec.venv,requirements.txtetescalation/contract.py, provenant de Entrées et sorties des prédictions : écrire le contrat.- Le dossier
~/projects/ticket-modeldu cours d’apprentissage automatique, à côté, avecmodel/(créé parpython train.py) etdata/(valid.csvetextra_features.csv). Si vous avez plutôt téléchargéC17/start, il contient les deux dossiers, mais pas de.venv. Pour le premier exemple, utilisez le.venvdeescalation-service. - Si votre projet manque ou ne fonctionne plus, vous pouvez copier le module terminé,
end-of-m01, et suivre la lecture. Utiliser un instantané présente les étapes.
Testé avec Python 3.14.6 sur macOS, avec une puce Apple. Les étapes pour Windows et Linux ne sont pas testées.
Une boîte de meubles à assembler
Section intitulée « Une boîte de meubles à assembler »Une bibliothèque à assembler arrive dans une seule boîte. Elle contient les planches, un sac de vis, les instructions et une liste pour vérifier les pièces. Un numéro de modèle figure sur la boîte. Si les vis étaient dans une autre boîte, les planches seraient inutiles. Quand l’usine change le modèle, elle imprime un nouveau numéro. Elle ne met jamais des planches différentes dans une boîte portant l’ancien numéro.
Le cours d’apprentissage automatique comparait un artefact de modèle à une recette qui doit inclure la température du four (Empaqueter les artefacts). Un service a besoin de plus que le dossier model/ de ce cours. Il lui faut le contrat, les versions testées, des billets de soutien pour les vérifications et une version désignant le tout.
Le problème : le modèle seul
Section intitulée « Le problème : le modèle seul »Tomás regarde le pipeline du cours d’apprentissage automatique : « Le vrai modèle, c’est la dernière étape, la régression logistique. Sauvegardez seulement ça : le fichier est cinq fois plus petit. » Il sauvegarde pipeline[-1] et calcule le score du billet d’exemple :
import joblibimport pandas as pd
pipeline = joblib.load("model/escalation_model.joblib")model = pipeline[-1] # only the logistic regressionticket = pd.DataFrame([dict(channel="email", team="payment", segment="home", region="west", priority=1, order_value=89.5, word_count=112, customer_tenure_days=400, prior_tickets_90d=2, created_hour=10, prior_escalations_90d=1)])model.predict_proba(ticket)La sortie (abrégée) :
UserWarning: X has feature names, but LogisticRegression was fitted without feature names...ValueError: could not convert string to float: 'email'Le fichier est plus petit (1 087 octets au lieu de 5 354), mais le modèle seul est inutilisable. La régression logistique n’a jamais vu le texte email. Elle a appris à partir de 28 nombres produits par le prétraitement. Celui-ci crée une colonne par catégorie connue et met les nombres à l’échelle, avec leurs indicateurs de valeurs manquantes. Les médianes, les moyennes, les échelles et la liste des catégories se trouvent toutes dans les étapes de prétraitement. Sans elles, personne ne peut reproduire ces 28 nombres exactement comme pendant l’entraînement.
La première règle de cette leçon est donc celle du cours d’apprentissage automatique : sauvegardez tout le pipeline, prétraitement et modèle, dans un seul objet. Le cas d’échec ci-dessous montre ce qui arrive quand Tomás tente plutôt de reconstruire le prétraitement.
L’idée : un paquet
Section intitulée « L’idée : un paquet »Un fichier créé pendant l’entraînement, puis utilisé par un autre programme, est un artefact, comme dans le cours d’apprentissage automatique. Pour servir le modèle, un artefact ne suffit pas. Le service a besoin de fichiers qui restent ensemble, sous une même version. Dans ce cours, cet ensemble est un paquet : un dossier par version du modèle. Il contient tout ce qui permet d’exécuter le modèle et tout ce qui permet de le vérifier.
Description textuelle du diagramme
À gauche se trouve le côté entraînement : le dossier ticket-model/model/ du cours d’apprentissage automatique. Il contient escalation_model.joblib, model_info.json et check_sample.csv. Le fichier data/valid.csv fournit les cas de parité. Une flèche mène à python make_bundle.py --version 1.0.0.
Au milieu se trouve le paquet versionné, bundles/escalation-1.0.0/. Il contient cinq fichiers : model.joblib (le pipeline : prétraitement et modèle), contract.json (variables, ordre, règles, seuil), metadata.json (version, empreintes des données, versions testées), parity_cases.csv (14 billets et leurs scores attendus) et SHA256SUMS (l’empreinte SHA-256 de chaque fichier ci-dessus). En dessous, une boîte en pointillés indique qu’une nouvelle version, comme bundles/escalation-1.1.0/, est un nouveau dossier. Elle indique aussi que 1.0.0 ne change jamais.
À droite se trouve le côté service : escalation-service lit le paquet avec load_bundle au démarrage. Sous le service, une boîte en pointillés contient MODEL_SHA256, 8f233029aa4c.... C’est l’empreinte globale, soit l’empreinte de SHA256SUMS, conservée dans un endroit fiable hors du paquet. Une flèche en pointillés marquée « pinned » (fixée) mène de cette boîte au service.
Chaque fichier a un rôle :
model.joblib: le pipeline ajusté, avec les mêmes octets que le fichierescalation_model.joblibdu cours d’apprentissage automatique. Il calcule les scores des billets exactement comme pendant l’entraînement.contract.json: le contrat de la leçon précédente, écrit pour les personnes et les programmes. Il contient les variables dans l’ordre, le schéma JSON deTicketFeatureset les catégories connues. Il contient aussi les règles pour les valeurs manquantes et le sens de la sortie, avec le seuil.metadata.json: l’origine du modèle. Il consigne la version, la date d’entraînement et le nombre de lignes d’entraînement. Il contient aussi les empreintes des données d’entraînement, les résultats de validation et les versions des bibliothèques ayant créé le modèle.parity_cases.csv: 14 billets fixes avec le score que le modèle a donné à chacun lors de la création du paquet. Toute copie du modèle, où qu’elle soit, doit donner les mêmes scores. Il joue le rôle decheck_sample.csvdu cours d’apprentissage automatique, la photo du gâteau terminé.SHA256SUMSet l’empreinte globale arrivent dans la prochaine leçon : ils prouvent que personne n’a modifié les fichiers.
Omar demande : « Le seuil est dans contract.json. Pourquoi pas dans les paramètres du service? » Parce que le seuil appartient à ce modèle. Un modèle réentraîné donne d’autres scores, et 0,19 peut alors signaler bien plus de 20 % des billets. Le module 5 montre exactement cela avec un vrai modèle candidat. Le seuil accompagne donc le modèle, dans le même dossier.
Les cas de parité
Section intitulée « Les cas de parité »parity_cases.csv contient quatre types de billets, car chacun peut causer un problème différent :
| Type | Nombre | Ce qu’il vérifie |
|---|---|---|
| normal | 6 | De vrais billets de validation sans valeur manquante |
| manquant | 3 | order_value nul, un invité avec customer_tenure_days nul, puis les deux : les médianes du mécanisme d’imputation |
| extrême | 3 | Des valeurs aux limites du contrat : T-900004 a tous les risques au maximum et un score d’exactement 1,0 |
| inconnu | 2 | Un canal jamais vu par le modèle (social), puis une région et une équipe inconnues |
Le module 3 utilise les mêmes 14 cas pour prouver que le service en cours d’exécution donne les scores de l’entraînement.
Une version ne change jamais
Section intitulée « Une version ne change jamais »Une fois que bundles/escalation-1.0.0/ existe, personne ne le modifie. Un modèle réentraîné, un nouveau seuil ou même une faute corrigée dans contract.json crée une nouvelle version dans un nouveau dossier.
Pourquoi cette règle stricte? D’autres éléments font référence à « 1.0.0 » : les paramètres du service, chaque réponse consignée ("model_version": "1.0.0") et le registre des mises en production. Si 1.0.0 pouvait changer, le même nom désignerait deux modèles. Un retour en arrière vers 1.0.0 ne ramènerait alors pas le modèle qui fonctionnait. make_bundle.py applique la règle : il s’arrête si le dossier existe. Le coût est faible : un dossier d’environ 12 ko par version, conservé tant qu’un service ou un registre peut en avoir besoin.
Les versions testées
Section intitulée « Les versions testées »metadata.json consigne les versions de scikit-learn, numpy, pandas et joblib. Dans la prochaine leçon, le service refuse de charger le paquet avec d’autres versions. scikit-learn ne garantit un objet sauvegardé que pour la version qui l’a sauvegardé (Empaqueter les artefacts : quand les versions diffèrent dans le cours d’apprentissage automatique). La version de Python est aussi consignée (3.14.6), mais pas vérifiée. Le conteneur du module 4 utilise Python 3.14.8. Une version corrective de Python ne change pas la façon dont ces bibliothèques calculent.
Écrire le programme de création du paquet
Section intitulée « Écrire le programme de création du paquet »Créez un fichier, escalation/bundle.py. Il définit les noms des fichiers du paquet et les bibliothèques dont les versions doivent correspondre. La prochaine leçon ajoute le code qui charge un paquet.
"""The model bundle: everything the model needs to run, in one folder.
model.joblib the fitted pipeline (preprocessing + model) contract.json the features, their order and rules, and the threshold metadata.json the version, the training data and the tested versions parity_cases.csv fixed tickets and the scores they got in training"""
FILES = ["model.joblib", "contract.json", "metadata.json", "parity_cases.csv"]PACKAGES = ["scikit-learn", "numpy", "pandas", "joblib"]make_bundle.py va dans le dossier du projet, à côté de requirements.txt. La première partie choisit les cas de parité :
"""Make a model bundle from the C05 project's model/ folder.
python make_bundle.py ../ticket-model --version 1.0.0
Reads ../ticket-model/model/ (escalation_model.joblib, model_info.json) and../ticket-model/data/ (for the parity cases), and writesbundles/escalation-<version>/ with the model, its contract, its metadata and theparity cases.
Run it only on a model that you trained yourself, or that comes from a sourceyou trust: it loads the joblib file, and loading can run code."""
import argparseimport jsonimport shutilimport sysfrom importlib.metadata import versionfrom pathlib import Path
import joblibimport pandas as pd
from escalation.bundle import PACKAGESfrom escalation.contract import FEATURES, TicketFeatures
# Fixed tickets that every copy of the model must score the same way.# Normal: real validation tickets. Missing: the two numbers that may be null.# Extreme: values at the edges of the contract. Unknown: a category the# model never saw.EDGE_CASES = [ ("missing", "T-900001", dict(team="account", order_value=None)), ("missing", "T-900002", dict(segment="guest", customer_tenure_days=None, prior_tickets_90d=0, prior_escalations_90d=0)), ("missing", "T-900003", dict(order_value=None, customer_tenure_days=None)), ("extreme", "T-900004", dict(priority=1, order_value=5000, word_count=10000, prior_tickets_90d=500, prior_escalations_90d=500, created_hour=23)), ("extreme", "T-900005", dict(priority=3, order_value=0, word_count=0, customer_tenure_days=0, prior_tickets_90d=0, prior_escalations_90d=0, created_hour=0)), ("extreme", "T-900006", dict(customer_tenure_days=20000, created_hour=0)), ("unknown", "T-900007", dict(channel="social")), ("unknown", "T-900008", dict(region="overseas", team="installation")),]BASE = dict(channel="email", team="payment", segment="home", region="west", priority=2, order_value=120.0, word_count=80, customer_tenure_days=500, prior_tickets_90d=1, created_hour=11, prior_escalations_90d=0)
def read(path): """Read a ticket CSV the way C05 does: only an empty cell is missing.""" return pd.read_csv(path, keep_default_na=False, na_values=[""])
def parity_cases(data: Path) -> pd.DataFrame: valid = read(data / "valid.csv") extra = read(data / "extra_features.csv")[["ticket_id", "prior_escalations_90d"]] valid = valid.merge(extra, on="ticket_id", how="left", validate="one_to_one") normal = valid.dropna(subset=["order_value", "customer_tenure_days"]).head(6) rows = [{"case": "normal", "ticket_id": r.ticket_id, **{f: getattr(r, f) for f in FEATURES}} for r in normal.itertuples()] rows += [{"case": case, "ticket_id": tid, **{**BASE, **change}} for case, tid, change in EDGE_CASES] cases = pd.DataFrame(rows, columns=["case", "ticket_id"] + FEATURES) for name in ["priority", "word_count", "customer_tenure_days", "prior_tickets_90d", "created_hour", "prior_escalations_90d"]: cases[name] = cases[name].astype("Int64") # whole numbers, with an empty cell when missing for row in cases[FEATURES].to_dict("records"): # every case must pass the contract TicketFeatures(**{k: (None if pd.isna(v) else v) for k, v in row.items()}) return casesLes cas limites modifient seulement quelques valeurs d’un billet BASE, pour que chaque cas teste une seule chose. La dernière boucle vérifie chaque cas avec le contrat : un cas de parité que le service rejetterait est inutile.
La deuxième partie, main(), va à la fin du même fichier. Elle écrit les quatre fichiers :
def main(): parser = argparse.ArgumentParser(description="Make a model bundle from a C05 project.") parser.add_argument("project", type=Path, help="the C05 ticket-model folder") parser.add_argument("--version", required=True, help="the model version, for example 1.0.0") parser.add_argument("--model-dir", default="model", help="the folder in the project with the model") args = parser.parse_args()
source = args.project / args.model_dir info = json.loads((source / "model_info.json").read_text(encoding="utf-8")) pipeline = joblib.load(source / "escalation_model.joblib") if list(pipeline.feature_names_in_) != FEATURES: sys.exit("Stopped: the model was trained on other features than this service expects.")
out = Path("bundles") / f"escalation-{args.version}" if out.exists(): sys.exit(f"Stopped: {out} exists. A version is never changed: choose a new version.") out.mkdir(parents=True)
shutil.copyfile(source / "escalation_model.joblib", out / "model.joblib")
cases = parity_cases(args.project / "data") cases["expected_score"] = pipeline.predict_proba(cases[FEATURES])[:, 1] cases.to_csv(out / "parity_cases.csv", index=False)
contract = { "model": "escalation", "model_version": args.version, "unit": "one support ticket, scored when it is created", "features": FEATURES, "schema": TicketFeatures.model_json_schema(), "known_categories": info["features"]["categories"], "missing_values": { "order_value": "null when the ticket has no order; the pipeline fills the training median", "customer_tenure_days": "null for a guest; the pipeline fills the training median", "other features": "never null: the request is rejected", }, "unknown_categories": "allowed; scored as none of the known values; reported as a warning", "output": { "score": "0 to 1; a higher score means a higher risk of escalation; it ranks tickets", "flag": "true if score >= threshold: send the ticket to the senior team", "threshold": info["threshold"], }, } (out / "contract.json").write_text(json.dumps(contract, indent=2) + "\n", encoding="utf-8")
metadata = { "model_version": args.version, "description": info["model"], "trained_at": info["trained_at"], "train_rows": info["train_rows"], "data_sha256": info["data_sha256"], "evaluation": {"on": info.get("evaluated_on", "valid (May 2026)"), **info["valid"]}, "review_capacity": info["review_capacity"], "python": info["versions"]["python"], "versions": {p: version(p) for p in PACKAGES}, } trained = {"scikit-learn": info["versions"]["scikit-learn"], "numpy": info["versions"]["numpy"], "pandas": info["versions"]["pandas"], "joblib": info["versions"]["joblib"]} if trained != metadata["versions"]: sys.exit(f"Stopped: the model was trained with {trained}, but this environment has {metadata['versions']}.") (out / "metadata.json").write_text(json.dumps(metadata, indent=2) + "\n", encoding="utf-8")
print(f"Made {out}: {len(cases)} parity cases, threshold {info['threshold']}.")
if __name__ == "__main__": main()Trois vérifications arrêtent le script avant qu’il écrive un paquet incorrect. Les variables du pipeline doivent correspondre à FEATURES. Le dossier ne doit pas exister. Et les versions des bibliothèques installées doivent correspondre à celles qui ont entraîné le modèle. Les scores attendus sont calculés ici, lors de la création du paquet, avec le pipeline inclus dans le paquet.
Créer le paquet
Section intitulée « Créer le paquet »-
Dans
~/projects/escalation-service, avec.venvactif, créez la version 1.0.0 :Terminal python make_bundle.py ../ticket-model --version 1.0.0La sortie :
Made bundles/escalation-1.0.0: 14 parity cases, threshold 0.19. -
Listez les fichiers.
Fenêtre de terminal ls -l bundles/escalation-1.0.0Fenêtre de terminal Get-ChildItem bundles\escalation-1.0.0La sortie (macOS, abrégée) :
-rw-r--r-- 1 ... 4270 Oct 8 10:35 contract.json-rw-r--r-- 1 ... 879 Oct 8 10:35 metadata.json-rw-r--r-- 1 ... 5354 Oct 8 10:35 model.joblib-rw-r--r-- 1 ... 1317 Oct 8 10:35 parity_cases.csvmodel.joblibcontient 5 354 octets, comme le fichier du cours d’apprentissage automatique. Le paquet entier fait environ 12 ko. -
Exécutez la même commande à nouveau pour voir la règle de version :
Terminal python make_bundle.py ../ticket-model --version 1.0.0La sortie :
Stopped: bundles/escalation-1.0.0 exists. A version is never changed: choose a new version.Le code de sortie est 1, et rien n’a changé dans le dossier.
Ouvrez bundles/escalation-1.0.0/metadata.json. Le vôtre contient votre propre trained_at; le reste correspond :
{ "model_version": "1.0.0", "description": "Larkfield ticket escalation: logistic regression in a scikit-learn pipeline", "trained_at": "2026-10-08 13:53:54 UTC", "train_rows": 16939, "data_sha256": { "train.csv": "63b8732a28e891189cc28a9bcd505a093a9c4c51c01f3d0e1fba6d4959242d53", ... }, "evaluation": { "on": "valid (May 2026)", ... "flag_rate": 0.193, "precision": 0.379, "recall": 0.47 }, "review_capacity": 0.2, "python": "3.14.6", "versions": { "scikit-learn": "1.9.1", "numpy": "2.5.3", "pandas": "3.0.6", "joblib": "1.6.0" }}Une personne qui trouve ce dossier dans un an peut répondre à quatre questions. Quel est ce modèle? Quelles données l’ont entraîné? Quels étaient ses résultats? Quelles bibliothèques permettent de l’exécuter? Sans le dossier, personne ne pourrait y répondre.
À vous de jouer : répondre avec le paquet seulement
Section intitulée « À vous de jouer : répondre avec le paquet seulement »Répondez à trois questions avec les fichiers de bundles/escalation-1.0.0/ seulement, sans utiliser le projet d’entraînement :
- Quel seuil le modèle 1.0.0 utilise-t-il?
- Quel cas de parité a le score attendu le plus élevé, et quel est ce score?
- Quelles valeurs de
channelle modèle connaît-il?
Afficher la réponse
Une façon de faire, dans python :
import jsonimport pandas as pd
contract = json.load(open("bundles/escalation-1.0.0/contract.json"))cases = pd.read_csv("bundles/escalation-1.0.0/parity_cases.csv")print(contract["output"]["threshold"])print(cases.loc[cases["expected_score"].idxmax(), ["case", "ticket_id", "expected_score"]].tolist())print(contract["known_categories"]["channel"])La sortie :
0.19['extreme', 'T-900004', np.float64(1.0)]['chat', 'email', 'phone', 'unknown', 'web_form']np.float64(1.0) est la façon dont numpy affiche le nombre 1,0. La valeur unknown (inconnu) est une vraie catégorie dans les données de Larkfield : le canal n’a pas été consigné. Ce n’est pas une valeur manquante. social n’est pas dans la liste, donc cette valeur déclenche un avertissement.
Erreur courante
Reconstruire le prétraitement au moment du calcul des scores
Symptôme : Tomás garde seulement la régression logistique et reconstruit le prétraitement avec les mêmes paramètres. Il l’ajuste sur les billets dont il doit calculer les scores. Sur tout le mois de juillet (1 400 billets), le calcul échoue :
ValueError: X has 29 features, but LogisticRegression is expecting 28 features as input.Il retire les 91 billets du nouveau canal social et réessaie. Cette fois, le calcul fonctionne sans erreur, mais en comparaison avec le vrai pipeline :
prep refitted on July without social (1309 rows): largest difference 0.068, median 0.007; flagged 0.177 vs 0.199; flag changed for 29 ticketsCause : le prétraitement reconstruit a appris les médianes, les moyennes et les catégories de juillet, pas celles de l’entraînement. Une nouvelle catégorie ajoute une colonne, donc les dimensions ne correspondent pas. Sans nouvelle catégorie, les dimensions correspondent, mais chaque nombre change un peu : 29 billets ont changé de signalement, sans avertissement.
Solution : ne reconstruisez jamais ce que fit a appris. Empaquetez tout le pipeline ajusté et vérifiez chaque copie avec les cas de parité. Une différence de 0,068 dépasse immédiatement une tolérance de 1e-9.
Pour un vrai service d’IA : l’échec visible est le cas chanceux. L’échec silencieux change les décisions pour de vrais clients. C’est pourquoi un paquet contient ensemble le pipeline et les cas de parité.
Vérifiez votre compréhension
Tomás dit : la régression logistique est le modèle, donc model.joblib peut contenir seulement ça, et le service peut préparer les données. Donnez deux raisons pour lesquelles le paquet contient tout le pipeline.
Afficher la réponse
D’abord, le modèle a appris à partir de 28 nombres produits par le prétraitement ajusté. Ce sont des colonnes de codage un à un, des nombres imputés et mis à l’échelle, et des indicateurs de valeurs manquantes. Seul, il ne peut pas lire un billet : could not convert string to float: 'email' (impossible de convertir la chaîne « email » en nombre à virgule flottante). Ensuite, un prétraitement reconstruit au moment du calcul apprend d’autres médianes, échelles et catégories. Soit il échoue (29 variables au lieu de 28), soit il change les scores silencieusement. En juillet, 29 signalements ont changé sans erreur.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Priya trouve une faute dans la description de contract.json du paquet 1.0.0, qui est en production. Devrait-elle corriger le fichier? Que devrait-elle faire?
Afficher la réponse
Non. Une version de paquet ne change jamais. Les paramètres, les réponses consignées et le registre des mises en production font tous référence à « 1.0.0 ». Un retour en arrière doit ramener exactement ce paquet. Elle crée une nouvelle version, par exemple 1.0.1, dans un nouveau dossier, et la met en production comme toute autre version.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Pourquoi le seuil de 0,19 accompagne-t-il le paquet au lieu de figurer dans les paramètres du service?
Afficher la réponse
Le seuil a été choisi pour les scores de ce modèle, afin de signaler environ 20 % des billets. Un modèle réentraîné peut donner des scores plus élevés ou plus faibles. L’ancien seuil peut alors signaler beaucoup plus ou beaucoup moins de billets. Le seuil appartient donc à la version du modèle, et chaque nouvelle version apporte le sien.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
- Un modèle sans son prétraitement ajusté ne peut pas calculer le score d’un billet. Reconstruire le prétraitement échoue ou change les scores silencieusement.
- Un paquet est un dossier par version du modèle. Il contient le pipeline, le contrat avec le seuil, les métadonnées avec les versions testées et 14 cas de parité.
- Une version ne change jamais. Une correction crée une nouvelle version dans un nouveau dossier, et
make_bundle.pyrefuse d’écraser le dossier existant. bundles/escalation-1.0.0/existe maintenant, avec quatre fichiers.
Ensuite, vous prouvez que personne n’a modifié le paquet. Vous ajoutez une empreinte par fichier et une empreinte globale pour tout le paquet. Un chargeur vérifie les deux avant d’exécuter quoi que ce soit.
Leçon suivante : Confiance et intégrité des artefacts