122 lines
30 KiB
JavaScript
122 lines
30 KiB
JavaScript
// Cours complets · DevOps lot 2 : Git, CI/CD
|
||
(function () {
|
||
var reg = function (c) {
|
||
(window.CONTENUS_PRO = window.CONTENUS_PRO || []).unshift(c.cours);
|
||
(window.CONTENUS_PLUS = window.CONTENUS_PLUS || {})[c.cours.id] = c.plus;
|
||
(window.CONTENUS_EXEMPLES = window.CONTENUS_EXEMPLES || {})[c.cours.id] = c.exemples;
|
||
(window.CONTENUS_SCHEMAS = window.CONTENUS_SCHEMAS || {})[c.cours.id] = { termes: c.termes, figs: [] };
|
||
};
|
||
|
||
reg({
|
||
cours: { id: 'git', match: /\bGit\b|versionn|gestion de versions/i,
|
||
titre: 'Git : versionner, collaborer et revenir en arrière', duree: '',
|
||
niveau: 'NSI Terminale · BTS SIO SLAM / SISR · BUT Informatique / R&T · Master',
|
||
prerequis: ['Le terminal Linux'],
|
||
competences: ['BTS SIO · Bloc 1 · Travailler en mode projet', 'BTS SIO SLAM · Bloc 2 · Gérer les versions d’une solution applicative', 'BUT Info · Compétence « Collaborer au sein d’une équipe informatique »'],
|
||
intro: 'Un fichier « rapport_final_v3_corrigé_VRAIMENT_final.docx » résume le problème que Git résout. Dès que plusieurs personnes modifient le même code, ou qu’une seule personne veut pouvoir revenir à la version d’hier, il faut un historique fiable. Git enregistre chaque état du projet, permet de travailler en parallèle sur des branches et de fusionner le travail de chacun. C’est l’outil commun à tous les développeurs et, de plus en plus, aux administrateurs qui versionnent leurs configurations, leurs playbooks et leurs fichiers Compose.',
|
||
cm: [
|
||
{ t: 'Le modèle de Git : instantanés et empreintes', d: '', html: 'Git ne stocke pas des différences mais des <b>instantanés</b> complets du projet. Chaque fichier, chaque dossier et chaque <b>commit</b> est un objet identifié par l’empreinte de son contenu (SHA-1, en transition vers SHA-256). Deux fichiers identiques ne sont stockés qu’une fois, et toute modification de l’historique change les empreintes : l’intégrité est vérifiable.<br><br>Un commit contient un instantané, un auteur, une date, un message et un lien vers son ou ses <b>parents</b>. L’historique est donc un graphe orienté de commits. Une <b>branche</b> n’est qu’un pointeur mobile vers un commit, et <b>HEAD</b> désigne la branche ou le commit sur lequel on travaille.' },
|
||
{ t: 'Les trois zones', d: '', html: 'Un fichier passe par trois zones. Le <b>répertoire de travail</b> contient les fichiers tels qu’on les édite. L’<b>index</b> (staging area) contient ce qui sera enregistré au prochain commit : on y ajoute les modifications avec <code>git add</code>. Le <b>dépôt</b> (.git) contient l’historique, et <code>git commit</code> y enregistre l’index.<br><br>Cette étape intermédiaire permet de faire des commits cohérents : on n’ajoute que les modifications qui vont ensemble, même si on a travaillé sur plusieurs sujets à la fois. <code>git status</code> montre à tout moment l’état des trois zones.', exemple: 'git init\ngit add index.html style.css\ngit commit -m "Page d’accueil du TP"\ngit status\ngit log --oneline --graph' },
|
||
{ t: 'Branches et fusions', d: '', html: 'On crée une branche pour chaque fonctionnalité ou correction (<code>git switch -c correctif-login</code>). La branche principale (main) reste stable. Quand le travail est terminé, on le <b>fusionne</b>. Si main n’a pas bougé, Git avance simplement le pointeur (<b>fast-forward</b>). Sinon, il crée un <b>commit de fusion</b> qui a deux parents.<br><br>Si les deux branches ont modifié les mêmes lignes, Git ne peut pas choisir : c’est un <b>conflit</b>. Il marque les zones concernées dans le fichier (<code><<<<<<<</code>, <code>=======</code>, <code>>>>>>>></code>). On édite pour garder la bonne version, on ajoute le fichier et on termine le commit.<br><br>Le <b>rebase</b> est l’alternative à la fusion : il rejoue les commits d’une branche au-dessus d’une autre, pour un historique linéaire. Il réécrit les commits, donc on ne rebase jamais une branche déjà partagée.' },
|
||
{ t: 'Travailler avec un dépôt distant', d: '', html: 'Un <b>dépôt distant</b> (GitHub, GitLab, Forgejo auto-hébergé) sert de point de rencontre. <code>git clone</code> copie un dépôt, <code>git fetch</code> récupère les nouveautés sans les appliquer, <code>git pull</code> récupère et fusionne, <code>git push</code> publie ses commits. Les branches distantes apparaissent sous le nom <code>origin/main</code>.<br><br>En équipe, on ne pousse pas directement sur main : on pousse une branche et on ouvre une <b>demande de fusion</b> (pull request ou merge request), relue par un collègue et validée par les tests automatiques avant d’être intégrée.', attention: 'Un push refusé (« rejected, non-fast-forward ») signifie que le dépôt distant a des commits que vous n’avez pas. On fait git pull (ou fetch puis rebase), jamais git push --force sur une branche partagée : on effacerait le travail des autres.' },
|
||
{ t: 'Revenir en arrière', d: '', html: 'Git offre plusieurs façons d’annuler, selon ce qu’on veut défaire. <code>git restore fichier</code> annule les modifications non ajoutées. <code>git restore --staged fichier</code> retire un fichier de l’index. <code>git commit --amend</code> corrige le dernier commit s’il n’a pas été poussé. <code>git revert id</code> crée un nouveau commit qui annule un commit précédent : c’est la seule méthode sûre sur un historique partagé. <code>git reset</code> déplace la branche en arrière et réécrit l’historique local.<br><br>Même après une erreur, rien n’est perdu tout de suite : <code>git reflog</code> liste les positions successives de HEAD et permet de retrouver un commit « supprimé ».' },
|
||
{ t: 'Ce qui ne va pas dans Git', d: '', html: 'Le fichier <code>.gitignore</code> exclut ce qui ne doit pas être versionné : dépendances (node_modules, vendor), fichiers générés, journaux, et surtout <b>secrets</b> (.env, clés). Un mot de passe poussé une fois reste dans l’historique même après suppression du fichier : il faut le considérer comme compromis et le changer.<br><br>Un bon <b>message de commit</b> décrit pourquoi, pas seulement quoi : « Corrige le calcul de la moyenne quand un élève est absent » plutôt que « fix ». Beaucoup d’équipes adoptent la convention Conventional Commits (feat:, fix:, docs:).' }
|
||
],
|
||
synthese: ['Git stocke des instantanés identifiés par empreinte ; une branche est un pointeur', 'Répertoire de travail → index (add) → dépôt (commit)', 'Une branche par fonctionnalité, fusion ou rebase ; conflit = même ligne modifiée des deux côtés', 'fetch, pull, push ; demande de fusion relue avant main', 'revert sur l’historique partagé, reset seulement en local, reflog pour retrouver', '.gitignore et aucun secret dans l’historique'],
|
||
qcm: [
|
||
['Que contient l’index ?', ['L’historique', 'Ce qui sera enregistré au prochain commit', 'Les branches distantes', 'Les fichiers ignorés'], 1, 'On y ajoute avec git add.'],
|
||
['Qu’est-ce qu’une branche pour Git ?', ['Une copie du projet', 'Un pointeur mobile vers un commit', 'Un dossier', 'Un dépôt distant'], 1, 'Créer une branche est instantané.'],
|
||
['Quand survient un conflit de fusion ?', ['Toujours', 'Quand les deux branches ont modifié les mêmes lignes', 'Quand on oublie git add', 'Quand le dépôt est trop gros'], 1, 'Git ne peut pas choisir seul.'],
|
||
['Comment annuler proprement un commit déjà poussé sur main ?', ['git reset --hard', 'git revert', 'git push --force', 'supprimer .git'], 1, 'revert ajoute un commit d’annulation.'],
|
||
['Différence entre fetch et pull ?', ['Aucune', 'fetch récupère sans fusionner, pull récupère et fusionne', 'fetch publie', 'pull supprime'], 1, 'fetch est sans risque.'],
|
||
['Un mot de passe a été poussé puis supprimé au commit suivant. Il est :', ['effacé', 'toujours dans l’historique, à changer', 'chiffré', 'invisible'], 1, 'L’historique garde tout.'],
|
||
['Quelle commande retrouve un commit perdu après un reset ?', ['git log', 'git reflog', 'git status', 'git show'], 1, 'Historique des positions de HEAD.']
|
||
],
|
||
td: [
|
||
{ n: 'Échauffement', c: 'Initialiser un dépôt, créer README.md, le versionner, le modifier, afficher la différence, puis faire un second commit.', r: 'git init · git add README.md · git commit -m "…" · git diff · git commit -am "…"' },
|
||
{ n: 'Application', c: 'Créer une branche nouvelle-page, y faire deux commits, revenir sur main, faire un commit qui touche la même ligne, fusionner et résoudre le conflit.', r: 'git switch -c nouvelle-page … git switch main … git merge nouvelle-page → conflit → éditer, git add, git commit' },
|
||
{ n: 'Application', c: 'Écrire le .gitignore d’un projet Laravel.', r: '/vendor\n/node_modules\n.env\n/storage/*.key\n/public/storage\n*.log' },
|
||
{ n: 'Approfondissement', c: 'Vous avez fait git reset --hard HEAD~2 par erreur. Retrouver les deux commits.', r: 'git reflog → repérer l’id avant le reset · git reset --hard <id> (ou git branch secours <id>)' }
|
||
],
|
||
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Un dépôt partagé sur un Forgejo de lab, travail en binôme.', etapes: ['Cloner le dépôt', 'Créer une branche et ouvrir une demande de fusion', 'Provoquer et résoudre un conflit avec le binôme', 'Annuler un commit avec revert', 'Retrouver un commit avec reflog'] },
|
||
plusLoin: [['Pro Git (livre officiel, en français)', 'https://git-scm.com/book/fr/v2'], ['Learn Git Branching (exercices interactifs)', 'https://learngitbranching.js.org/?locale=fr_FR'], ['Conventional Commits', 'https://www.conventionalcommits.org/fr/']] },
|
||
plus: {
|
||
cas: { titre: '', texte: '' },
|
||
resolu: { enonce: 'Vendredi 17 h : un développeur a poussé sur main un commit qui casse la page de connexion de l’application de vie scolaire, puis un collègue a déjà poussé deux commits par-dessus. Rétablir sans perdre le travail du collègue.', etapes: [
|
||
['Identifier le commit fautif', 'git log --oneline -5\n9c1e4f2 Traductions du menu\n51ab0d7 Ajout export CSV\ne73f210 Refonte formulaire connexion\n4d02a9b Correctif date\n→ La panne est apparue après e73f210. Les deux commits suivants ne touchent pas à la connexion.'],
|
||
['Confirmer avec git bisect si le doute subsiste', 'git bisect start ; git bisect bad ; git bisect good 4d02a9b\n… git bisect good / bad à chaque étape\ne73f210 is the first bad commit\ngit bisect reset\n→ bisect teste par dichotomie et désigne le premier commit fautif.'],
|
||
['Annuler ce seul commit', 'git revert e73f210\n[main 0f5a8c3] Revert "Refonte formulaire connexion"\n→ Un nouveau commit annule les modifications de e73f210 ; les commits du collègue sont conservés. Surtout pas de reset suivi d’un push --force sur main.'],
|
||
['Publier et vérifier', 'git push origin main\n→ La chaîne d’intégration redéploie ; la connexion fonctionne à nouveau.'],
|
||
['Reprendre le travail proprement', 'git switch -c refonte-connexion\ngit revert 0f5a8c3 (réapplique la refonte sur la branche)\n→ La refonte est corrigée sur une branche, testée, puis fusionnée par demande de fusion. Action préventive : protéger main (fusion uniquement par demande relue et tests verts).']
|
||
] },
|
||
erreurs: ['Pousser directement sur main', 'Utiliser push --force sur une branche partagée', 'Versionner .env ou des clés', 'Faire un seul énorme commit pour tout', 'Messages de commit du type « fix » ou « modif »', 'Rebaser une branche déjà partagée'],
|
||
memo: [['Commande', 'Usage'], ['git status', 'état des trois zones'], ['git add -p', 'ajouter une partie des modifications'], ['git commit -m "…"', 'enregistrer'], ['git log --oneline --graph --all', 'voir le graphe des commits'], ['git diff · git diff --staged', 'différences non ajoutées / ajoutées'], ['git switch -c <branche>', 'créer une branche et s’y placer'], ['git merge <branche>', 'fusionner'], ['git pull --rebase', 'récupérer en gardant un historique linéaire'], ['git push -u origin <branche>', 'publier une branche'], ['git revert <id>', 'annuler un commit partagé'], ['git reflog', 'retrouver une position perdue'], ['git bisect', 'trouver le commit qui a introduit un bug']],
|
||
examen: { titre: 'Sujet type · 20 points', q: ['Expliquer les trois zones de Git avec un schéma. (4 pts)', 'Décrire les commandes d’un travail en branche jusqu’à la demande de fusion. (5 pts)', 'Résoudre un conflit fourni. (4 pts)', 'Choisir entre revert et reset dans trois situations. (4 pts)', 'Rédiger le .gitignore d’un projet donné. (3 pts)'], r: ['working tree, index, dépôt', 'switch -c, add, commit, push -u, PR', 'selon le fichier', 'partagé → revert, local → reset', 'dépendances, secrets, fichiers générés'] },
|
||
prof: { deroule: [['Ouverture', 'Le fichier « final_v3_VRAIMENT_final » projeté'], ['Cours', 'Commits, index, branches'], ['Pratique', 'Exercices Learn Git Branching'], ['Pratique', 'Binômes sur un dépôt partagé, conflit provoqué'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : fiche mémo, un seul flux sans branches. Élèves avancés : rebase interactif, bisect, hooks.', eval: 'Historique du dépôt de binôme évalué, sujet type.' }
|
||
},
|
||
exemples: [
|
||
['Lire l’état du dépôt', 'Avant un commit.', 'git status -s', ' M app/Http/Controllers/NoteController.php\nM routes/web.php\n?? notes.csv', 'M en deuxième colonne : modifié, pas encore ajouté. M en première : ajouté à l’index. ?? : fichier non suivi ; ici un export de notes à ne surtout pas versionner, à ajouter au .gitignore.'],
|
||
['Voir le graphe', 'Comprendre où en sont les branches.', 'git log --oneline --graph --all -6', '* 9c1e4f2 (HEAD -> main, origin/main) Traductions du menu\n| * 3aa71be (export-pdf) Export PDF des bulletins\n|/\n* 51ab0d7 Ajout export CSV', 'La branche export-pdf est partie de 51ab0d7 ; main a avancé depuis. La fusion créera un commit de fusion, ou un rebase rendra l’historique linéaire.'],
|
||
['Push refusé', 'Publication de son travail.', 'git push', '! [rejected] main -> main (fetch first)\nhint: Updates were rejected because the remote contains work that you do not have locally.', 'Un collègue a poussé avant vous. git pull --rebase rejoue vos commits au-dessus des siens, puis git push passe.'],
|
||
['Résoudre un conflit', 'Fusion de deux branches.', 'git merge nouvelle-page ; git diff --name-only --diff-filter=U', 'CONFLICT (content): Merge conflict in index.html\nAutomatic merge failed; fix conflicts and then commit the result.\nindex.html', 'On ouvre index.html, on choisit le contenu entre les marqueurs, on supprime les marqueurs, puis git add index.html et git commit.'],
|
||
['Retrouver un travail « perdu »', 'Après un reset malheureux.', 'git reflog -4', '4d02a9b HEAD@{0}: reset: moving to HEAD~2\n9c1e4f2 HEAD@{1}: commit: Traductions du menu\n51ab0d7 HEAD@{2}: commit: Ajout export CSV', 'Le commit 9c1e4f2 existe toujours. git reset --hard 9c1e4f2 le récupère.'],
|
||
['Trouver qui a modifié une ligne', 'Une règle de calcul semble fausse.', 'git blame -L 42,44 app/Services/Moyenne.php', '51ab0d7 (Inès K. 2026-09-24) $coef = $note->coef ?? 1;\ne73f210 (Tom L. 2026-09-27) $total += $note->valeur;\ne73f210 (Tom L. 2026-09-27) return $total / count($notes);', 'blame associe chaque ligne à son dernier commit. La division par count($notes) ignore les coefficients : on regarde le commit e73f210 avec git show pour comprendre le changement.']
|
||
],
|
||
termes: ['Git', 'commit', 'index', 'branche', 'HEAD', 'main', 'merge', 'rebase', 'conflit', 'fast-forward', 'git add', 'git commit', 'git status', 'git log', 'git diff', 'git switch', 'git pull', 'git push', 'git fetch', 'git revert', 'git reset', 'git reflog', 'git bisect', 'git blame', '.gitignore', 'pull request', 'origin']
|
||
});
|
||
|
||
reg({
|
||
cours: { id: 'cicd', match: /CI\/CD|intégration continue|déploiement continu|pipeline/i,
|
||
titre: 'CI/CD : intégration et déploiement continus', duree: '',
|
||
niveau: 'BTS SIO SLAM / SISR · BUT Informatique / R&T DevCloud · AIS · Master',
|
||
prerequis: ['Git', 'Conteneurs Docker'],
|
||
competences: ['BTS SIO SLAM · Bloc 2 · Déployer et tester une solution applicative', 'BUT R&T DevCloud · Automatiser le déploiement', 'AIS · Industrialiser les mises en production'],
|
||
intro: 'Livrer à la main, c’est se connecter au serveur, tirer le code, lancer les migrations, vider les caches et espérer ne rien oublier. Une chaîne d’intégration et de déploiement continus automatise ces étapes : à chaque push, les tests s’exécutent, l’image est construite et, si tout est vert, l’application est déployée. Les erreurs sont détectées en minutes plutôt qu’en production, et chaque livraison devient un non-événement.',
|
||
cm: [
|
||
{ t: 'Intégration, livraison, déploiement', d: '', html: 'L’<b>intégration continue</b> (CI) fusionne le travail de chacun fréquemment et vérifie automatiquement chaque modification : compilation, analyse statique, tests. La <b>livraison continue</b> (continuous delivery) produit à chaque fois un artefact prêt à déployer, la mise en production restant une décision humaine. Le <b>déploiement continu</b> (continuous deployment) va jusqu’à la production sans intervention.<br><br>Le principe commun : la branche principale est toujours dans un état déployable, et toute régression est signalée immédiatement à celui qui l’a introduite.' },
|
||
{ t: 'Anatomie d’un pipeline', d: '', html: 'Un <b>pipeline</b> est une suite d’<b>étapes</b> (stages) composées de <b>jobs</b>, exécutés par des <b>runners</b> dans des environnements propres, souvent des conteneurs. On y trouve typiquement : vérification du style (lint), tests unitaires, construction de l’image, analyse de sécurité, publication dans un registre, déploiement en préproduction, puis en production.<br><br>Chaque job reçoit le code du commit, produit éventuellement des <b>artefacts</b> (paquet, rapport de tests, image) et renvoie un statut. Un seul échec arrête le pipeline.', exemple: '# .github/workflows/ci.yml (GitHub Actions)\nname: ci\non: [push, pull_request]\njobs:\n tests:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - uses: actions/setup-python@v5\n with: { python-version: "3.12" }\n - run: pip install -r requirements.txt\n - run: ruff check .\n - run: pytest -q\n image:\n needs: tests\n if: github.ref == \'refs/heads/main\'\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n - run: docker build -t registre.lycee.local/resa:${{ github.sha }} .\n - run: trivy image --exit-code 1 --severity HIGH,CRITICAL registre.lycee.local/resa:${{ github.sha }}\n - run: docker push registre.lycee.local/resa:${{ github.sha }}' },
|
||
{ t: 'Les tests, cœur de la CI', d: '', html: 'Une CI ne vaut que par ses tests. Les <b>tests unitaires</b> vérifient une fonction isolée, en millisecondes. Les <b>tests d’intégration</b> vérifient l’application avec sa base de données, souvent démarrée comme service du pipeline. Les <b>tests de bout en bout</b> pilotent un navigateur (Playwright, Cypress). On en écrit beaucoup des premiers, peu des derniers : c’est la pyramide des tests.<br><br>Un test instable (qui échoue une fois sur dix sans raison) est pire que pas de test : l’équipe apprend à ignorer le rouge. Il se corrige en priorité.' },
|
||
{ t: 'Déployer sans interruption', d: '', html: 'Le déploiement tire l’image construite et testée, identifiée par l’empreinte du commit, jamais par latest. Plusieurs stratégies limitent le risque. Le <b>déploiement progressif</b> (rolling) remplace les instances une à une. Le <b>blue-green</b> prépare la nouvelle version à côté de l’ancienne puis bascule le trafic. Le <b>canary</b> envoie d’abord une petite part des utilisateurs sur la nouvelle version.<br><br>Dans tous les cas, on prévoit le <b>retour arrière</b> : redéployer l’image précédente doit prendre une commande. Les migrations de base de données doivent rester compatibles avec la version précédente le temps de la bascule.' },
|
||
{ t: 'Secrets et sécurité de la chaîne', d: '', html: 'Le pipeline manipule des secrets : accès au registre, clés SSH de déploiement, jetons d’API. Ils se stockent dans le gestionnaire de secrets de la plateforme (secrets GitHub, variables masquées GitLab), jamais dans le dépôt, et avec des droits minimaux (une clé de déploiement ne peut que déployer).<br><br>La chaîne elle-même est une cible : une action tierce compromise ou une dépendance malveillante s’exécute avec les secrets du pipeline. On épingle les actions sur une version précise, on analyse les dépendances (Dependabot, Renovate) et on protège les branches : pas de déploiement en production depuis une branche non relue.' }
|
||
],
|
||
synthese: ['CI : vérifier chaque modification ; CD : livrer ou déployer automatiquement', 'Pipeline = étapes → jobs → runners, arrêt au premier échec', 'Pyramide des tests ; un test instable se corrige en priorité', 'Image identifiée par le commit ; stratégie de déploiement et retour arrière prévus', 'Secrets dans la plateforme, actions épinglées, branches protégées'],
|
||
qcm: [
|
||
['Différence entre livraison continue et déploiement continu ?', ['Aucune', 'La livraison laisse la mise en production à une décision humaine', 'Le déploiement ne teste pas', 'La livraison est manuelle'], 1, 'Continuous delivery vs deployment.'],
|
||
['Que se passe-t-il quand un job échoue ?', ['Le pipeline continue', 'Le pipeline s’arrête et signale l’échec', 'Le job est ignoré', 'Le code est supprimé'], 1, 'On ne déploie pas du rouge.'],
|
||
['Pourquoi taguer l’image avec l’empreinte du commit ?', ['Esthétique', 'Pour savoir exactement quel code tourne et pouvoir revenir en arrière', 'Obligatoire', 'Plus léger'], 1, 'Traçabilité et reproductibilité.'],
|
||
['Quelle stratégie envoie d’abord 5 % des utilisateurs sur la nouvelle version ?', ['Rolling', 'Blue-green', 'Canary', 'Big bang'], 2, 'Détection précoce sur un petit échantillon.'],
|
||
['Où stocker la clé de déploiement ?', ['Dans le dépôt', 'Dans les secrets de la plateforme CI', 'Dans le Dockerfile', 'Dans le README'], 1, 'Jamais dans le code.'],
|
||
['Que faire d’un test qui échoue au hasard ?', ['L’ignorer', 'Le corriger en priorité', 'Le supprimer toujours', 'Relancer jusqu’au vert'], 1, 'Sinon on ne croit plus au rouge.']
|
||
],
|
||
td: [
|
||
{ n: 'Application', c: 'Écrire un pipeline GitLab CI (.gitlab-ci.yml) en trois étapes : lint, test, build, avec une base PostgreSQL en service pour les tests.', r: 'stages: [lint, test, build]\nlint: { stage: lint, image: python:3.12, script: [pip install ruff, ruff check .] }\ntest: { stage: test, image: python:3.12, services: [postgres:16], variables: { POSTGRES_PASSWORD: test, DATABASE_URL: postgresql://postgres:test@postgres/postgres }, script: [pip install -r requirements.txt, pytest] }\nbuild: { stage: build, image: docker:27, services: [docker:27-dind], script: [docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA ., docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA], rules: [{ if: $CI_COMMIT_BRANCH == "main" }] }' },
|
||
{ n: 'Application', c: 'Proposer une procédure de retour arrière pour une application déployée avec Compose et des images taguées par commit.', r: 'Garder le tag précédent dans l’historique des déploiements · modifier le tag dans compose.yaml (ou variable IMAGE_TAG) · docker compose pull && up -d · vérifier · migrations compatibles N-1.' },
|
||
{ n: 'Approfondissement', c: 'Lister les risques de sécurité d’un pipeline et une mesure pour chacun.', r: 'Secret fuité → secrets masqués, droits minimaux · action tierce compromise → épingler par empreinte · dépendance malveillante → analyse et verrouillage des versions · déploiement non relu → branches protégées.' }
|
||
],
|
||
tp: { type: 'Lab VM', url: 'apercu-labs.html', desc: 'Un Forgejo avec runner, une API et son déploiement.', etapes: ['Écrire le workflow lint + tests', 'Casser un test et observer le blocage', 'Construire et scanner l’image', 'Déployer sur la VM de préproduction', 'Revenir à la version précédente'] },
|
||
plusLoin: [['GitHub Actions · documentation', 'https://docs.github.com/fr/actions'], ['GitLab CI/CD · documentation', 'https://docs.gitlab.com/ee/ci/'], ['Forgejo Actions', 'https://forgejo.org/docs/latest/user/actions/']] },
|
||
plus: {
|
||
cas: { titre: '', texte: '' },
|
||
resolu: { enonce: 'Le pipeline de l’application de réservation est rouge depuis ce matin sur l’étape tests, alors que les tests passent sur le poste du développeur. Plus aucune correction ne peut être déployée.', etapes: [
|
||
['Lire le job en échec', 'tests / pytest\nE sqlalchemy.exc.OperationalError: could not connect to server: Connection refused\n Is the server running on host "localhost" and accepting TCP/IP connections on port 5432?\n→ Ce n’est pas un test fonctionnel qui échoue : l’application ne trouve pas sa base.'],
|
||
['Comparer les environnements', 'Sur le poste : PostgreSQL installé localement, DATABASE_URL absente → valeur par défaut localhost.\nDans le pipeline : la base est un service nommé postgres.\n→ Un commit d’hier a supprimé la variable DATABASE_URL du workflow en « nettoyant » le fichier.'],
|
||
['Corriger le workflow', 'services:\n postgres:\n image: postgres:16\n env: { POSTGRES_PASSWORD: test }\n options: --health-cmd pg_isready --health-interval 5s --health-retries 10\nenv:\n DATABASE_URL: postgresql://postgres:test@localhost:5432/postgres\n→ Le service est déclaré avec un healthcheck et la variable est rétablie.'],
|
||
['Vérifier', 'git push → pipeline : lint ✓ tests ✓ image ✓\n→ Le pipeline est vert ; la correction en attente est déployée.'],
|
||
['Prévenir', '→ Le fichier de workflow passe sous relue obligatoire (CODEOWNERS), et un test de fumée vérifie au démarrage que DATABASE_URL est définie, avec un message explicite plutôt qu’une erreur de connexion.']
|
||
] },
|
||
erreurs: ['Pipeline sans tests réels', 'Tolérer un pipeline rouge « connu »', 'Déployer l’image latest', 'Secrets en clair dans le workflow', 'Actions tierces non épinglées', 'Pas de procédure de retour arrière'],
|
||
memo: [['Élément', 'À retenir'], ['on: push / pull_request', 'déclencheurs d’un workflow GitHub'], ['needs:', 'ordre entre jobs'], ['services:', 'base de données pour les tests'], ['secrets.NOM', 'accès à un secret masqué'], ['rules / if', 'n’exécuter que sur main'], ['artifacts', 'conserver rapports et paquets'], ['cache', 'accélérer les dépendances'], ['${{ github.sha }} / $CI_COMMIT_SHA', 'taguer l’image par commit'], ['trivy image --exit-code 1', 'bloquer sur une vulnérabilité critique'], ['environments + approbation', 'validation humaine avant production']],
|
||
examen: { titre: 'Sujet type · 20 points', q: ['Définir CI, livraison et déploiement continus. (3 pts)', 'Écrire un pipeline complet pour une application fournie. (8 pts)', 'Comparer trois stratégies de déploiement. (4 pts)', 'Diagnostiquer un pipeline en échec à partir d’un journal. (3 pts)', 'Citer deux risques de sécurité de la chaîne. (2 pts)'], r: ['voir cours', 'lint, tests, build, push, deploy', 'rolling, blue-green, canary', 'selon le journal', 'secrets, actions tierces'] },
|
||
prof: { deroule: [['Ouverture', 'Déploiement manuel chronométré, puis le même en un push'], ['Cours', 'Pipeline, tests, artefacts'], ['Pratique', 'Workflow lint + tests'], ['Pratique', 'Build et déploiement'], ['Clôture', 'Quiz flash']], diff: 'Élèves en difficulté : workflow à compléter. Élèves avancés : déploiement canary, analyse de dépendances.', eval: 'Pipeline vert et retour arrière démontré, sujet type.' }
|
||
},
|
||
exemples: [
|
||
['Lire l’état des pipelines', 'Après un push.', 'gh run list --limit 3', 'STATUS TITLE WORKFLOW BRANCH\n✓ Export CSV ci main\nX Refonte connexion ci refonte\n✓ Traductions ci main', 'La branche refonte est rouge : elle ne pourra pas être fusionnée tant que la protection de branche exige des tests verts.'],
|
||
['Voir le journal d’un job', 'Pipeline rouge.', 'gh run view --log-failed | tail -4', 'tests FAILED tests/test_moyenne.py::test_absent - ZeroDivisionError\ntests 1 failed, 47 passed in 3.21s', 'Un seul test échoue, avec une erreur précise : la moyenne d’un élève sans note divise par zéro. La CI a attrapé le bug avant la production.'],
|
||
['Relancer localement comme la CI', 'Reproduire un échec.', 'docker run --rm -v "$PWD":/app -w /app python:3.12 sh -c "pip install -q -r requirements.txt && pytest -q"', '1 failed, 47 passed', 'Exécuter les tests dans la même image que la CI élimine le « chez moi ça marche ».'],
|
||
['Bloquer une image vulnérable', 'Étape de sécurité.', 'trivy image --exit-code 1 --severity CRITICAL registre.lycee.local/resa:9c1e4f2', 'Total: 1 (CRITICAL: 1)\nopenssl 3.0.2 CVE-2024-XXXX fixed in 3.0.14', 'Le code de sortie 1 fait échouer le job : l’image n’est pas publiée. On met à jour l’image de base et on reconstruit.'],
|
||
['Déployer une version précise', 'Déploiement depuis le pipeline.', 'ssh deploy@preprod "cd /srv/resa && IMAGE_TAG=9c1e4f2 docker compose up -d && docker compose ps"', 'resa-app registre.lycee.local/resa:9c1e4f2 Up 5 seconds (healthy)', 'Le tag est l’empreinte du commit : on sait exactement quel code tourne.'],
|
||
['Revenir en arrière', 'La nouvelle version provoque des erreurs.', 'ssh deploy@prod "cd /srv/resa && IMAGE_TAG=51ab0d7 docker compose up -d"', 'Container resa-app Recreated', 'Retour à la version précédente en une commande, parce que chaque image est conservée avec son tag de commit.']
|
||
],
|
||
termes: ['CI/CD', 'intégration continue', 'déploiement continu', 'livraison continue', 'pipeline', 'job', 'runner', 'artefact', 'workflow', 'GitHub Actions', 'GitLab CI', 'Forgejo', 'tests unitaires', 'lint', 'canary', 'blue-green', 'rolling', 'retour arrière', 'secrets', 'Trivy']
|
||
});
|
||
})();
|