--- # LES DEPOTS D'UN RUNNER SUIVENT SA DECLARATION — DANS LES DEUX SENS. # # `serveur_ops` clone ce que `serveur_ops_depots` declare. Il ne retire rien : un depot # raye du plan reste sur le disque, et le runner garde de quoi lire un ecosysteme qu'il # ne declare plus. Mesure du 2026-09-20 : le runner de Chezlepro-LOCATAIRE portait encore # le clone de `SITE-Chezlepro` — la carte de son hebergeur — apres que son plan ait cesse # de la declarer. # # POURQUOI CE N'EST PAS DANS LE ROLE. Retirer un depot est DESTRUCTIF, et la regle du # depot est qu'une action destructive s'annonce et se confirme. Un role qui efface des # dossiers a chaque passage serait une grenade degoupillee : il suffirait d'une faute de # frappe dans `serveur_ops_depots` pour perdre un travail local. On en fait donc un geste # separe, qui REGARDE par defaut et n'efface que sur `CONFIRMER=true`. # # CE QUI NE SERA JAMAIS EFFACE, meme avec la confirmation : # - un dossier qui n'est pas un depot git (on ne sait pas ce que c'est) ; # - un depot qui porte des modifications non validees ; # - un depot qui porte des commits qu'aucun distant ne porte. # Dans ces trois cas le playbook le DIT et passe. Ce qui n'existe qu'ici ne se detruit # pas depuis ici. # # make depots-perimes regarde, n'efface rien # make depots-perimes CONFIRMER=true retire ceux qui ne portent rien d'unique - name: Les dépôts qu'un runner ne déclare plus hosts: "{{ cible | default('serveur_ops') }}" become: true become_user: "{{ serveur_ops_utilisateur | default('setops') }}" gather_facts: false vars: racine: "{{ serveur_ops_racine | default('/opt/setops') }}" confirme: "{{ (lookup('env', 'CONFIRMER') | default('', true)) == 'true' }}" tasks: - name: Exiger de savoir ce que ce runner déclare ansible.builtin.assert: that: - (serveur_ops_depots | default([])) | length > 0 fail_msg: >- Cet hôte ne déclare aucun dépôt (`serveur_ops_depots`). Sans la liste de ce qu'il DOIT porter, on ne peut pas dire ce qui est périmé — et surtout pas l'effacer. - name: Lister ce que le runner porte ansible.builtin.find: paths: "{{ racine }}" file_type: directory recurse: false hidden: false register: presents - name: Retenir ceux que le plan ne déclare plus ansible.builtin.set_fact: perimes: >- {{ presents.files | map(attribute='path') | map('basename') | list | difference(serveur_ops_depots | map(attribute='dest') | list) }} - name: Le dire quand il n'y a rien à retirer ansible.builtin.debug: msg: >- Ce runner ne porte que ce qu'il déclare ({{ serveur_ops_depots | map(attribute='dest') | join(', ') }}). when: perimes | length == 0 # ÉCRIRE, PUIS RELIRE — ici : REGARDER avant de détruire. Trois questions par dépôt, et # la réponse décide. `git status` et `git log` ne modifient rien. - name: Regarder ce que chaque dépôt périmé porte d'unique ansible.builtin.shell: cmd: | # `pipefail` SANS `-e` : la regle `risky-shell-pipe` l'exige des qu'un `|` # parait — ici ce sont des `||`, mais l'armer ne coute rien et dit la bonne # intention. Pas de `-e` : un `git` qui echoue sur un dossier douteux doit # laisser ce script REPONDRE (le depot sera juge « pas un depot git »), pas # faire tomber la tache sans rien dire. set -uo pipefail d={{ (racine ~ '/' ~ item) | quote }} if [ ! -d "$d/.git" ]; then echo '{"git": false}'; exit 0; fi # AUCUN TUBE ICI, ET C'EST VOULU. `… | head -c 2000` bornait la sortie ; avec # `pipefail` — qu'ansible-lint exige a juste titre — `head` qui ferme tot fait # echouer la commande, et sans `pipefail` c'est l'echec de `git` qui se perd. # On ne borne pas ce qu'on ne lit pas : seule la VACUITE de ces deux sorties # est interrogee, jamais leur contenu. sale=$(git -C "$d" status --porcelain 2>/dev/null) # Des commits qu'AUCUN distant ne porte : ce qui n'existe qu'ici. seuls=$(git -C "$d" log --branches --not --remotes --oneline 2>/dev/null) printf '{"git": true, "sale": %s, "seuls": %s}\n' \ "$([ -n "$sale" ] && echo true || echo false)" \ "$([ -n "$seuls" ] && echo true || echo false)" executable: /bin/bash loop: "{{ perimes }}" register: examen changed_when: false - name: Nommer ce qui est retenu, et pourquoi ansible.builtin.debug: msg: >- {{ item.item }} : {{ 'ce n_est pas un dépôt git — on ne sait pas ce que c_est, on n_y touche pas' if not (item.stdout | from_json).git else 'des modifications non validées vivent ici' if (item.stdout | from_json).sale else 'des commits qu_aucun distant ne porte vivent ici' if (item.stdout | from_json).seuls else 'ne porte rien d_unique — retirable' }} loop: "{{ examen.results }}" loop_control: label: "{{ item.item }}" - name: Retenir ceux qu'on peut retirer sans rien perdre ansible.builtin.set_fact: retirables: >- {{ examen.results | selectattr('stdout') | selectattr('stdout', 'search', '"git": true') | rejectattr('stdout', 'search', '"sale": true') | rejectattr('stdout', 'search', '"seuls": true') | map(attribute='item') | list }} - name: Dire ce qu'une confirmation retirerait ansible.builtin.debug: msg: >- {{ retirables | length }} dépôt(s) retirable(s) : {{ retirables | join(', ') }}. Rien n'a été touché — relancer avec CONFIRMER=true pour les retirer. when: - retirables | length > 0 - not (confirme | bool) - name: Retirer les dépôts que ce runner ne déclare plus ansible.builtin.file: path: "{{ racine }}/{{ item }}" state: absent loop: "{{ retirables }}" when: - confirme | bool - not ansible_check_mode # RELIRE APRÈS AVOIR ÉCRIT. Un `state: absent` qui se dit satisfait sans que le # dossier ait disparu est exactement le genre de succès qui ment. - name: Vérifier qu'ils ont bien disparu ansible.builtin.stat: path: "{{ racine }}/{{ item }}" loop: "{{ retirables }}" register: apres when: confirme | bool - name: Refuser un retrait qui n'a pas eu lieu ansible.builtin.assert: that: - not (item.stat.exists | default(false)) fail_msg: "{{ item.item }} est toujours là après le retrait." success_msg: "{{ item.item }} : retiré." loop: "{{ apres.results | default([]) }}" loop_control: label: "{{ item.item | default('') }}" when: confirme | bool