In het open-data warehouse beloofde ik dat
er niets naar main gaat zonder dat de volledige keten heeft gedraaid. Die
belofte is één YAML-bestand: .github/workflows/ci.yml, zestig regels, één
job. In dit stuk loop ik er doorheen — niet regel voor regel, maar langs de
beslissingen die erin zitten. Want een CI-pipeline is net als een datamodel:
de code is snel geschreven, de keuzes zijn het werk.
Het probleem: de data staat niet in git
De eerste vraag bij CI voor een dataproject is niet "welke stappen draai ik",
maar "waar draai ik ze op". De ruwe data — miljoenen RDW-registraties —
staat in data/raw/ en die map staat om goede redenen niet in git. Een
pipeline die de echte bron nodig heeft, is traag, duur en afhankelijk van een
externe API die op elk moment iets anders kan teruggeven.
De oplossing zit niet in de workflow maar in het dbt-project zelf. De bronnen zijn gedefinieerd met een omgevingsvariabele in het pad:
meta:
external_location: "{{ env_var('DBT_RAW_DIR', 'data/raw') }}/rdw_{name}.parquet"
Lokaal wijst DBT_RAW_DIR standaard naar data/raw/. In CI zet de workflow
hem op tests/fixtures/ — een kleine, ingecheckte set parquetbestanden met
dezelfde structuur als de echte bron, inclusief de datakwaliteitsproblemen
die het warehouse geacht wordt op te lossen. Eén variabele, en exact dezelfde
modellen, tests en documentatie draaien op een bron die in seconden laadt.
CI test niet de data van vandaag. CI test of de logica klopt op data waarvan je precies weet wat erin zit.
Dat DuckDB de motor is, maakt de rest triviaal: het warehouse is een bestand.
Geen testomgeving aanvragen, geen credentials in secrets, geen kosten per
run. In profiles.yml — bewust in de repo, niet in ~/.dbt/ — staat naast
dev een apart ci-target, zodat een CI-run nooit per ongeluk een lokale
database raakt.
Snel falen, in de goede volgorde
De job zelf is een rechte lijn, maar de volgorde van de stappen is bewust:
ruff check .— de goedkoopste controle eerst. Een tikfout in een Python-loader hoeft geen dbt-run van minuten te kosten.dbt deps— vóór de SQL-linter, en dat is minder vanzelfsprekend dan het lijkt. sqlfluff draait hier met de dbt-templater, die de modellen écht compileert in plaats van Jinja weg te gokken. Dat betekent wel datdbt_utilsgeïnstalleerd moet zijn, anders struikelt de linter over elke{{ dbt_utils.* }}-aanroep.sqlfluff lint models— stijl en structuur van alle SQL, gezien zoals DuckDB hem ziet.dbt build --target ci— modellen bouwen én alle tests draaien in afhankelijkheidsvolgorde: uniciteit, verplichte velden, referentiële integriteit tussen feiten en dimensies.dbt docs generate --static— de volledige documentatie met lineage, als één zelfstandig HTML-bestand.
Die laatste stap is meer dan een formaliteit. De documentatie en het
manifest.json worden als artifact geüpload — met if-no-files-found: error,
want een documentatiestap die stilletjes niets oplevert is erger dan een die
faalt. Zo hoort bij elke wijziging een downloadbare, actuele beschrijving van
het hele model. Documentatie die bij elke merge opnieuw wordt gegenereerd,
kan niet verouderen.
De kleine dingen die je pas mist als ze er niet zijn
Drie regels bovenin doen onevenredig veel werk:
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
Wie drie keer kort na elkaar pusht, wil één relevante uitslag — niet drie runs in de wachtrij waarvan er twee over verouderde code gaan. Verouderde runs worden afgebroken zodra er een nieuwe start.
Daarnaast: permissions: contents: read. De workflow kan de repo lezen en
verder niets. Een CI-pipeline die alleen hoeft te bouwen, hoort geen token te
hebben waarmee hij kan schrijven — hetzelfde principe als autorisatie in een
datawarehouse, alleen dan voor de pipeline zelf.
En de pip-cache is gekoppeld aan beide requirements-bestanden, zodat een gewijzigde dev-dependency de cache net zo goed ongeldig maakt als een gewijzigde runtime-dependency. Klein detail; scheelt elke run een minuut.
Waarom dit relevant is voor jouw organisatie
Dit is dezelfde discipline die ik bij opdrachtgevers tegenkom onder duurdere namen: een OTAP-straat, een releaseproces, een kwaliteitspoort. De essentie past in zestig regels YAML: elke wijziging bewijst eerst dat de hele keten nog werkt, op een voorspelbare bron, en levert zijn eigen actuele documentatie mee. De technologie verschilt per omgeving — Azure DevOps, GitLab, Databricks Workflows — maar de vragen zijn overal dezelfde: waar draait je test op, wat controleer je in welke volgorde, en wat bewaar je als bewijs?
Het volledige workflowbestand staat op GitHub.
Draait jouw datateam al elke wijziging door zo'n poort — of gaat er nog weleens iets stilletjes mee naar productie? Plan een kennismaking — dan kijk ik graag een keer mee.