learn-dataops-with-phoebe / Builder session 1 of 8
Learn DataOps with Phoebe · Builder track · Session 1 of 8

Foundations: repo, Git flow, CI, Docker

DataOps is DevOps discipline pointed at data. Before a single pipeline runs, the data product needs the same spine every serious software project has: one repo as the source of truth, a branching flow, an automated safety net, and a container that runs the same everywhere. Tonight you build that spine for RetailPulse - and green CI on your first pull request.

🟢 Builder track Practitioners: DA · DE · DS · ML Python 3.10+ · Git · Docker · a GitHub account Start here
0-3 · Welcome 3-18 · Why the spine 18-42 · Build-along: scaffold + CI 42-45 · Q&A
Part 0

How this track works

Eight sessions, one growing artifact: RetailPulse, a retail sales data product that starts tonight as a repo with one small pipeline and graduates in session b8 with orchestration, data contracts, database migrations, a monitored forecast model, and a promote-to-prod release. The unit of work is not a tool - it is the diff: one reviewable change that can touch code, schema, data expectations, and a model at once, and either passes every gate or does not merge. Each session adds one more thing a single safe change can now move. Two rules run through the track: you build each capability by hand and feel it break first, so the tooling never gets to be magic - and everything you build is really a way to read a change and judge it, because by b8 the change you review will have been written by an AI agent, not you.

Live - presented in session Self-study - read after class ★ Try it now command Open-source stack + AWS notes
★ What you walk out with today A RetailPulse repo laid out like a real project, trunk-based Git flow you can defend in a review, pre-commit hooks that format and lint before code ever lands, a GitHub Actions pipeline that runs your tests on every pull request, and a Dockerfile so "works on my machine" stops being a sentence anyone says.
Part 1 · covers the DataOps definition + the CI/CD idea

What DataOps actually industrializes 6 min live

Strip the buzzword and DataOps is a promise: any change to your data, models, or schema can go from a laptop to production quickly, safely, and without heroics - because automated gates catch mistakes instead of on-call humans. That promise rests on four moves borrowed straight from DevOps. Tonight is move one: put everything in version control and let a machine check it.

1 · Commit code + config in Git 2 · Automated gate lint · test · quality 3 · Repeatable run same container, anywhere 4 · Production shipped, trusted Artisanal data: steps 2 and 3 are a human remembering to run things. DataOps: they are code that runs itself. Session b1 builds steps 1-3 for RetailPulse. Step 4 is the whole rest of the track.
🔍 Click to zoom - the DataOps assembly line: commit, gate, repeatable run, production
LiveThe four moves DataOps borrows from DevOps3 min

DataOps is not a product you buy. It is a set of practices - version control, continuous integration, continuous delivery, and observability - applied to the data and ML lifecycle instead of just application code. The four that matter:

  • Version everything: pipeline code, SQL, config, and eventually the schema and the model - all in Git. If it is not in version control, it does not exist and cannot be reviewed, reverted, or trusted.
  • Continuous integration (CI): every change is automatically linted and tested before it merges. The machine, not the reviewer, catches the broken import at 2am.
  • Continuous delivery (CD): merged changes flow to environments through the same automated path every time - no manual copy-paste to a server.
  • Containers: the code carries its own environment, so it runs identically on your laptop, in CI, and in prod. "Works on my machine" becomes irrelevant.
★ Try it now (your terminal)git --version && docker --version && python --version # All three should print. If Docker is missing, install Docker Desktop - you need it in Part 3.
Real world

The Friday deploy that took the dashboards down. A retail analytics team shipped a one-line SQL change straight to prod on a Friday. It silently changed a column type; every downstream dashboard broke over the weekend and nobody knew until Monday's board meeting. The fix was not a smarter analyst - it was a CI gate that would have caught the schema change in 40 seconds. That gate is what you build tonight.

Self-studyDataOps, MLOps, DevOps - who owns what2 min read

The terms overlap and people fight about the borders. A working map:

DisciplineObject it ships safelyIn this course
DevOpsApplication codeThe spine - b1 (this session)
Data CI/CDPipelines and datasetsb2 orchestration, b3 testing
DBOpsDatabase schemab4 migrations
MLOpsModelsb5 tracking, b6 deploy/monitor

DataOps is the umbrella - the culture and automation that makes all four move at the same cadence. The leader track's session a2 maps each of these to a business risk; worth a skim if you ever have to justify this work to a budget holder.

Part 2 · covers repo layout + trunk-based Git flow

The repo, and the flow that keeps it sane 6 min live

A data project that lives half in notebooks, half in someone's Downloads folder, cannot be industrialized. Step one is a real repo layout and a branching model simple enough that the whole team actually follows it. For most data teams that is trunk-based development, not the heavy GitFlow of a decade ago.

retailpulse/ src/retailpulse/pipeline.py tests/test_pipeline.py data/ (gitignored) .github/workflows/ci.yml .pre-commit-config.yaml Dockerfile pyproject.toml · README.md every artifact in one place, in Git main always releasable feature/clean-nulls short-lived · opens a pull request Branch small, merge fast through a PR that CI must pass. Long branches are where data bugs hide.
🔍 Click to zoom - RetailPulse repo layout and the trunk-based flow every change follows
LiveWhy trunk-based beats GitFlow for data teams3 min

GitFlow - with its develop, release, and hotfix branches - was built for shrink-wrapped software with scheduled releases. Data teams ship continuously and are usually small. Trunk-based development fits better:

  • One long-lived branch: main, always releasable. Everything else is a short branch that lives hours to a day.
  • Small pull requests: a change that touches one transformation, reviewed and merged same-day. Small PRs get better review and cause smaller incidents.
  • CI is the gatekeeper: the branch cannot merge until the automated checks pass. This is the rule that makes the whole thing safe - and it is a repo setting, not a person's willpower.
  • Protect main: require the PR and require CI green. In GitHub: Settings -> Branches -> add a rule. Do it on day one, before anyone can push straight to main.
Data-specific rule Never commit data. Put data/, .env, and model binaries in .gitignore. Repos are for code and config; real data lives in object storage or a database. A committed 2GB CSV is a mistake you cannot fully undo.
Self-studyA repo layout you will not outgrow2 min read

The src/ layout (package under src/, tests beside it) is the boring, correct default - it forces you to install your package the way users will and stops "it imports locally but breaks in CI" surprises.

Prompt A - the scaffold (bash)mkdir -p retailpulse/src/retailpulse retailpulse/tests retailpulse/data cd retailpulse printf "data/\n.env\n__pycache__/\n*.parquet\n" > .gitignore git init -b main

Add a pyproject.toml declaring the package and its dependencies (pandas, pyarrow, pytest). This becomes the single place that says what RetailPulse needs - CI and Docker both read it.

Part 3 · covers pre-commit + GitHub Actions CI

CI: the safety net that never sleeps 3 min live

Continuous integration means: every push, a machine checks out your code, installs it clean, and runs your linters and tests. If anything is red, the pull request cannot merge. Two layers do this - pre-commit runs on your laptop before the commit, GitHub Actions runs in the cloud on the PR. Defense in depth.

pre-commit black · ruff, on your laptop Install (pip) Lint (ruff) Test (pytest) GitHub Actions runs on every pull request all green -> merge allowed any red -> merge blocked main stays green Same three steps run on your laptop and in the cloud. The cloud run is the one that guards main.
🔍 Click to zoom - two layers of CI: pre-commit locally, GitHub Actions on the PR
LivePre-commit: catch it before it is even a commit2 min

Pre-commit hooks run on git commit and can auto-format and lint. They keep the diff clean and stop the whole team arguing about style in review. Your workspace default is black for formatting and ruff for linting.

Prompt B - .pre-commit-config.yamlrepos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.6.0 hooks: [{ id: ruff, args: [--fix] }, { id: ruff-format }] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.6.0 hooks: [{ id: end-of-file-fixer }, { id: check-added-large-files }]

Then pip install pre-commit && pre-commit install. The check-added-large-files hook is your insurance against the accidental committed dataset.

Demo 1 of 2

Scaffold RetailPulse and its first pipeline ★ 14 min · everyone builds

RetailPulse v0.1: a repo with one honest pipeline - read a raw retail sales CSV, clean it, write a parquet file - plus one test that proves it works. Nothing fancy. Everything in version control.

Scaffold with Prompt A above: the src/ layout, .gitignore, and git init -b main. Confirm data/ is gitignored before you put any CSV in it.

Write src/retailpulse/pipeline.py: a function clean_sales(in_path, out_path) that reads the CSV with pandas, drops null order ids, coerces the date column, and writes parquet. Keep it under 30 lines - it grows all track long.

Write tests/test_pipeline.py: build a tiny 3-row DataFrame in the test, run it through the cleaning logic, and assert the row count and that dates parsed. This is the test CI will run.

Wire pre-commit (Prompt B), run pre-commit install, then git add -A && git commit. Watch black and ruff tidy your files before the commit completes.

Run pytest -q locally and see it pass. You now have step 1 and the local half of step 2 from the Part 1 diagram.

★ RetailPulse v0.1 - pipeline.py, the whole fileimport pandas as pd def clean_sales(in_path: str, out_path: str) -> int: """Read raw retail sales, clean, write parquet. Returns rows written.""" df = pd.read_csv(in_path) df = df.dropna(subset=["order_id"]) df["order_date"] = pd.to_datetime(df["order_date"], errors="coerce") df = df.dropna(subset=["order_date"]) df.to_parquet(out_path, index=False) return len(df) if __name__ == "__main__": n = clean_sales("data/raw_sales.csv", "data/clean_sales.parquet") print(f"wrote {n} rows")
Data tip Use a public or synthetic retail CSV (Kaggle's "Online Retail" or a generated one). Real confidential data does not belong in a teaching repo - and that discipline, set in session b1, never relaxes.
Demo 2 of 2

Green CI on a pull request, and a Dockerfile ★ 8 min · build your own

Now the cloud half. A GitHub Actions workflow runs your tests on every PR; a Dockerfile makes the pipeline run identically anywhere. Push a branch, open a PR, and watch the green check appear.

Create .github/workflows/ci.yml from Prompt C. Commit it on main first so the workflow exists.

Push RetailPulse to a new GitHub repo. In Settings -> Branches, add a rule protecting main: require a pull request and require the CI check to pass.

Make a change on a branch - git switch -c feature/tidy-dates, tweak the pipeline, push. Open a pull request and watch Actions run install -> lint -> test in the cloud.

Break it on purpose: push a failing test. See the red X and the blocked merge button. Fix it, see green, merge. That loop is CI, felt.

Write the Dockerfile (Prompt D) and run docker build -t retailpulse . && docker run retailpulse. The pipeline runs in a clean container - the same one CI and prod will use later.

★ Prompt C - .github/workflows/ci.ymlname: CI on: [pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: { python-version: "3.11" } - run: pip install -e ".[dev]" - run: ruff check . - run: pytest -q
★ Prompt D - DockerfileFROM python:3.11-slim WORKDIR /app COPY pyproject.toml . RUN pip install --no-cache-dir -e . COPY src/ src/ CMD ["python", "-m", "retailpulse.pipeline"]
Real world

The green check that changed the culture. A data team added exactly this workflow to a two-year-old pile of scripts. In the first week it caught three broken imports and a hardcoded path that only worked on one laptop. Nobody had to be the bad cop in review anymore - the machine was. That is the quiet, real payoff of DataOps: fewer arguments, fewer 2am pages.

☁️ AWS mapping GitHub Actions is the OSS default here. The same job maps to AWS CodePipeline + CodeBuild (a buildspec.yml instead of a workflow file), or you keep Actions and just deploy to AWS. The container you built runs unchanged on ECS, Fargate, or Batch. The concept - automated lint/test gate before merge - is identical whichever you pick.
Homework

Try it yourself - this week ◐ 30-45 min total

Source material

What this session covers

This track teaches DataOps on an open-source stack from the tools' official docs, with AWS mapping notes. This page covers:

The DataOps definition + CI/CD conceptsPart 1 · the four DevOps moves applied to data
Trunk-based development + repo layoutPart 2 · flow, branch protection, the src/ layout
pre-commit + GitHub Actions docsPart 3 + Demo 2 · hooks and the CI workflow
Docker for Python appsDemo 2 · a minimal image; deeper Compose lands in b7
AWS CodePipeline / CodeBuild mappingDemo 2 sidebar · concept parity, not a full AWS tutorial
Check yourself

Three questions before you go 🎯 ◐ 90 seconds

1 · What does continuous integration (CI) actually guarantee?

CI is the automated gate on the way in - install, lint, test on every PR. Deploying is CD (continuous delivery), which comes later in the track.

2 · Why trunk-based development over heavy GitFlow for most data teams?

Data teams ship continuously and are usually small. One releasable main plus short branches gated by CI fits far better than develop/release/hotfix branches.

3 · Why should data/ be in .gitignore?

Never commit data. It bloats the repo forever (history keeps it), risks leaking confidential records, and mixes two things that version differently. Code in Git, data in storage.

Builder session 1 cheat sheet · pin this

DataOps in one lineDevOps discipline - version control, CI, CD, containers - applied to data, models, and schema.
The assembly lineCommit -> automated gate (lint/test/quality) -> repeatable run (container) -> production.
Git flowTrunk-based: one releasable main, short branches, small PRs, CI must pass. Protect main on day one.
Never commit datadata/, .env, model binaries -> .gitignore. Use check-added-large-files as insurance.
Two CI layerspre-commit (black/ruff, local) + GitHub Actions (install/lint/test on the PR, guards main).
Dockerpython:3.11-slim, install from pyproject.toml, COPY src, one CMD. Same image local, CI, prod.
AWS mappingActions ≈ CodePipeline + CodeBuild (buildspec.yml). Container runs on ECS/Fargate/Batch unchanged.
Running projectRetailPulse v0.1 lives: repo, pipeline, test, green CI. Next: b2 turns the script into an Airflow DAG.