De consistentie van je dataplatform waarborgen is lastig, vooral op grote schaal. Je bouwt een ETL pipeline die gebruikt wordt in meerdere repositories. Maanden later, wil je een nieuwe feature toevoegen die op alle repositories toegepast moet worden. Elke repo handmatig updaten is tijdrovend, foutgevoelig en risicovol: mis je er een, dan introduceer je drift; update je er een verkeerd, dan kan je een productieflow breken.
Template generatie tools zoals Cruft en Copier lossen dit op door automatische updates naar bestaande projecten mogelijk te maken. In plaats van alleen initiële projectstructuren te genereren, kunnen ze wijzigingen doorvoeren over je hele platform.
Deze post vergelijkt beide tools, met focus op hun praktische verschillen voor teams die meerdere projecten beheren en de lessen die wij geleerd hebben bij het implementeren van geautomatiseerde template-updates in productie.
Een van de meest gebruikte tools voor projectgeneratie in het Python ecosysteem is Cookiecutter. Het laat je herbruikbare project templates definiëren met Jinja2. Je genereert nieuwe projecten door waarden in te vullen zoals project_name of author. Cookiecutter is een eenvoudige en bekende tool die al jaren meegaat.
Cookiecutter heeft echter een significante beperking: het werkt alleen op het moment van project creatie. Zodra een project gegenereerd is, is er geen ingebouwde manier om template updates te tracken. Je kunt die wijzigingen ook niet toepassen op bestaande projecten. Dit wordt een probleem wanneer je op grotere schaal werkt.
Stel je voor dat je een gedeeld template hebt dat gebruikt wordt in tientallen services, pipelines of interne tools. In de loop van de tijd verandert het template. Misschien wil je nieuwe CI/CD jobs toevoegen, configuratie defaults aanpassen, of nieuwe features toevoegen. Zonder een geautomatiseerde manier om die wijzigingen door te voeren, moet je deze handmatig toepassen op elk project. Dat is te doen met twee of drie repositories, maar het wordt bijna onmogelijk wanneer het aantal tientallen of honderden bereikt.
Dit is precies het probleem dat Cruft oplost. Wij gebruikten al een Cookiecutter-template voor onze projecten, dus het toevoegen van Cruft was eenvoudig. Het bouwt namelijk direct voort op Cookiecutter. Het voegt functionaliteit toe om Cookiecutter-gebaseerde projecten te tracken en updaten. Wanneer je een project creëert met Cruft, registreert het metadata over de template. Dit omvat de Git source, de exacte commit die gebruikt werd, en de context variabelen. Dit alles wordt opgeslagen in een .cruft.json file binnen het gecreëerde project. Met deze link kan Cruft later updates ophalen van het template en ze toepassen op je project.
Bij het updaten checkt Cruft de huidige staat van je project tegen de laatste versie van het template. Het genereert de template opnieuw met dezelfde context en vergelijkt het resultaat met je lokale codebase. Dan past het de verschillen toe. Als wijzigingen botsen met lokale edits, dan word je gevraagd om ze op te lossen via je reguliere Git merge workflow.
Dat gezegd hebbende, cruft update runnen en conflicts handmatig oplossen voor elke repo is nog steeds behoorlijk wat werk. Om developer werk te verminderen, integreerden we Cruft in onze CI/CD pipeline. Als er wijzigingen zijn in het template, dan wordt één keer per week automatisch een merge request aangemaakt. Dit omvat alle wijzigingen sinds de laatste update. De verantwoordelijke developer kan deze dan reviewen, goedkeuren en eventuele merge conflicts oplossen.
De grootste uitdaging die we tegenkwamen was hoe Cruft omgaat met deze merge conflicts.
Wanneer een conflict optreedt tijdens een update, genereert Cruft een .rej file om aan te geven dat het bepaalde wijzigingen van de template niet kon toepassen. Dit veroorzaakte een paar problemen voor ons.
Ten eerste blijft het originele bestand onveranderd, zelfs als de update gedeeltelijk faalt. Dat betekent dat onze CI/CD pipeline nog steeds kan slagen, ondanks dat de update niet volledig toegepast werd. Dit is vooral problematisch omdat Cruft nog steeds de commit hash in .cruft.json update naar de laatste versie. Dit gebeurt zelfs als de template wijzigingen niet relevant zijn voor een specifieke repo. Als resultaat kan een merge request geaccepteerd en gemerged worden met onopgeloste conflicts. Belangrijke wijzigingen kunnen overgeslagen worden zonder dat je het door hebt.
Ten tweede zijn .rej files onhandig om mee te werken. In tegenstelling tot conflict markers die wijzigingen inline tonen en makkelijk op te lossen zijn, vereisen .rej files handmatige patching. Bijvoorbeeld, een inline conflict ziet er zo uit:
Python:
1<<<<<<< before updating
2def greet(name):
3 print("Hello, " + name + "!")
4=======
5# New version from the updated template:
6def greet(name):
7 print(f"Hi there, {name}!")
8>>>>>>> after updating
Dit vereist alleen dat de developer dit specifieke bestand aangepast. Ze hoeven alleen de code te houden die nuttig is voor hun project. Voor de meeste kleinere wijzigingen kan dit in een IDE of direct in de MR-pagina van GitLab of GitHub gedaan worden, waardoor deze conflicts vrij snel opgelost kunnen worden.
Er is geen optie in Cruft om te switchen naar een meer gebruiksvriendelijke conflict resolution methode. Hoewel dit een paar jaar geleden opgebracht is in een open GitHub issue, is er nog geen fix gemerged.
Omdat deze issues significant waren voor onze workflow, besloten we Copier uit te proberen, een modernere scaffolding tool die updates anders behandelt.
Copier bestaat niet zo lang als Cookiecutter, maar heeft snel populariteit gewonnen dankzij een moderne aanpak voor code generatie. Het is geschreven in Python en gebruikt net als Cookiecutter Jinja2 voor templating. Copier heeft bovendien een ingebouwde manier om projecten bij te werken, waardoor je niet meer op twee verschillende repositories hoeft te vertrouwen.
Naast updates biedt Copier verschillende verbeteringen. Zo heeft het gebruiksvriendelijke interface voor het invullen van template variabelen, met type-safe prompts met validatie. Ook ondersteunt het conditioneel opnemen van bestanden en folders, afhankelijk van de antwoorden die gebruikers geven. Verder heeft Copier ook een intuïtief systeem voor het oplossen van conflicten: het toont duidelijke diffs in plaats van .rej-bestanden te maken. Mocht je toch de voorkeur geven aan .rej-bestanden, dan kun je de standaardinstellingen voor output van merge conflicts veranderen.
Er is veel meer ruimte voor configuratie. Je kunt input types definiëren zoals strings, booleans en integers. Je kunt intelligente defaults instellen. Je kunt ook hele files en directories includeren of excluderen gebaseerd op hoe users prompts beantwoorden.
Als je begintmet een bestaande Cookiecutter template, moet je de syntax en structuur iets aanpassen. Door de Copier-documentatie te volgen, is dit prima te doen. Variabelen en vragen zijn iets anders opgezet. Het belangrijkste is om de .cruft.json file correct te migreren naar .copier-answers.yml. Dit is waar Copier naar het project metadata zal zoeken.
De manier om een taak toe te voegen aan je CI/CD-pijplijn die automatisch een merge request aanmaakt, lijkt sterk op die van Cruft. Het grootste verschil zit in de gebruikte commands. Hieronder staat een voorbeeld van zo’n taak in GitLab CI/CD:
YAML:
1create_merge_request:
2 stage: copier
3 image: python:3.11-slim
4 before_script:
5 - apt-get update
6 - apt-get -y install git curl
7
8 # https://git-scm.com/docs/git-credential-store
9 - git config --global credential.helper store
10 - echo "https://gitlab-ci-token:${CI_JOB_TOKEN}@gitlab.com" > ~/.git-credentials
11
12 - git config --global user.email "<email>"
13 - git config --global user.name "<username>"
14
15 script:
16 - copier update --defaults
17 - git restore --staged .
18
19 - |
20 if [ "$CI_COMMIT_REF_NAME" == "main" ]; then
21 git checkout -b copier-update
22
23 # Check for changes
24 if git diff --quiet; then
25 echo "No changes detected. Exiting."
26 exit 0
27 fi
28
29 COMMIT_MSG="chore: New updates to Copier template detected"
30 DESCRIPTION="This MR has been created to remain up-to-date with the Copier template."
31
32 # Push changes to git
33 git remote set-url origin https://gitlab-ci-token:${SEMANTIC_RELEASE_GL_TOKEN}@gitlab.com/${CI_PROJECT_PATH}.git
34 git add .
35
36 git commit -m "$COMMIT_MSG"
37 git push origin copier-update
38
39
40 curl --request POST --header "PRIVATE-TOKEN: ${PROPERLY_SCOPED_TOKEN}"
41 --form "source_branch=copier-update"
42 --form "target_branch=main"
43 --form "title=$COMMIT_MSG"
44 --form "description=$DESCRIPTION"
45 "https://gitlab.com/api/v4/projects/$CI_PROJECT_ID/merge_requests"
46 --form "remove_source_branch=true"
47 --form "squash=true"
48 fi
49 rules:
50 - if: $CI_PIPELINE_SOURCE == "schedule"
Al met al is Cruft een goede keuze als je al een bestaande Cookiecutter-template hebt. Het werkt vooral goed als je workflow weinig overlap heeft tussen de bestanden die je per project aanpast en de bestanden die in het template worden bijgewerkt. Kortom, situaties waarin merge-conflicten zeldzaam zijn. In zulke gevallen, en als je geen ingewikkelde templatelogica nodig hebt, kan de directe compatibiliteit van Cruft met Cookiecutter een voordeel zijn.
Voor de meeste andere toepassingen is Copier echter de betere optie. Het geeft je meer vrijheid in hoe je updates beheert en biedt betere hulpmiddelen om conflicten op te lossen. Daarnaast bevat het moderne templatingfuncties die zowel het maken van templates als het onderhouden van projecten eenvoudiger maken. De inspanning die nodig is om van Cookiecutter naar Copier over te stappen, verdient zich op de lange termijn terug. Je krijgt een verbeterde developer ervaring en betrouwbaardere update processen. Als je een nieuw begin maakt of vaak merge-conflicten hebt met je huidige werkwijze, zal investeren in Copier je team waarschijnlijk veel tijd en frustratie besparen.
Copier is inmiddels vier maanden geïntegreerd in onze workflow en heeft al veel uren bespaard die anders zouden zijn besteed aan het handmatig bijwerken van bestaande projecten.
Of wil jij meer weten over automatisering?
Neem dan contact met ons op, we helpen je graag verder.