Every git push tests, builds, and ships your app — automatically.
Module 9 · We put the notes app on autopilot. Free · Runs on GitHub.
Beginner+ Automation ~55 minBy the end of this module you will be able to:
Prerequisites: Module 6 (Docker) + a free GitHub account and Git installed (Module 4).
So far, every time you changed the notes app you had to remember to: run the tests, build the image, push it, then redeploy. By hand. Every time. Miss a step and broken code ships.
Humans forget steps, skip tests when rushed, and do things slightly differently each time. The result: "it worked locally but broke in production" — the exact problem DevOps exists to kill.
CI/CD automates the whole path from code to running app. You push code; a robot does the rest, the same way, every time.
| Term | Meaning | In plain English |
|---|---|---|
| CI | Continuous Integration | On every push, automatically test & build the code so problems surface instantly. |
| CD | Continuous Delivery / Deployment | Automatically ship the result — to a registry (Delivery) or straight to production (Deployment). |
Faster releases, fewer bugs in production, and confidence to ship small changes often. This is the single biggest productivity multiplier in modern software.
GitHub Actions is a free CI/CD tool built into every GitHub repo. Six words cover almost everything:
| Term | What it is |
|---|---|
| Workflow | The whole automated process — a YAML file in .github/workflows/. |
| Event (trigger) | What starts it: a push, a pull request, a schedule… (the on: key). |
| Job | A group of steps that run together on one machine. Jobs can run in parallel or in sequence. |
| Step | A single task in a job — either a shell command (run) or a reusable action (uses). |
| Runner | The machine that executes a job (GitHub gives you free Linux runners). |
| Action | A pre-built, shareable step (e.g. actions/checkout) from the Marketplace. |
Workflow → triggered by an event → runs one or more jobs → each on a runner → each job has steps → steps run commands or use actions.
We'll put the notes app on GitHub and build a pipeline that tests it, then builds and publishes its Docker image — all on every push. Start in your notes-app folder.
Create an empty repo on GitHub, then from your project folder:
tests/test_app.pyFirst add a /health route to app.py (it doesn't touch the database, so it's easy to test):
Now create tests/test_app.py:
Add pytest to requirements.txt:
Run it locally once to confirm it passes: pip install -r requirements.txt && pytest
.github/workflows/ci.ymlThis is the heart of the module. On every push to main, GitHub spins up a runner, installs your deps, and runs the tests.
checkout copies your repo onto the runner · setup-python installs Python · the two run steps install deps and run the tests. If pytest fails, the whole job goes red.
Open your repo on GitHub → the Actions tab. You'll see your workflow running live. A green check ✅ means your tests passed automatically — no laptop required.
You just delegated testing to a robot that runs on every push, forever. Break a test on purpose and watch the check turn red — that's CI catching bugs before they spread.
Now add a second job that runs only if tests pass (needs: test), builds the Docker image, and pushes it to GitHub's free registry (GHCR). Append this under jobs: in the same file:
secrets.GITHUB_TOKEN is provided automatically by GitHub for every workflow — that's why GHCR is the easiest registry to start with. For Docker Hub you'd add your own secrets (next step).
For any credential that isn't the built-in token, store it safely: repo → Settings → Secrets and variables → Actions → New repository secret. Then reference it as ${{ secrets.NAME }}:
Passwords and tokens go in Secrets, never in the YAML or your code. Secrets are encrypted and masked in logs — this is the cardinal rule of CI/CD.
In the Actions tab you'll now see two jobs: test runs first, then build-and-push. When it's green, your freshly built image is published at ghcr.io/YOUR_USERNAME/notes-app:latest — ready for anyone (or your Kubernetes cluster from Module 8) to pull. ✅
Show the build status on your README:
What you built is Continuous Delivery: every push produces a ready-to-ship image. The last step — Continuous Deployment — is automatically rolling that image out. Conceptually you add one more job:
Real deployment needs a running cluster the GitHub runner can reach (a cloud cluster + a KUBECONFIG secret). Your local Minikube from Module 8 isn't reachable from GitHub's runners. We'll wire up a real auto-deploy in the Capstone (Module 12). For now, know the shape: test → build → deploy.
The keys and actions you'll reach for constantly. Bookmark this.
| Key / Action | What it does |
|---|---|
on: push | Trigger the workflow on every push |
on: pull_request | Trigger on PRs (great for testing before merge) |
on: schedule | Run on a cron schedule (e.g. nightly) |
on: workflow_dispatch | Add a manual "Run workflow" button |
runs-on: ubuntu-latest | Pick the runner OS |
needs: <job> | Run this job only after another succeeds |
uses: actions/checkout@v4 | Check out your repo onto the runner |
uses: actions/setup-python@v5 | Install a Python version (also -node, -java, -go) |
uses: docker/build-push-action@v6 | Build and push a Docker image |
run: <command> | Run a shell command |
${{ secrets.NAME }} | Read an encrypted secret |
if: <condition> | Run a step/job conditionally |
| Symptom | Likely cause & fix |
|---|---|
| Workflow never runs | File must be in .github/workflows/ and end in .yml; check the on: branch matches. |
| YAML syntax error | Indentation — 2 spaces, no tabs (same rule as Compose). |
pytest: command not found | pytest missing from requirements.txt, or the install step didn't run. |
denied: permission on push to GHCR | Add permissions: packages: write to the job. |
| Secret is empty / login fails | Secret name in YAML must match exactly; secrets aren't available to forks' PRs. |
| build-and-push runs even when tests fail | Add needs: test so it waits for the test job. |
Make the pipeline yours before moving on:
build-and-push is skipped (red check).pull_request so PRs get tested before merge.latest (hint: ${{ github.sha }}).workflow_dispatch trigger so you can run it manually from the Actions tab.Write a GitHub Actions workflow that tests your code, builds your Docker image, and publishes it — automatically, on every push, with secrets handled safely. That's a real CI/CD pipeline.
Next up: Module 10 — Infrastructure as Code, where you'll define servers and infrastructure in code with Terraform & Ansible — so your environment is as repeatable as your pipeline.