Analiză de incident
Tiparul de ignore care a șters un modul din producție
Un director de date a fost trecut în ignore fără slash inițial, așa că tiparul s-a potrivit și cu un pachet de cod aflat mai adânc în arbore. Pachetul n-a fost niciodată comis, un deploy-oglindă l-a șters de pe server, iar singura descriere care i-a supraviețuit a fost suita lui de teste.
Publicat
Ce s-a stricat
Un serviciu a încetat să mai pornească și a intrat în buclă de restart. Cauza imediată era banală: o rută activă importa un modul, modulul nu era pe disc, iar importul a picat cu ModuleNotFoundError înainte ca aplicația să apuce să servească ceva. Nimeni nu ștersese fișierul intenționat.
Lanțul pornește de la un tipar de ignore și se termină la scriptul de deploy. Fișierul de ignore avea o intrare pentru un director cu fișiere de date exportate. Tiparul era scris fără slash inițial. Mai adânc în arborele de surse exista un pachet cu exact același nume de director, iar tiparul l-a prins și pe acela, pentru că un tipar se potrivește cu un nume, nu cu o poziție.
Așa că pachetul n-a ajuns niciodată în repo. Exista pe discul celui care îl scrisese și pe server, unde îl pusese cândva un deploy mai vechi, bazat pe arhivă. Ambele copii păreau în regulă atât timp cât nimeni n-a refăcut serverul dintr-o clonă.
Un tipar fără slash inițial se potrivește la orice adâncime
Este comportament documentat al sintaxei standard de ignore, nu un bug și nu un caz-limită. Dacă tiparul are un slash la început sau la mijloc, e ancorat: se aplică relativ la directorul fișierului de ignore. Dacă nu are niciun astfel de slash, se poate potrivi oriunde mai jos în arbore. Un nume simplu de director devine, practic, un wildcard pe tot arborele.
E comportamentul implicit potrivit pentru artefacte de build sau fișiere lăsate de editor, care apar oriunde. E comportamentul implicit greșit pentru un singur director de date aflat în rădăcina repo-ului, fiindcă același cuvânt e un nume perfect rezonabil și pentru un pachet de cod.
Eșecul e tăcut prin însăși construcția lui. Un fișier ignorat nu apare printre fișierele neurmărite, deci git status a rămas curat atât timp cât pachetul era ignorat. Nu exista niciun avertisment pe care să-l fi ratat cineva. Unealta a făcut exact ce i s-a cerut.
Un deploy-oglindă face din discul mașinii care face deploy-ul sursa de adevăr
Scriptul de deploy șterge directoarele de surse de pe server și despachetează o arhivă construită de pe discul mașinii care face deploy-ul. Asta e o oglindire, nu un merge. Orice există pe server și lipsește din arhivă dispare după deploy, fie că era gunoi vechi, fie că era un modul de care depinde o rută din producție.
Mașina care făcea deploy-ul clonase din repo, deci pe discul ei se aflau exact fișierele versionate. Pachetul ignorat nu era printre ele. Arhiva era, prin urmare, corectă față de controlul versiunilor și greșită față de realitate, iar deploy-ul a propagat diferența pe server.
Formulat general: un deploy care oglindește un disc șterge tăcut orice fișier real prezent pe server și absent de pe mașina care face deploy-ul. Mașina aceea devine definiția aplicației. Dacă îi lipsește ceva, deploy-ul e o ștergere cu pași în plus.
Ne așteptam să recuperăm fișierul. Ne-am înșelat
Primul plan a fost recuperarea de pe server. E planul la care sare mai toată lumea și pleacă de la ipoteza că datele șterse stau încă în blocuri nereferențiate, așteptând să fie reasamblate. Pe sistemele de fișiere moderne ipoteza nu mai ține: harta de blocuri e pusă pe zero la unlink, deci legăturile de la inode către date sunt distruse înaintea datelor. Unealta obișnuită de recuperare forensică a întors zero inoduri recuperabile — nu o reconstrucție parțială stricată, ci nimic.
Ce a salvat modulul au fost testele. Ele erau urmărite de git, pentru că niciun tipar de ignore nu se potrivea cu calea lor. Erau 32 de teste într-un fișier și 6 în altul, iar împreună descriau exact comportamentul cerut: intrări, ieșiri, cazuri de eroare, forma fiecărei valori pe care restul sistemului o aștepta înapoi. Modulul a fost reconstruit din propria suită de teste, folosită ca specificație.
Lecția pe care am reținut-o nu e despre fișierul de ignore. E că o suită de teste este singura descriere a unui modul care supraviețuiește modulului. Documentația rămâne în urma codului, comentariile se pierd odată cu fișierul pe care îl explicau, iar o unealtă de recuperare poate întoarce nimic. Un test care rulează e o declarație executabilă de intenție, ținută în alt fișier. E un argument mai puternic pentru scrierea testelor decât orice țintă de acoperire pe care am primit-o vreodată.
Două măsuri de prevenire, amândouă ieftine
Ancorează în rădăcina repo-ului, cu slash inițial, tiparele de ignore pentru directoare de date. Tiparul se aplică atunci exact directorului pentru care a fost scris și nu mai poate prinde un director cu același nume din arborele de surse. Asta elimină întreaga categorie de accidente.
Înainte de orice deploy care oglindește un disc pe server, importă local modulul de intrare al aplicației. Un import parcurge graful real de dependențe și atinge fiecare modul de care are nevoie o rută activă. Dacă importul pică pe mașina care face deploy-ul, va pica și pe server — numai că afli înaintea serverului, pe o mașină unde reparația e un checkout, nu un incident.
Niciuna dintre măsuri nu cere o unealtă nouă, un document de politică sau acordul cuiva. Motivul pentru care lipseau e motivul obișnuit pentru care lipsesc astfel de măsuri: până în ziua în care tiparul de ignore s-a întâlnit cu scriptul de deploy, fiecare părea inofensiv separat.
Cum rulezi aceeași verificare
Listează fiecare fișier pe care repo-ul îl ignoră și care există totuși pe server, apoi întreabă-te despre fiecare: sunt date sau e cod? E normal ca datele să fie ignorate și să difere de la o mașină la alta. Codul care există doar pe server e un modul pe care nu-l ai, iar următorul deploy-oglindă îl va șterge. Din partea repo-ului, git ls-files --others --ignored --exclude-standard enumeră ce e ignorat; compară lista cu arborele de pe server.
Apoi recitește fiecare tipar de ignore fără slash inițial și decide dacă a fost gândit să se potrivească la orice adâncime. Unele da: cache-urile și artefactele compilate apar peste tot. Directoarele cu nume propriu, aflate în rădăcina repo-ului, aproape niciodată nu sunt. Pentru orice fișier de care nu ești sigur, git check-ignore -v îți spune exact tiparul și linia care l-au exclus, ceea ce transformă întrebarea dintr-o ghiceală într-o simplă căutare.
La final, rulează verificarea prin import manual, înainte să o legi în deploy, ca să vezi cum arată o rulare care trece curat, pe o mașină în care ai încredere. O rulare reușită dovedește că graful de dependențe e complet pe discul care urmează să devină sursa de adevăr.
Limite
Rezultatul recuperării ține de sistemul de fișiere. Pe unul care nu pune harta de blocuri pe zero la unlink, un undelete poate foarte bine să reușească, iar concluzia despre teste ar fi venit ca preferință, nu ca singura opțiune rămasă. Nu citi aici „recuperarea e imposibilă”, ci „nu-ți face planul pe ea”.
Comportamentul de potrivire a tiparelor nu e specific nouă. E comportamentul documentat al sintaxei standard de ignore și se generalizează la orice repo care o folosește.
Acesta e un singur incident, într-o singură bază de cod. Mecanismul e general, iar cele două măsuri de prevenire sunt destul de ieftine ca să merite aplicate fără dovezi în plus, dar frecvența nu e ceva ce un caz izolat îți poate spune.
Întrebări și răspunsuri
De ce un tipar din .gitignore prinde și directoare aflate mai adânc?
Un tipar din .gitignore prinde și directoare aflate mai adânc atunci când nu are slash la început sau la mijloc. Un astfel de tipar se poate potrivi oriunde mai jos de directorul fișierului de ignore, așa că un nume simplu de director devine un wildcard pe tot arborele. Un slash inițial ancorează tiparul la directorul din rădăcina repo-ului.
Cum poate un deploy să șteargă un fișier pe care nu l-a șters nimeni?
Un deploy-oglindă poate șterge un fișier pe care nu l-a șters nimeni pentru că elimină directoarele de surse de pe server și despachetează o arhivă construită de pe discul mașinii care face deploy-ul. Tot ce există pe server și lipsește din arhivă dispare. Dacă un pachet ignorat n-a fost comis niciodată, o mașină care a clonat repo-ul nu-l are, iar deploy-ul îl șterge.
Pot recupera un fișier de cod șters de pe un server?
Recuperarea unui fișier de cod șters de pe un server nu e ceva pe care să te bazezi. Pe sistemele de fișiere moderne harta de blocuri e pusă pe zero la unlink, deci legăturile de la inode către date sunt distruse. În incidentul descris, unealta obișnuită de recuperare forensică a întors zero inoduri recuperabile. Pe alte sisteme de fișiere un undelete poate reuși, dar nu-ți face planul pe el.
Cum pot ajuta testele la reconstruirea unui modul pierdut?
Testele pot ajuta la reconstruirea unui modul pierdut pentru că o suită de teste descrie intrările, ieșirile, cazurile de eroare și forma fiecărei valori pe care o așteaptă restul sistemului. Testele stau în alt fișier decât modulul, deci pot supraviețui pierderii lui. În incident, modulul a fost reconstruit folosind propria suită de teste, urmărită de git, ca specificație.
Cum verific ce fișiere ignoră git înainte de un deploy?
Verificarea fișierelor ignorate de git începe cu git ls-files --others --ignored --exclude-standard, care enumeră ce e ignorat; compari lista cu arborele de pe server și te întrebi despre fiecare fișier dacă sunt date sau cod. Codul care există doar pe server va fi șters de următorul deploy-oglindă. Pentru un fișier nesigur, git check-ignore -v arată exact tiparul și linia care l-au exclus.
Cum se citează
Going Stellar (2026). Tiparul de ignore care a șters un modul din producție. GOING STELLAR S.R.L..
https://goingstellar.ro/ro/cercetare/tipar-ignore-neancorat-a-sters-un-modul/