Ce document dit pourquoi le catalogue est organisé ainsi. Il a été écrit le 2026-09-25 à partir d'une lecture complète des 69 leçons du parcours et d'une série de mesures sur act 0.2.89. Quand une décision repose sur une mesure, la mesure est donnée : une conception qui ne se vérifie pas ne vaut rien.
Le parcours LMS github-actions compte 12 modules et 69 leçons, dont 6 de type
lab. Ces six labs s'appuient sur le dépôt stephrobert/secure-python-pipeline
et se vérifient à la main : aucune page ne fournit de point de départ, de
vérification automatique ni de solution rejouable. Six modules sur douze n'ont
aucun lab : fondations, workflows, optimiser, runners,
gouvernance, outils, soit 45 leçons sans pratique.
La question qui pilote ce dépôt est donc la même que celle des catalogues frères : combien de compétences du parcours l'apprenant peut-il démontrer réellement, et se vérifier lui-même ?
Une vérification qui passe avant le travail ne mesure rien. Chaque lab de ce
dépôt est joué par scripts/valider-labs.py dans cet ordre :
- le point de départ (
challenge/) est copié dans un répertoire temporaire ; - la vérification DOIT rendre 0/100 ;
- la solution de référence (
solution/) est posée par-dessus ; - la vérification DOIT rendre 100/100 ;
- le cycle est rejoué une seconde fois, pour prouver qu'il est rejouable.
Le verdict est écrit dans validation-labs.json, avec la date, la version d'act
et l'empreinte de l'image du runner. Un lab absent de ce fichier est un lab
livrable, pas un lab validé.
Les tests d'un lab lisent l'état, jamais les commandes tapées : ils jouent le workflow avec act et regardent ce qu'il produit (résultat des jobs, lignes écrites, jobs sautés, artefacts, findings d'un scanner). Un test qui ne fait que relire le YAML est un complément, jamais la preuve ; il est nommé comme tel. Le dernier test de chaque lab exerce les deux côtés : ce qui doit être refusé l'est, et ce qui doit passer passe.
Mesuré le 2026-09-25, act 0.2.89 (mise), Docker 29.1.3, image
catthehacker/ubuntu:act-24.04 au digest
sha256:c58e2b364da03b0c804c7d660f2ecbedf2f221a382b9baa0b344b0144780ff43
(construite le 2026-08-15).
| Ce que le lab a besoin de jouer | act | Mesure |
|---|---|---|
jobs, steps, needs, if, github.event_name |
oui | outputs.yml : use tourne sur push, only-pr sur pull_request |
$GITHUB_OUTPUT, outputs de job, needs.X.outputs |
oui | got v1.2.3 relu dans le job dépendant |
strategy.matrix, include |
oui | 3 jobs générés, experimental=true sur la seule combinaison ajoutée |
uses: ./.github/workflows/x.yml (workflow_call) avec inputs et outputs |
oui | resultat=deploye-sur-staging remonté à l'appelant |
| action composite locale avec inputs et outputs | oui | msg=bonjour monde |
actions/cache (--cache-server-path) |
oui | cache-hit=true au second passage |
masquage d'un secret (-s), vars (--var) |
oui | le secret est *** |
actions/setup-python puis pip, pytest |
oui | 36 s au premier passage |
commandes ::warning::, ::notice:: |
oui | affichées |
-e event.json (payload d'une issue, d'une PR, inputs) |
oui | une injection par github.event.issue.title s'exécute, la forme env: l'affiche telle quelle |
filtres branches: et paths: de on: |
non | le job tourne pour refs/heads/feature comme pour main |
actions/upload-artifact v6 et v7, download-artifact v7 et v8 |
non | unknown field "mime_type" ; PR amont nektos/act#6174 ouverte ; v5.0.0 + v6.0.0 fonctionnent |
services: |
non | le job tourne en network=host, le service sur un réseau dédié : redis:6379 et 127.0.0.1:6379 injoignables |
concurrency, timeout-minutes réels, environment et approbations, OIDC, attestations, GHCR, rulesets, Scorecard, harden-runner |
non | ce sont des services de la plateforme, act ne les émule pas |
Trois conséquences :
- un lab dont le sujet est un filtre de déclencheur ne se prouve pas avec act,
il se prouve avec
gh run listsur un dépôt de l'apprenant ; - un lab sur les artefacts épingle
upload-artifactv5.0.0 etdownload-artifactv6.0.0 tant que la PR amont n'est pas fusionnée, et le dit ; - tout ce qui exige la plateforme reçoit un chemin « plateforme » : le test lit
le dépôt de l'apprenant par
gh apiquandLAB_REPO=owner/nomest fourni, et s'arrête avec un message clair sinon.
labs/<module>/<id>/
lab.yaml le contrat : module, leçons éprouvées, jouabilité act
scenario.md la situation et l'objectif, jamais un mode d'emploi
challenge/ le point de départ, que l'apprenant modifie
.actrc l'image du runner, épinglée par digest
.github/workflows/ ce qu'il y a à écrire ou à corriger
hints.yaml quatre indices, encodés en base64, de coût croissant
tests/test_functional.py la preuve, lue par pytest
solution/ la solution de référence, posée par-dessus challenge/
<module> est l'identifiant du module dans le parcours (fondations,
workflows, securite, ...). meta.yml donne l'ordre de jeu, calqué sur le
parcours ; tests/test_meta_declare_les_labs.py refuse un lab présent sur le
disque et absent de meta.yml, ou l'inverse.
L'apprenant joue par dsoxlab, et rien d'autre : le catalogue ne porte ni
vérificateur, ni gestion des indices, ni calcul de score. Une première version
de cette conception prévoyait scripts/verifier-lab.py et scripts/indice.py ;
Stéphane l'a refusée le 2026-09-25, dsoxlab étant le moteur de tous ses
catalogues. Ce qu'il ne sait pas faire devient une issue dans
stephrobert/dsoxlab.
mise install # act, actionlint, zizmor, pinact, uv
dsoxlab run fondations-premier-workflow # fixtures copiées dans challenge/work
dsoxlab challenge fondations-premier-workflow # la mission
# ... le travail, dans labs/fondations-premier-workflow/challenge/work ...
dsoxlab hint fondations-premier-workflow # un indice, déduit du score
dsoxlab check fondations-premier-workflow # les tests jouent le workflow avec actLes labs sont à plat sous labs/<module>-<sujet>/, comme dans les catalogues
frères, et non sous labs/<module>/<sujet>/.
Refondu en labs/fondations/premier-workflow. Trois raisons, mesurées :
- sa vérification (
validate.py) était une analyse statique du YAML : un workflow qui écritrun: echo pytestla passait sans jamais exécuter un test. Le nouveau lab joue le workflow avec act, et vérifie qu'un test cassé fait échouer le pipeline ; - son README était un mode d'emploi de 500 lignes, en Node.js, alors que son
challenge était en Python, et la page
workflows/index.mdxdu blog le décrivait dans les deux langages à la fois ; - le blog y renvoie par
github.com/stephane-robert/github-actions-training/blob/main/...: mauvais propriétaire (stephrobert) et mauvaise branche (master, renomméemainle 2026-10-17). Le lien est mort. C'est au blog de le corriger, verslabs/fondations/premier-workflow/scenario.md.
L'application calculatrice et ses neuf tests sont conservés.
Le dépôt secure-python-pipeline reste le résultat de référence des cinq
labs du module securite-lab et du lab de promotion du module deployer :
c'est lui que les pages du blog décrivent, et c'est lui qui porte un Scorecard,
un score Plumber et des attestations publiques qu'un tiers peut vérifier.
Ce qu'il ne peut pas être, et que ce dépôt apporte :
- un point de départ : chaque lab
securite-lab/*part de l'état laissé par le lab précédent (l'application minimale, puis les fichiers de gouvernance, puis la CI) ; - une vérification : locale quand elle existe (
ruff,pytest,bandit,pip-audit,trivy,actionlint,zizmor --offline,poutine analyze_local), pargh apisur le fork de l'apprenant pour les rulesets, les environnements, les attestations et Scorecard ; - une solution rejouable, paramétrée par
${{ github.repository }}et${{ github.repository_owner }}là où le dépôt de référence écritstephroberten dur.
Trois écarts mesurés entre les pages et le dépôt de référence, à remonter à
Stéphane et non à corriger ici : les SHA de checkout, setup-python,
login-action, plumber, scorecard-action et upload-sarif des pages ne
sont pas ceux du dépôt ; deploy.yml, promis par lab-promotion.mdx, n'existe
pas dans le dépôt ; le digest de l'image de base cité dans bootstrap.mdx a
été remplacé le 2026-09-11.
Le tableau dit ce que chaque lab prouve et comment. La colonne act vaut « oui » quand la preuve entière se joue en local, « partiel » quand une partie seulement, « non » quand la preuve exige la plateforme.
| Lab | Ce qu'il prouve | act |
|---|---|---|
premier-workflow |
les tests d'une application tournent à chaque push et pull_request, et un test cassé fait échouer le pipeline |
oui |
secrets-et-variables |
un secret passe par env:, reste masqué, et une valeur dérivée (base64) ne l'est plus ; une vars s'affiche en clair |
oui |
evaluer-et-epingler-une-action |
chaque action est épinglée sur le SHA du tag annoncé (vérifié par pinact --verify), et Dependabot est déclaré pour les maintenir |
partiel |
| Lab | Ce qu'il prouve | act |
|---|---|---|
pipeline-en-graphe |
needs, outputs de job, if sur l'événement, propagation des sauts, rapport final en !cancelled() |
oui |
matrice-de-tests |
produit cartésien, include qui enrichit ou ajoute, exclude, matrice dynamique par fromJSON |
oui |
reutiliser |
un workflow_call local avec inputs et outputs, une action composite locale avec shell: obligatoire |
oui |
declencheurs |
workflow_dispatch avec inputs typés, github.event_name ; les filtres se prouvent sur la plateforme |
partiel |
services-conteneurs |
un job qui attend la santé de son service ; act ne le joue pas | non |
| Lab | Ce qu'il prouve | act |
|---|---|---|
neutraliser-une-injection |
une charge dans le titre d'une issue s'exécute avant, s'affiche telle quelle après ; zizmor rend 0 finding | oui |
epingler-et-maintenir |
un dépôt à tags mobiles passe en SHA, pinact --verify confirme, dependabot.yml porte un cooldown |
partiel |
auditer-avec-les-scanners |
un dépôt piégé sur chaque règle citée par les leçons zizmor, poutine et plumber rend 0 finding, et ses workflows tournent toujours | oui |
harden-runner-et-threat-model |
harden-runner en premier step, en block avec une allowlist, et un modèle de menace versionné et validé par schéma |
partiel |
| Lab | Ce qu'il prouve | act |
|---|---|---|
permissions-minimales |
permissions: {} en tête, write seulement dans le job qui publie ; Scorecard Token-Permissions en local |
oui |
desamorcer-pull-request-target |
le motif en deux workflows (pull_request puis workflow_run), la garde head.repo.fork jouée sous act avec un payload de fork |
partiel |
oidc-sans-cle-longue-duree |
aucune clé cloud dans les secrets, id-token: write au niveau job, une trust policy qui refuse repo:owner/* |
partiel |
attester-et-verifier |
gh attestation verify et cosign verify sur une image publique, puis sur l'image du fork de l'apprenant |
non |
| Lab | Ce qu'il prouve | act |
|---|---|---|
1-bootstrap-securise |
gouvernance, dépendances par hash, image non-root, zéro vulnérabilité HIGH/CRITICAL | oui |
2-pipeline-ci-durci |
actionlint, zizmor, poutine et plumber à zéro, et la CI qui teste vraiment | oui |
3-build-verifiable |
provenance SLSA, SBOM attesté, signature Cosign, vérifiés par un tiers | non |
4-protection-et-gouvernance |
un ruleset relu par gh api, CODEOWNERS sans erreur, plumber à 21/21 |
non |
5-scoring-et-durcissement |
Scorecard décortiqué contrôle par contrôle, fuzzing | non |
| Lab | Ce qu'il prouve | act |
|---|---|---|
promouvoir-un-deploiement-approuve |
deux environnements, une approbation, la promotion par digest, le rollback sans rebuild | non |
| Lab | Ce qu'il prouve | act |
|---|---|---|
artefacts-entre-jobs |
un build partagé par needs et artefact, if-no-files-found: error, if: always() pour les rapports |
oui (v5/v6, voir plus haut) |
cache-des-dependances |
cache-hit au second passage, invalidation quand le lockfile change, version de Python dans la clé |
oui |
debug-d-un-workflow |
un workflow qui échoue pour trois raisons distinctes, à diagnostiquer et corriger | oui |
concurrency |
l'annulation des runs obsolètes, lue sur les runs du dépôt de l'apprenant | non |
| Lab | Ce qu'il prouve | act |
|---|---|---|
runner-ephemere-en-conteneur |
une image de runner non-root construite, enregistrée en --ephemeral, qui disparaît après un job |
non |
securiser-un-runner |
nettoyage entre les jobs par les hooks du runner, compte dédié, sortie réseau filtrée | partiel |
| Lab | Ce qu'il prouve | act |
|---|---|---|
rulesets-et-codeowners |
un ruleset actif relu par gh api, codeowners/errors vide, une PR bloquée sans revue |
non |
actions-autorisees |
la politique d'actions posée au niveau du dépôt par API, et un workflow hors liste refusé | non |
| Lab | Ce qu'il prouve | act |
|---|---|---|
actionlint-corriger-des-workflows |
trois workflows fautifs rendus propres, actionlint.yaml pour les labels maison |
oui |
act-rejouer-en-local |
un workflow rejoué avec événement, inputs, secrets et matrice filtrée | oui |
gh-piloter-les-runs |
déclencher, suivre avec --exit-status, relancer les seuls jobs en échec |
non |
Les décisions qui ne se prennent pas ici :
- corriger dans le blog le lien vers ce dépôt (
workflows/index.mdx), et les erreurs de leçons relevées dans les issues de chaque lab ; - publier
deploy.ymldanssecure-python-pipelineet y remplacerstephroberten dur par le contexte du dépôt, pour qu'un fork produise une image sans retouche ; - décider si les labs « plateforme » exigent un fork public par apprenant, ce qui est le cas aujourd'hui pour les rulesets et les environnements protégés.