All Modules Why CI/CD Concepts Hands-on Lab Cheat Sheet

CI/CD with GitHub Actions

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 min

What You'll Learn

By the end of this module you will be able to:

  • Explain what CI and CD mean — and why every team uses them
  • Read the GitHub Actions vocabulary: workflow, event, job, step, runner, action
  • Write a workflow that runs your tests automatically on every push
  • Add a job that builds your Docker image and pushes it to a registry
  • Use GitHub Secrets to handle credentials safely
  • Read the green/red checks and debug a failed pipeline

Prerequisites: Module 6 (Docker) + a free GitHub account and Git installed (Module 4).

Why CI/CD Exists

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.

Manual = slow + risky

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.

TermMeaningIn plain English
CIContinuous IntegrationOn every push, automatically test & build the code so problems surface instantly.
CDContinuous Delivery / DeploymentAutomatically ship the result — to a registry (Delivery) or straight to production (Deployment).

The payoff

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 Vocabulary

GitHub Actions is a free CI/CD tool built into every GitHub repo. Six words cover almost everything:

TermWhat it is
WorkflowThe whole automated process — a YAML file in .github/workflows/.
Event (trigger)What starts it: a push, a pull request, a schedule… (the on: key).
JobA group of steps that run together on one machine. Jobs can run in parallel or in sequence.
StepA single task in a job — either a shell command (run) or a reusable action (uses).
RunnerThe machine that executes a job (GitHub gives you free Linux runners).
ActionA pre-built, shareable step (e.g. actions/checkout) from the Marketplace.

How they nest

Workflow → triggered by an event → runs one or more jobs → each on a runner → each job has steps → steps run commands or use actions.

Hands-on Lab: Build the Pipeline

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.

1

Put the project on GitHub

Create an empty repo on GitHub, then from your project folder:

git init git add . git commit -m "Initial notes app" git branch -M main git remote add origin https://github.com/YOUR_USERNAME/notes-app.git git push -u origin main
2

Add a tiny test — tests/test_app.py

First add a /health route to app.py (it doesn't touch the database, so it's easy to test):

@app.route("/health") def health(): return "ok"

Now create tests/test_app.py:

from app import app def test_health(): client = app.test_client() resp = client.get("/health") assert resp.status_code == 200 assert resp.data == b"ok"

Add pytest to requirements.txt:

flask==3.0.3 psycopg2-binary==2.9.9 pytest==8.2.0

Run it locally once to confirm it passes: pip install -r requirements.txt && pytest

3

Write the CI job — .github/workflows/ci.yml

This is the heart of the module. On every push to main, GitHub spins up a runner, installs your deps, and runs the tests.

name: CI/CD on: push: branches: [main] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.12" - run: pip install -r requirements.txt - run: pytest

What each step does

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.

4

Push it and watch it run

git add . git commit -m "Add tests and CI workflow" git push

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.

The "aha!" moment

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.

5

Add the CD job: build & publish the image

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:

build-and-push: needs: test runs-on: ubuntu-latest permissions: contents: read packages: write steps: - uses: actions/checkout@v4 - uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - uses: docker/build-push-action@v6 with: context: . push: true tags: ghcr.io/${{ github.repository }}:latest

No secrets to set up here

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).

6

Using your own secrets (e.g. Docker Hub)

For any credential that isn't the built-in token, store it safely: repo → SettingsSecrets and variablesActionsNew repository secret. Then reference it as ${{ secrets.NAME }}:

- uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }}

Never hard-code credentials

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.

7

Push and watch the full pipeline

git add . git commit -m "Add build-and-push job" git push

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. ✅

8

Add a status badge (optional but satisfying)

Show the build status on your README:

![CI/CD](https://github.com/YOUR_USERNAME/notes-app/actions/workflows/ci.yml/badge.svg)

From Delivery to Deployment

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:

deploy: needs: build-and-push runs-on: ubuntu-latest steps: - run: kubectl set image deployment/web web=ghcr.io/${{ github.repository }}:latest # needs a kubeconfig secret + a cluster the runner can reach

Why we stop at Delivery here

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.

Workflow Cheat Sheet

The keys and actions you'll reach for constantly. Bookmark this.

Key / ActionWhat it does
on: pushTrigger the workflow on every push
on: pull_requestTrigger on PRs (great for testing before merge)
on: scheduleRun on a cron schedule (e.g. nightly)
on: workflow_dispatchAdd a manual "Run workflow" button
runs-on: ubuntu-latestPick the runner OS
needs: <job>Run this job only after another succeeds
uses: actions/checkout@v4Check out your repo onto the runner
uses: actions/setup-python@v5Install a Python version (also -node, -java, -go)
uses: docker/build-push-action@v6Build and push a Docker image
run: <command>Run a shell command
${{ secrets.NAME }}Read an encrypted secret
if: <condition>Run a step/job conditionally

Troubleshooting

SymptomLikely cause & fix
Workflow never runsFile must be in .github/workflows/ and end in .yml; check the on: branch matches.
YAML syntax errorIndentation — 2 spaces, no tabs (same rule as Compose).
pytest: command not foundpytest missing from requirements.txt, or the install step didn't run.
denied: permission on push to GHCRAdd permissions: packages: write to the job.
Secret is empty / login failsSecret name in YAML must match exactly; secrets aren't available to forks' PRs.
build-and-push runs even when tests failAdd needs: test so it waits for the test job.

Your Challenge

Make the pipeline yours before moving on:

  • Break a test on purpose, push, and confirm build-and-push is skipped (red check).
  • Also trigger the workflow on pull_request so PRs get tested before merge.
  • Tag the image with the commit SHA instead of latest (hint: ${{ github.sha }}).
  • Bonus: add a workflow_dispatch trigger so you can run it manually from the Actions tab.
with: context: . push: true tags: | ghcr.io/${{ github.repository }}:latest ghcr.io/${{ github.repository }}:${{ github.sha }}

Recap & What's Next

You can now

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.

CI/CD with Actions

Objectives Why CI/CD Vocabulary Hands-on Lab Delivery vs Deploy Cheat Sheet Troubleshooting Challenge Recap