Aller au contenu
English

Construire et tester

  • Leçon 5
  • 45 min
  • Module 2 : Créer un pipeline CI reproductible
Leçon 5 sur 15 · CI/CD, tests et mises en production sûresConstruire et tester

Ce que vous saurez faire

Une exécution verte doit signifier quelque chose : les tests ont réussi, les paquets n’ont aucune vulnérabilité connue et l’image démarre et répond. Construisez cette image une seule fois, testez-la et conservez exactement celle-là.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 45 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

Exécuté avec GitHub Actions (exécuteurs ubuntu-24.04) dans le dépôt public Nextia-Intelligence/ticket-api-ci, et Docker Desktop 4.81.0 sous macOS (Apple silicon). Les étapes pour Windows et Linux n’ont pas été testées.

La tâche installe les paquets depuis requirements-lock.txt, et non depuis requirements.txt :

.github/workflows/ci.yml (part of the checks job)
- name: Install the exact versions
run: python -m pip install -r requirements-lock.txt

requirements.txt indique ce dont le projet a besoin. Le fichier de verrouillage indique la version exacte de chaque paquet, y compris les paquets dont ceux-ci dépendent. Le journal de l’exécution verte énumère ce qui a été installé (extrait abrégé) :

Successfully installed CacheControl-0.14.4 ... fastapi-0.142.2 ... pip_audit-2.10.1 ... pytest-9.1.1 ... ruff-0.16.10 ... uvicorn-0.54.0 ...

Chaque exécution installe ces mêmes versions jusqu’à ce que quelqu’un modifie le fichier de verrouillage dans un commit (enregistrement des changements). Une exécution rouge indique donc que le code ou le fichier de verrouillage a changé. Elle ne signale jamais qu’un paquet a publié une nouvelle version pendant la nuit. La nouvelle version arrive seulement quand vous la choisissez, dans un commit que le pipeline vérifie.

Une vérification est utile quand elle peut échouer pour une raison importante pour les utilisateurs. L’analyse statique a trouvé deux vrais problèmes dans Configurer l’exécuteur. Ajoutez maintenant deux autres vérifications à la première tâche, après Lint (analyse statique) :

.github/workflows/ci.yml (checks job, add after Lint)
- name: Unit and contract tests
run: python -m pytest --junitxml=reports/junit.xml
- name: Security - known vulnerabilities in the image's packages
run: pip-audit -r requirements-run.txt --progress-spinner off

Tests. pytest exécute les 25 tests des cours sur les API et sur Docker. Ils testent les points de terminaison, les entrées non valides et le classificateur qui échoue. Un test de contrat compare l’API à contract/openapi.json. --junitxml=reports/junit.xml écrit aussi les résultats dans un fichier au format JUnit XML, que de nombreux outils peuvent lire. Publier les preuves conserve ce fichier. Ajoutez reports/ à .gitignore, car ce fichier est une sortie, pas du code source.

Vérification de sécurité. pip-audit compare les paquets à une base de données publique de vulnérabilités connues. Il lit requirements-run.txt, les paquets que l’image installe, car ce sont ceux que les utilisateurs rencontrent. Les outils de développement, comme pytest, ne vont jamais dans l’image.

La sortie des deux étapes de l’exécution verte :

collected 25 items
tests/test_classify.py ............... [ 60%]
tests/test_contract.py .. [ 68%]
tests/test_history.py ........ [100%]
- generated xml file: /home/runner/work/ticket-api-ci/ticket-api-ci/reports/junit.xml -
============================== 25 passed in 0.42s ==============================
No known vulnerabilities found

Connaissez aussi les limites. pip-audit voit seulement les paquets Python, pas les paquets Debian de l’image de base. Il trouve seulement les vulnérabilités déjà connues et publiées. Une réussite signifie « rien de connu aujourd’hui ». Exécutez-le donc à chaque changement.

Construire une seule fois, puis conserver cette construction

Section intitulée « Construire une seule fois, puis conserver cette construction »

Les vérifications testent le code. Les utilisateurs reçoivent une image. Le pipeline doit donc aussi tester l’image, puis livrer cette image, pas une nouvelle.

Pourquoi ne pas reconstruire plus tard, par exemple une fois pour la préproduction et une fois pour la production? Amira fait le test sur son Mac. Elle construit deux fois le même commit, sans cache :

Terminal
docker build -q --no-cache -t c19m02-ticket-api:a .
docker build -q --no-cache -t c19m02-ticket-api:b .

La sortie :

sha256:9f93ccc6caaab583c4b1b9dc8e4e892f86ee0d7e7bd0ca6b5bea94904e833618
sha256:3a4656c8cadaab5acd3a8509580d9005626ac47a8f1385c6282c9645fd514256

Même commit, même Dockerfile, deux images différentes. Chaque construction enregistre son heure. Une construction sans cache peut aussi télécharger un paquet ou une image de base plus récente. Une deuxième construction produit un nouvel artefact que personne n’a testé.

Deux flux. Construire une seule fois, ce que fait ci.yml : commit c862935, puis une seule construction, testée sur l'exécuteur pour vérifier son bon fonctionnement. Ensuite, l'image sha256:6c9bb4ed dans GHCR passe en préproduction et en production. Construire pour chaque environnement, l'erreur : le commit c862935 est construit pour la préproduction et donne l'image 9f93ccc6. Il est reconstruit pour la production et donne l'image 3a4656c8, qui n'a jamais été testée.
Construisez une seule fois et promouvez l'empreinte. Les deux identifiants du flux inférieur proviennent de deux vraies constructions locales du même commit.
Description textuelle du diagramme

Dans le flux supérieur, une seule construction est testée et poussée. Chaque environnement suivant exécute exactement cette empreinte. Dans le flux inférieur, chaque environnement reçoit sa propre construction. L’image de production n’a jamais été testée. Personne ne peut prouver qu’elle est identique à l’image de préproduction, car son identifiant diffère. Les identifiants du bas proviennent des deux constructions locales ci-dessus. L’empreinte du haut correspond à la vraie image poussée par le pipeline de référence. Le module 3 la promeut.

La deuxième tâche construit l’image, la teste sur l’exécuteur et la pousse vers le GitHub Container Registry (GHCR, ghcr.io). Elle s’exécute seulement après la réussite des vérifications :

.github/workflows/ci.yml (add under jobs:)
image:
name: Build, smoke-test and push the image
needs: checks
runs-on: ubuntu-24.04
timeout-minutes: 15
permissions:
contents: read
packages: write
env:
IMAGE: ghcr.io/${{ github.repository }}
PUSH: ${{ github.event_name != 'pull_request' }}
steps:
- uses: actions/checkout@v7
- uses: docker/setup-buildx-action@v4
- name: Read the version from pyproject.toml
id: version
run: python3 -c "import tomllib; print('version=' + tomllib.load(open('pyproject.toml', 'rb'))['project']['version'])" >> "$GITHUB_OUTPUT"
- name: Image names and labels
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.IMAGE }}
tags: |
type=sha,prefix=sha-
type=semver,pattern={{version}}
- name: Build (and keep it here for the smoke test)
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64
load: true
tags: ticket-api:candidate
build-args: |
VERSION=${{ steps.version.outputs.version }}
REVISION=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Smoke-test the image before it leaves this runner
run: |
docker run -d --name candidate -p 127.0.0.1:8000:8000 -e API_KEY=ci-smoke-key ticket-api:candidate
for _ in $(seq 1 30); do curl -fs http://127.0.0.1:8000/health && break; sleep 1; done
scripts/smoke.sh http://127.0.0.1:8000 ci-smoke-key
- name: Sign in to GitHub Container Registry
if: env.PUSH == 'true'
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push the same build
if: env.PUSH == 'true'
id: push
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
VERSION=${{ steps.version.outputs.version }}
REVISION=${{ github.sha }}
cache-from: type=gha

Les parties, depuis le haut :

  • needs: checks : la tâche d’image attend la tâche dont l’identifiant est checks. Si les vérifications échouent, GitHub saute cette tâche, comme dans la première exécution rouge.
  • permissions : seule cette tâche peut écrire des paquets. Son GITHUB_TOKEN peut donc pousser vers GHCR. La tâche de vérification garde seulement contents: read.
  • PUSH vaut true pour chaque déclencheur sauf une demande de tirage (pull request). Une demande de tirage construit l’image et vérifie son bon fonctionnement, mais ne pousse rien. Une branche ne peut donc jamais placer d’image dans le registre. Les lignes if: env.PUSH == 'true' sautent les dernières étapes pour une demande de tirage.
  • La version vient de pyproject.toml (1.1.0). Elle a donc une seule source.
  • docker/metadata-action crée les noms d’images. type=sha,prefix=sha- donne l’étiquette sha- suivie des 7 premiers caractères du commit, comme sha-c862935. L’étiquette indique le commit, donc elle ne pointe jamais vers un autre code. type=semver ajoute une étiquette comme 1.1.0 seulement quand le déclencheur est une étiquette de version v1.1.0. L’étape crée aussi des étiquettes de métadonnées (des renseignements dans l’image). Publier les preuves les lit.
  • La première construction produit linux/amd64, le type de processeur du nuage (voir le cours sur Docker). load: true conserve l’image dans Docker sur l’exécuteur sous le nom ticket-api:candidate. C’est une version candidate : une image qui peut devenir une version publiée si elle réussit toutes les vérifications. cache-to: type=gha enregistre les couches dans le cache de GitHub Actions. Le Dockerfile n’utilise pas encore VERSION et REVISION. Les arguments de construction sont là pour plus tard.
  • La vérification de bon fonctionnement démarre la version candidate sur l’exécuteur et lance scripts/smoke.sh contre elle.
  • L’étape de poussée reconstruit l’image avec cache-from: type=gha. Les étapes du Dockerfile sont identiques, donc chaque étape vient du cache. BuildKit ne fait qu’emballer et envoyer les mêmes couches.

scripts/smoke.sh reprend la vérification de bon fonctionnement du cours sur les API. Ce script termine avec le code de sortie 1 si une vérification échoue :

scripts/smoke.sh
#!/usr/bin/env bash
# Smoke check of a running ticket API: the main paths, and one failure path.
# scripts/smoke.sh <base URL> <API key>
# Exits with 1 when a check fails. Prints no secret.
set -uo pipefail
url=${1:?Give the base URL, for example https://ticket-api-staging...}
key=${2:?Give the API key}
failed=0
check() { # name, expected status, curl arguments...
local name=$1 expected=$2; shift 2
local status
status=$(curl -s -o /dev/null -w '%{http_code}' --max-time 30 "$@")
if [ "$status" = "$expected" ]; then
echo "ok $name ($status)"
else
echo "FAIL $name: expected $expected, got $status"
failed=1
fi
}
check "health" 200 "$url/health"
check "ready" 200 "$url/ready"
check "classify with the key" 200 -X POST "$url/v1/classify" \
-H 'Content-Type: application/json' -H "X-API-Key: $key" -d '{"subject":"Parcel lost"}'
check "classify without a key" 401 -X POST "$url/v1/classify" \
-H 'Content-Type: application/json' -d '{"subject":"Parcel lost"}'
exit $failed

Rendez-le exécutable avant de créer son commit : chmod +x scripts/smoke.sh. Git conserve cette permission, et l’exécuteur en a besoin.

Le fichier complet à cette étape se trouve dans le dépôt de référence : ci.yml à c862935. Il contient trois étapes de plus, que la prochaine leçon ajoute.

Un déclencheur (pull_request, ou une poussée vers main, hotfix/** ou une étiquette v*) démarre la tâche 1, analyse statique, tests et audit, sur une nouvelle machine virtuelle : installation du fichier de verrouillage, ruff check, pytest avec --junitxml, pip-audit. La tâche 1 téléverse toujours l'artefact reports avec junit.xml. Si une vérification échoue, l'exécution est rouge et la tâche 2 est sautée. Si tout réussit, needs : checks démarre la tâche 2, construction, test de bon fonctionnement et poussée de l'image, sur une autre nouvelle machine virtuelle : construction linux/amd64 et test de bon fonctionnement sur l'exécuteur. Puis une décision : est-ce une demande de tirage? Si oui, arrêt, rien n'est poussé. Sinon, poussée de la même construction avec l'étiquette sha-c862935, attestation de provenance et écriture de build-info.json. La poussée envoie l'image vers GHCR, où son empreinte permet de la trouver, et build-info.json devient l'artefact build-info.
Les deux tâches, leur ordre et ce qui quitte les exécuteurs : l'artefact reports, l'image dans GHCR et l'artefact build-info. Une demande de tirage s'arrête avant la poussée.
Description textuelle du diagramme

La tâche 1 exécute les quatre vérifications. Quoi qu’il arrive, elle téléverse ensuite le rapport de test comme artefact reports. Si une vérification échoue, l’exécution est rouge et la tâche 2 ne démarre jamais. Si tout réussit, la tâche 2 construit l’image et vérifie son bon fonctionnement sur son exécuteur. Pour une demande de tirage, l’exécution s’arrête là, et rien ne va dans le registre. Pour une poussée vers main, une branche de correctif urgent ou une étiquette de version, la tâche 2 pousse la même construction vers GHCR. Elle enregistre ensuite la provenance de l’image et téléverse build-info.json comme artefact build-info. Cette leçon ajoute tout sauf les deux artefacts et la provenance. Publier les preuves les ajoute.

L’exécution de référence 37663364319 montre le même graphe :

La page de l'exécution CI 2, Fix the two lint findings (corriger les deux problèmes d'analyse statique), affiche le statut Success (réussite), une durée totale de 1 minute 58 secondes et 4 artefacts. Une poussée du commit c862935 vers main l'a déclenchée. Le graphe des tâches de ci.yml pour push montre Lint, test, audit (analyse statique, tests et audit), en vert, 44 secondes, reliée à Build, smoke-test and push the image (construire, tester le bon fonctionnement et pousser l'image), en vert, 1 minute 5 secondes. Le tableau Artifacts (artefacts) contient build-info (362 octets), reports (773 octets) et deux fichiers qui se terminent par .dockerbuild (41,6 Ko et 34,6 Ko), chacun avec une empreinte sha256.
L'exécution verte dans le dépôt de référence : vérifications en 44 secondes, puis tâche d'image en 1 minute 5 secondes. Les deux artefacts .dockerbuild sont des rapports de construction que docker/build-push-action téléverse automatiquement. L’image est en anglais.

Le journal de la tâche d’image montre chaque partie. La première construction a exécuté chaque étape, car le cache était encore vide (extrait abrégé) :

#11 [5/7] RUN pip install -r requirements-run.txt
#11 DONE 10.4s
#13 [7/7] COPY ticket_api ./ticket_api
#13 DONE 0.0s
#16 exporting to GitHub Actions Cache
#16 DONE 10.3s

La vérification de bon fonctionnement sur l’exécuteur :

{"status":"ok"}ok health (200)
ok ready (200)
ok classify with the key (200)
ok classify without a key (401)

La première ligne semble étrange. Dans la boucle d’attente, curl -fs …/health a affiché le corps {"status":"ok"} sans saut de ligne. La première ligne ok (réussite) le suit donc directement. C’est sans conséquence. Le dépôt de référence a ensuite ajouté -o /dev/null à ce curl pour ne pas afficher le corps.

Ensuite, la poussée. Chaque étape venait du cache, et la poussée a envoyé les couches testées par la vérification de bon fonctionnement :

#7 [2/7] RUN useradd --create-home --uid 10001 app
#7 CACHED
...
#10 [5/7] RUN pip install -r requirements-run.txt
#10 CACHED
...
#12 [7/7] COPY ticket_api ./ticket_api
#12 CACHED
#14 pushing manifest for ghcr.io/usmanahmed99/ticket-api-ci:sha-c862935@sha256:6c9bb4ed7c7cffc27f0c5489ba867d4d5db16def1c59c8bcbaeb3d26c45ce2e1

sha256:6c9bb4ed… est l’empreinte de ce que le registre contient maintenant. Le module 3 déploie exactement cette empreinte. Un détail à préciser : l’image poussée contient aussi des étiquettes de métadonnées, contrairement à la copie testée. L’empreinte poussée n’est donc pas identique à l’identifiant de la copie testée. Le code, les paquets et chaque couche sont identiques : chaque étape indique CACHED (en cache).

Déclencheur Vérifications Construction et vérification de bon fonctionnement Poussée Étiquettes d’image
Demande de tirage, de n’importe quelle branche vers main oui oui non aucune
Poussée vers main oui oui oui sha-<7>
Poussée vers hotfix/1.2.1 oui oui oui sha-<7>
Poussée de l’étiquette v1.1.0 oui oui oui sha-<7> et 1.1.0

Dans la dernière ligne, l’étiquette 1.1.0 pointe vers une nouvelle construction d’un ancien commit. Le module 3 retire ce déclencheur et étiquette l’image que main a déjà construite. La première demande de tirage du dépôt de référence (exécution 37664331105) prouve la première ligne. Sa tâche d’image a pris 32 secondes, avec chaque étape du Dockerfile à CACHED et aucune poussée.

Attendre 2 minutes pour que l’exécuteur trouve une erreur d’analyse statique, c’est long. scripts/check.sh fait les mêmes vérifications, dans le même ordre, sur votre ordinateur :

scripts/check.sh
#!/usr/bin/env bash
# Run the same checks as the CI pipeline, on your computer, in the same order.
# Use it before you push. Needs the virtual environment (requirements-lock.txt)
# and Docker. Database tests run only when TEST_DATABASE_URL is set.
set -euo pipefail
step() { printf '\n== %s\n' "$1"; }
step "Lint"
ruff check .
step "Tests"
python -m pytest -q --junitxml=reports/junit.xml
step "Security: known vulnerabilities in the image's packages"
pip-audit -r requirements-run.txt --progress-spinner off
step "Build the image"
docker build -q -t ticket-api:check . >/dev/null
step "Smoke check of the image"
docker run -d --rm --name ticket-api-check -p 127.0.0.1:8099:8000 -e API_KEY=check-key ticket-api:check >/dev/null
trap 'docker stop ticket-api-check >/dev/null' EXIT
for _ in $(seq 1 20); do curl -fs http://127.0.0.1:8099/health >/dev/null && break; sleep 0.5; done
scripts/smoke.sh http://127.0.0.1:8099 check-key
printf '\nAll checks passed.\n'

set -e arrête le script au premier échec, comme une étape échouée arrête une tâche. L’image utilise le port 8099, pour éviter un conflit avec une API sur le port 8000. La ligne trap arrête le conteneur à la fin du script, même après un échec. Le script construit pour le processeur de votre ordinateur (sur un Mac avec Apple silicon : arm64). Cela suffit pour détecter une image défectueuse avant de pousser. L’exécuteur construit linux/amd64. (Le commentaire mentionne les tests de base de données, qui arrivent au module 4.)

Terminal, with .venv active
chmod +x scripts/check.sh
scripts/check.sh

La sortie du Mac de test. (Le test utilisait les noms c19m02-ticket-api:check et c19m02-ticket-api-check.)

== Lint
All checks passed!
== Tests
......................... [100%]
25 passed in 0.25s
== Security: known vulnerabilities in the image's packages
No known vulnerabilities found
== Build the image
== Smoke check of the image
ok health (200)
ok ready (200)
ok classify with the key (200)
ok classify without a key (401)
All checks passed.

Cela a pris 14 secondes, avec les couches de l’image déjà dans le cache local.

Exercice guidé : ajouter les vérifications et la tâche d’image

Section intitulée « Exercice guidé : ajouter les vérifications et la tâche d’image »
  1. Ajoutez les deux étapes Unit and contract tests (tests unitaires et de contrat) et Security (sécurité) à la tâche de vérification.
  2. Ajoutez reports/ à .gitignore.
  3. Ajoutez scripts/smoke.sh et scripts/check.sh, puis rendez les deux exécutables avec chmod +x.
  4. Exécutez scripts/check.sh. Continuez seulement quand il affiche All checks passed. (toutes les vérifications ont réussi).
  5. Ajoutez la tâche image.
  6. Vérifiez le fichier avec actionlint.
  7. Créez un commit et poussez vers main.
  8. Ouvrez l’exécution. Vérifiez que la tâche d’image a démarré seulement après la tâche de vérification.
  9. Dans le journal de la tâche d’image, trouvez les lignes de vérification de bon fonctionnement, les lignes CACHED de la poussée et l’empreinte.
  10. Ouvrez la page du paquet, https://github.com/<you>/ticket-api-ci/pkgs/container/ticket-api-ci. Trouvez l’étiquette sha- suivie des 7 premiers caractères de votre commit.
Afficher le résultat attendu

scripts/check.sh termine avec All checks passed. Sur GitHub, l’exécution contient deux tâches vertes. Lint, test, audit prend environ 45 secondes. Build, smoke-test and push the image prend environ une minute. L’étape de test indique 25 passed (25 tests réussis), et l’étape de sécurité indique No known vulnerabilities found (aucune vulnérabilité connue trouvée). La vérification de bon fonctionnement affiche quatre lignes ok. La poussée indique CACHED pour les six étapes du Dockerfile après FROM. Elle affiche ensuite pushing manifest for ghcr.io/<you>/ticket-api-ci:sha-…@sha256:… (poussée du manifeste de l’image). La page du paquet montre l’étiquette sha-….

La documentation de GitHub indique qu’un nouveau paquet est privé par défaut. Mais un paquet publié par un flux de travail avec GITHUB_TOKEN reçoit la visibilité de son dépôt. Dans le dépôt de référence, le paquet était immédiatement public : une requête anonyme pour le manifeste a reçu 200. Vérifiez le vôtre. S’il indique Private (privé), ouvrez le paquet, puis Package settings (paramètres du paquet) → Change visibility (changer la visibilité). Cette option est en bas, sous « Danger Zone » (zone de danger). Un paquet public ne peut plus redevenir privé.

À vous de jouer : faites échouer une vérification et trouvez-la en une seconde

Section intitulée « À vous de jouer : faites échouer une vérification et trouvez-la en une seconde »

Faites ceci sur votre ordinateur seulement. Dans ticket_api/config.py, retirez encore from None de la ligne 28. La ligne sera comme lors de la première exécution en échec. Prédisez : quelles étapes scripts/check.sh exécute-t-il, combien de temps prend-il et quel est son code de sortie? Exécutez-le. Annulez ensuite le changement avec git restore ticket_api/config.py.

Afficher la réponse

Il exécute seulement Lint (analyse statique). La sortie :

== Lint
B904 Within an `except` clause, raise exceptions with `raise ... from err` or `raise ... from None` to distinguish them from errors in exception handling
--> ticket_api/config.py:28:13
...
Found 1 error.

Le code de sortie est 1, et l’exécution a pris moins d’une seconde sur le Mac de test. set -e a arrêté le script au premier échec. Aucun test n’a donc été exécuté et aucune image n’a été construite. L’exécuteur fait la même chose : la tâche de l’image est ignorée. Vous avez trouvé en une seconde ce que l’exécuteur a trouvé en 25 secondes, sans envoi au dépôt. C’est la première moitié de la vérification de fin de ce module : reproduire une vérification en échec sur votre ordinateur.

Erreur courante

Le test de bon fonctionnement réussit dans le cas normal, et l'API est ouverte

Symptôme : quelqu’un raccourcit l’étape de test de bon fonctionnement et démarre l’image candidate sans -e API_KEY=ci-smoke-key. L’image démarre, /health répond et la classification fonctionne. Le Mac de test a exécuté l’image ainsi et appelé scripts/smoke.sh :

Terminal
docker run -d --name c19m02-candidate -p 127.0.0.1:8098:8000 c19m02-ticket-api:a
scripts/smoke.sh http://127.0.0.1:8098 ci-smoke-key

La sortie :

ok health (200)
ok ready (200)
ok classify with the key (200)
FAIL classify without a key: expected 401, got 200

Le code de sortie était 1. Dans le pipeline CI, cette étape échoue, et les étapes d’envoi ne s’exécutent jamais.

Cause : sans API_KEY ni REQUIRE_API_KEY, l’API de billets démarre sans restriction, comme dans le cours L’essentiel de Docker et du nuage pour les développeurs en IA. Toute requête permet une classification, avec ou sans clé. Trois vérifications ont réussi. Seule la vérification qui doit échouer a révélé le problème.

Solution : démarrez l’image candidate comme en production : avec une clé. Dans le nuage, REQUIRE_API_KEY=true empêche l’application de démarrer si la clé manque, comme dans le cours L’essentiel de Docker et du nuage pour les développeurs en IA. Gardez le cas d’échec dans le test de bon fonctionnement. Supprimez ensuite le conteneur : docker rm -f c19m02-candidate.

Pour un vrai service d’IA : une API de modèle qui répond sans clé coûte de l’argent à chaque requête et peut divulguer des données. Un test de bon fonctionnement qui teste seulement ce qui devrait fonctionner ne peut pas détecter cela. Chaque test de bon fonctionnement exige au moins une requête qui doit être refusée.

Vérifiez votre compréhension

La tâche de l'image construit deux fois : d'abord avec load : true, puis avec push : true. Pourquoi n'est-ce pas l'erreur de reconstruction du diagramme?

Afficher la réponse

La deuxième construction utilise le même commit (enregistrement des changements), le même Dockerfile et le cache des couches de la première (cache-from: type=gha). Chaque étape affichait CACHED (en cache), donc aucune étape n’a été exécutée de nouveau. BuildKit a assemblé les mêmes couches que le test de bon fonctionnement avait testées et les a envoyées à GHCR. L’erreur est une deuxième construction qui exécute les étapes de nouveau, par exemple un autre jour ou pour un autre environnement. Les paquets ou l’image de base peuvent alors différer, et le résultat est une image que personne n’a testée. Ici, la seule différence concerne les étiquettes que porte l’image envoyée.

Vérifiez votre compréhension

Prédisez : Sam ouvre une demande de tirage de feature/x vers main. Quelles tâches et étapes s'exécutent, et que contient GHCR ensuite?

Afficher la réponse

Les deux tâches s’exécutent. La tâche de vérification exécute l’analyse statique, les tests et pip-audit. Si ces vérifications réussissent, la tâche de l’image construit linux/amd64 et teste le bon fonctionnement de l’image candidate sur son exécuteur. Ensuite, PUSH vaut false (faux), car le déclencheur est pull_request (demande de tirage). Les étapes de connexion et d’envoi sont donc ignorées. GHCR ne contient rien de nouveau. L’image est envoyée seulement après l’arrivée du changement dans main. L’envoi vers main exécute alors le pipeline de nouveau sur le code fusionné.

Vérifiez votre compréhension

Exercice pratique : montrez une exécution réussie de votre pipeline avec deux tâches. Où trouvez-vous l'empreinte de votre image, et quelle étiquette indique son commit?

Afficher la réponse

L’exécution présente Lint, test, audit (analyse statique, tests, audit), puis Build, smoke-test and push the image (construction, test de bon fonctionnement et envoi de l’image), reliées dans le graphique. L’étape Push the same build (envoyer la même construction) de la tâche de l’image contient la ligne pushing manifest for ghcr.io/<you>/ticket-api-ci:sha-<7>@sha256:…, avec l’empreinte. La sortie Digest (empreinte) de l’étape l’affiche de nouveau. L’étiquette sha- suivie des 7 premiers caractères du commit indique le commit. La page du paquet de votre dépôt affiche la même étiquette.

Récapitulatif

  • Installez à partir du fichier de verrouillage. Une exécution en échec indique alors que votre changement a brisé quelque chose, pas qu’un paquet a changé pendant la nuit.
  • Vérifications utiles : l’analyse statique, les tests (avec un rapport JUnit) et pip-audit sur les paquets de l’image. Sachez ce que chacune ne peut pas détecter.
  • Construisez une seule fois. Testez le bon fonctionnement de l’image candidate sur l’exécuteur, puis envoyez cette même construction avec l’étiquette sha-<7>. Une deuxième construction est un nouvel artefact non testé.
  • Une demande de tirage construit et teste, mais n’envoie jamais l’image.
  • scripts/check.sh exécute les mêmes vérifications sur votre ordinateur avant votre envoi au dépôt.

Ensuite, conservez les preuves. Ce sont le rapport de test, un relevé du commit et de l’exécution ayant produit chaque empreinte, et une preuve signée de la source de l’image. Protégez ensuite main avec un ensemble de règles.

Leçon suivante : Publier les preuves