Build a CI/CD Pipeline with GitHub Actions
Automate testing and Docker builds for a Python app using GitHub Actions.
Introduction
30 Second Summary
Imagine pushing a change to your team's code, only to find out hours later that you accidentally broke something. In the real world, teams prevent this with automated systems that test code the moment it changes.
In this project, you will build a fully automated pipeline that tests your Python app and packages it into a Docker container every time you push code to GitHub.
What You'll Build
A Python web application packaged in a Docker container, with a GitHub Actions workflow that automatically runs tests and builds the Docker image on every code push.
By the end of this project, you'll have:
- A working CI/CD pipeline that automatically tests and builds your code on every push.
- A containerized Python web application that runs identically on any machine.
- Hands-on experience with GitHub Actions, Docker, and automated testing used in real DevOps jobs.
- Secret Mission: Add a deployment gate that prevents untested code from deploying.
What tools do I need?
This project assumes you already have VS Code, Python 3.14, Git (Git Bash), a GitHub account, and Docker Desktop installed. No additional tools or accounts are needed.
Set Up Your Project
Every CI/CD pipeline needs a project to automate. Before you write any application code or pipeline configuration, you need a properly structured development environment with version control and isolated dependencies.
In this step, you'll create your project folder, initialize Git for version control, set up a Python virtual environment, install your dependencies, and confirm Docker is accessible.
In this step, get ready to:
- Create the project folder and initialize Git.
- Install Python dependencies.
- Verify Docker is running.
Create the project folder and initialize Git
You need a dedicated folder for your project, tracked by Git from the very beginning. This mirrors how real DevOps projects are structured.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Git Bash and press Enter to open it.
What is Git Bash?
Git Bash is a terminal application that comes bundled with Git on Windows. It gives you access to Linux-style commands (like mkdir and cd) on a Windows machine.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir hello-devops
cd hello-devops
- Confirm the folder exists by running:
pwd
You should see a path ending in hello-devops.
Don't see hello-devops in the path?
Make sure you typed mkdir hello-devops (not mkdr or mk dir). Check for typos in the folder name.
Help me troubleshoot my folder creation in Git Bash.
- Initialize a Git repository by running:
git init
What does git init do?
This command turns your folder into a Git repository. Git starts tracking every change you make, which is essential for triggering your CI/CD pipeline later.
You should see a message like Initialized empty Git repository in the output.
- Create a Python virtual environment by running:
python -m venv venv
What is a virtual environment?
A virtual environment is an isolated space for your project's Python packages. It prevents your project's dependencies from conflicting with other Python projects on your machine.
- Activate the virtual environment by running:
source venv/Scripts/activate
Why Scripts instead of bin?
On Windows, Python stores virtual environment scripts in a Scripts folder. On macOS/Linux it would be bin instead. Since you're using Git Bash on Windows, you use the Scripts path with the source command.
You should see (venv) appear at the beginning of your terminal prompt. This confirms your virtual environment is active.
Don't see (venv) in your prompt?
Make sure you're using Git Bash, not Command Prompt or PowerShell. The source command only works in Bash-style terminals.
Also check that the venv folder was created by running ls. You should see a venv folder in the output.
Help me activate my virtual environment in Git Bash on Windows.
- Confirm everything is working by running these two commands:
git status
python --version
You should see On branch main (or On branch master) from the first command, and Python 3.14 followed by a patch number from the second.
Install Python dependencies
Your project needs two packages: Flask for building the web application, and pytest for running automated tests.
- Install Flask and pytest by running:
pip install flask==3.1.3 pytest==9.1.1
Why pin specific versions?
Using ==3.1.3 and ==9.1.1 pins the exact version of each package. This ensures your project works the same way everywhere, which is a core principle of CI/CD.
You should see output ending with Successfully installed followed by a list of packages.
Seeing a permission error?
Make sure your virtual environment is active. Look for (venv) at the start of your prompt. If it's missing, re-run source venv/Scripts/activate before trying the install again.
Help me fix my pip install error.
- Generate a requirements.txt file by running:
pip freeze > requirements.txt
What is requirements.txt?
This file records every Python package installed in your virtual environment. Later, your Dockerfile and CI/CD pipeline will use this file to install the exact same packages automatically.
The pip freeze command captures all packages, including dependencies that Flask needs to work. You'll see additional entries beyond just Flask and pytest.
- Verify the packages are installed by running:
pip list
You should see Flask with version 3.1.3 and pytest with version 9.1.1 in the list.
Verify Docker is running
Your CI/CD pipeline will build a Docker container, so Docker Desktop needs to be running on your machine before you proceed.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type Docker Desktop and press Enter to open it.
- Wait for Docker Desktop to fully start up. You'll know it's ready when the dashboard loads and no loading spinners are visible.
Give Docker a minute
Docker Desktop can take 30-60 seconds to fully start, especially on first launch. Wait until the dashboard is fully loaded before running the next command.
- Switch back to Git Bash and confirm Docker is accessible by running:
docker --version
You should see a version number in the output, confirming Docker is ready to use.
Seeing 'command not found'?
This means Docker Desktop hasn't finished starting, or it isn't installed correctly. Close and reopen Docker Desktop, wait for it to fully load, then try the command again.
Help me get Docker working in Git Bash.
Your development environment is ready to go. Next up, you'll build the Python web application and write tests that your pipeline will run automatically.
Build Your Python App and Tests
Your development environment is ready. Now it is time to write the actual application that your pipeline will test and build.
Every CI/CD pipeline needs code to work with. In this step, you will create a simple Python web application, write automated tests for it, and set up a .gitignore file to keep unnecessary files out of your repository.
In this step, get ready to:
- Create a Flask web application with a health check endpoint.
- Write automated tests using pytest.
- Configure Git to ignore unnecessary files.
Create the Flask application
Your pipeline needs an application to test. You will build a minimal web API with Flask that responds to HTTP requests.
- Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
- Type VS Code and press Enter to open it.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop, select the hello-devops folder, and click Select Folder.
- Create a new file called app.py by clicking the new file icon in the Explorer sidebar, then typing app.py.
- Paste the following code into app.py:
from flask import Flask, jsonify
# Create the Flask application instance
app = Flask(__name__)
# Home route - returns a simple welcome message
@app.route("/")
def home():
return "Hello, DevOps!"
# Health check route - returns JSON with the app's status
@app.route("/health")
def health():
return jsonify({"status": "healthy"})
# Only run the server when this file is executed directly
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
What does this code do?
This file creates a small web server with two endpoints. The / route returns a plain text greeting. The /health route returns a JSON response that monitoring tools can check to confirm the app is alive.
The if __name__ == "__main__" block at the bottom lets you run the file directly with python app.py. It binds to 0.0.0.0 so the app is accessible when running inside a Docker container later.
- Save app.py by pressing Ctrl+S.
- Switch back to Git Bash and start the Flask development server by running this command:
python -m flask --app app run
You should see output including a line that says Running on http://127.0.0.1:5000. This means your server is running.
- Open your web browser and go to http://127.0.0.1:5000/health.
You should see {"status":"healthy"} displayed in the browser.
Not seeing the JSON response?
- Make sure you saved app.py before running the command.
- Check that your virtual environment is still active. You should see (venv) at the start of your terminal prompt.
- If port 5000 is already in use, stop the other process or close Docker Desktop temporarily.
- help me debug why my Flask app is not responding on localhost
Create the test file
Automated tests are the core of any CI/CD pipeline. They verify your code works correctly every time it runs, without a human clicking through the app.
- Stop the Flask server by pressing Ctrl+C in Git Bash.
- In VS Code, create a new file called test_app.py in the hello-devops folder.
- Paste the following code into test_app.py:
from app import app
def test_home():
# Create a test client to simulate HTTP requests
client = app.test_client()
response = client.get("/")
# Check that the response is successful and contains expected text
assert response.status_code == 200
assert b"Hello, DevOps!" in response.data
def test_health():
# Create a test client for the health endpoint
client = app.test_client()
response = client.get("/health")
# Check status code and JSON body match expectations
assert response.status_code == 200
assert response.get_json() == {"status": "healthy"}
What does this code do?
Each function starting with test_ is a test that pytest automatically discovers and runs. The app.test_client() creates a fake HTTP client that sends requests to your app without needing a running server.
The assert statements check that the response matches what you expect. If any assertion fails, pytest marks the test as failed and tells you exactly what went wrong.
- Save test_app.py.
- Run your tests by running this command in Git Bash:
pytest
You should see output ending with a green line that says 2 passed. This confirms both tests are working.
Tests not passing?
- Make sure you stopped the Flask server with Ctrl+C before running pytest.
- Check that test_app.py is in the same folder as app.py. The from app import app line needs both files side by side.
- Verify the strings in your test match your app exactly. The test checks for Hello, DevOps! with that exact capitalization and comma.
- help me figure out why my pytest tests are failing
Create a .gitignore file
Your project has a venv folder containing hundreds of dependency files. These do not belong in your Git repository because anyone can recreate them from requirements.txt.
- In VS Code, create a new file called .gitignore in the hello-devops folder.
- Paste the following into .gitignore:
venv/
__pycache__/
*.pyc
What does this file do?
A .gitignore file tells Git which files and folders to skip when tracking changes. The venv/ entry hides your virtual environment. The __pycache__/ and *.pyc entries hide compiled Python bytecode files that get auto-generated when you run your code.
- Save .gitignore.
- Verify it works by running this command in Git Bash:
git status
You should see app.py, test_app.py, requirements.txt, and .gitignore listed as untracked files. The venv folder should NOT appear in the list.
Still seeing venv in git status?
- Make sure the file is named exactly .gitignore (with the leading dot). On Windows, VS Code creates it correctly, but some file explorers hide the dot.
- Check that venv/ is on its own line in .gitignore with no extra spaces.
- help me figure out why my .gitignore is not working
✔️ Awesome, I've got everything!
Great. Double check you have saved all three files before moving on.
ⓧ I'd like to double check the full code
Here is the full code for each file at this point:
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/")
def home():
return "Hello, DevOps!"
@app.route("/health")
def health():
return jsonify({"status": "healthy"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
from app import app
def test_home():
client = app.test_client()
response = client.get("/")
assert response.status_code == 200
assert b"Hello, DevOps!" in response.data
def test_health():
client = app.test_client()
response = client.get("/health")
assert response.status_code == 200
assert response.get_json() == {"status": "healthy"}
venv/
__pycache__/
*.pyc
flask==3.1.3
pytest==9.1.1
Your application, tests, and Git configuration are all in place. Next up, you will package this app into a Docker container so it can run identically on any machine.
Containerize Your App with Docker
Your Flask app runs perfectly on your machine and your tests confirm it works. But what happens when you hand your code to a teammate, or deploy it to a server with a different Python version?
Docker solves this by packaging your app into a container. A container bundles your code, dependencies, and runtime into one portable unit that runs identically anywhere. In this step, you'll write a Dockerfile, build your first container image, and run your app inside it.
In this step, get ready to:
- Create a Dockerfile that packages your Flask app.
- Build and run the Docker container locally.
- Verify the health endpoint works from inside the container.
Create the Dockerfile
A Dockerfile is a recipe that tells Docker how to build your container image. Each instruction in the file adds a layer to the image, starting from a base image and ending with the command that runs your app.
- In VS Code, create a new file called Dockerfile (no file extension) in your hello-devops folder.
- Paste the following code into Dockerfile:
# Use a minimal Python base image
FROM python:3.14-slim
# Set the working directory inside the container
WORKDIR /app
# Copy requirements first for better layer caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the application code
COPY . .
# Declare the port the app listens on
EXPOSE 5000
# Start the Flask app when the container runs
CMD ["python", "app.py"]
What does this Dockerfile do?
FROM python:3.14-slim starts from an official Python image that's small and fast to download.
WORKDIR /app creates a folder inside the container where your code lives. COPY requirements.txt . and RUN pip install install your dependencies before copying the rest of your code. This order means Docker can cache the dependency layer and skip reinstalling if only your app code changes.
COPY . . copies your application files into the container. EXPOSE 5000 documents which port the app uses. CMD ["python", "app.py"] tells Docker what command to run when the container starts.
- Save Dockerfile.
File named Dockerfile.txt instead?
Windows sometimes adds a hidden .txt extension. In VS Code's Explorer sidebar, confirm the file is named exactly Dockerfile with no extension. If you see Dockerfile.txt, right-click the file and select Rename to remove the extension.
Help me fix my Dockerfile name
✔️ Awesome, I've got everything!
Great. Double check you've saved Dockerfile and it starts with FROM python:3.14-slim.
ⓧ I'd like to double check the full code
FROM python:3.14-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
Build and run the Docker container locally
With your Dockerfile ready, you can now build an image and start a container from it. Building reads your Dockerfile instructions and produces a reusable image.
- Build your Docker image by running this command in Git Bash:
docker build -t hello-devops .
What does this command do?
docker build reads the Dockerfile in the current directory (the . at the end). -t hello-devops tags the resulting image with the name hello-devops so you can reference it later.
- Wait for the build to finish. You should see Docker pulling the base image, installing dependencies, and copying your files.
- Start a container from your new image by running:
docker run -d -p 5000:5000 --name devops-app hello-devops
What does this command do?
-d runs the container in the background (detached mode). -p 5000:5000 maps port 5000 on your machine to port 5000 inside the container.
--name devops-app gives the container a friendly name so you can reference it easily. hello-devops at the end tells Docker which image to use.
- Open http://127.0.0.1:5000/health in your browser.
- You should see {"status": "healthy"} displayed on the page. This response is coming from your app running inside the Docker container.
Don't see the health response?
Make sure the Flask dev server from Step 2 is not still running. If it is, both processes compete for port 5000. Stop the old server with Ctrl+C in the terminal where it was running, then try the browser again.
Run docker logs devops-app to see if the container started correctly. If you see an error, double check your Dockerfile matches the code above.
Help me troubleshoot my Docker container
Stop and clean up the container
You've confirmed the container works. Now clean it up so the port is free for the next time you need it.
- Stop and remove the container by running these commands:
docker stop devops-app
docker rm devops-app
What do these commands do?
docker stop devops-app gracefully shuts down the running container. docker rm devops-app removes the stopped container so it no longer takes up space on your system.
- Confirm no containers are running by running:
docker ps
- You should see only the header row with no containers listed beneath it. This confirms your container was removed successfully.
Still see the devops-app container?
If docker stop returned an error, the container might have already stopped on its own. Try running docker rm devops-app directly. If that also fails, run docker rm -f devops-app to force-remove it.
Help me remove my Docker container
Your app is containerized and proven to run identically inside Docker. Next up, you'll push everything to GitHub and create your CI/CD pipeline to automate testing and building.
Push to GitHub and Create Your Pipeline
Your app is containerized and runs identically inside Docker. Now it's time to put your code somewhere the whole world (and your future pipeline) can access it.
In this step, you'll push your project to GitHub and then write a GitHub Actions workflow file that defines your CI/CD pipeline. This single YAML file tells GitHub exactly what to do every time you push new code.
In this step, get ready to:
- Create a public GitHub repository and push your code.
- Connect your local Git repo to the remote and verify your files appear on GitHub.
- Write a GitHub Actions workflow file that automates testing and building.
Create a GitHub repository
You need a remote repository on GitHub to store your code and run your pipeline. The repository must be public so that GitHub Actions runs with unlimited free minutes.
- Open your browser and go to github.com/new.
- Type hello-devops in the Repository name field.
- Select Public for the repository visibility.
- Leave all checkboxes unchecked (no README, no .gitignore, no license). You already have local files.
- Click Create repository.
- On the next page, copy the HTTPS URL shown under Quick setup. It looks like https://github.com/your-username/hello-devops.git.
Why must the repo be public?
GitHub Actions gives unlimited free minutes to public repositories. Private repos on the free plan have a 2,000 minute per month cap. Keeping yours public means your pipeline will always run without cost.
Connect your local repo and push
Your local Git repository needs to know where the remote lives. You'll stage all your files, commit them, then push everything to GitHub.
- Stage all your project files by running this command in Git Bash:
git add .
- Commit the staged files by running:
git commit -m "Initial commit: Flask app with tests and Dockerfile"
- Connect your local repo to the GitHub remote by running:
git remote add origin [[REPO_URL="your GitHub repository HTTPS URL"]]
- Rename your branch to main and push by running:
git branch -M main
git push -u origin main
What do these commands do?
- git add . stages every file in your project folder for the next commit.
- git commit -m saves a snapshot of your staged files with a message describing the change.
- git remote add origin tells Git where your GitHub repository lives.
- git branch -M main renames your current branch to main (the standard default branch name on GitHub).
- git push -u origin main uploads your commit to GitHub and sets origin main as the default push target.
- Refresh your GitHub repository page in the browser.
- You should see all your project files listed: app.py, test_app.py, Dockerfile, requirements.txt, and .gitignore.
Push failed or asking for credentials?
- If Git asks for a username and password, GitHub no longer accepts passwords for HTTPS. You need a Personal Access Token. Go to Settings > Developer settings > Personal access tokens > Tokens (classic) on GitHub and generate one with the repo scope. Use that token as your password.
- If you see "remote origin already exists", you may have run the command twice. Run git remote -v to check. If the URL is wrong, fix it with git remote set-url origin <correct-url>.
Why can't I push to GitHub?
Create the GitHub Actions workflow
A GitHub Actions workflow is a YAML file that lives inside a special folder in your repository. When you push code, GitHub reads this file and runs the steps you defined automatically.
- Create the required folder structure by running:
mkdir -p .github/workflows
Why this specific folder?
GitHub Actions only looks for workflow files inside the .github/workflows directory. The .yml or .yaml extension tells GitHub this is a workflow definition. Any YAML file placed here becomes a pipeline.
- In VS Code, create a new file at .github/workflows/ci.yml.
- Paste the following workflow into ci.yml:
name: CI/CD Pipeline
on:
push:
branches: [main]
jobs:
test-and-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: pytest
- name: Build Docker image
run: docker build -t hello-devops .
What does this workflow do?
- on: push: branches: [main] triggers the workflow every time you push to the main branch.
- runs-on: ubuntu-latest spins up a fresh Linux virtual machine on GitHub's servers to run your pipeline.
- actions/checkout@v6 pulls your repository code into that virtual machine.
- actions/setup-python@v5 installs Python 3.14 so your project can run.
- The final three steps install dependencies, run your tests with pytest, and build the Docker image. If any step fails, the pipeline stops and reports a failure.
- Save ci.yml.
- Confirm the file exists by running:
ls .github/workflows/
You should see ci.yml listed in the output.
Don't see the file?
- Make sure you created the file inside .github/workflows/ (note the dot at the start of .github). Folders starting with a dot are hidden by default.
- Check for typos in the folder path. It must be exactly .github/workflows/ci.yml with no spaces or capital letters.
Help me find my ci.yml file
✔️ Awesome, I've got everything!
Great. Double check you have saved ci.yml and you are ready for the next step.
ⓧ I'd like to double check the full code
Here is the complete ci.yml file for comparison:
name: CI/CD Pipeline
on:
push:
branches: [main]
jobs:
test-and-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.14"
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run tests
run: pytest
- name: Build Docker image
run: docker build -t hello-devops .
Your pipeline is written and ready to go. In the next step, you'll push this workflow to GitHub and watch it run in real time with green checkmarks confirming everything works.
Trigger Your Pipeline and Watch It Run
Your pipeline is defined, your workflow file is ready, and your repository is waiting. Now it's time to trigger the automation and watch GitHub Actions do the heavy lifting.
In this step, you'll push the workflow file to GitHub, watch the pipeline run your tests and build your Docker image automatically, then make a code change to prove the pipeline triggers on every push.
In this step, get ready to:
- Push your workflow file to GitHub and trigger the pipeline.
- Watch the pipeline execute and verify all steps pass with green checkmarks.
- Add a new feature and confirm the pipeline runs automatically on every push.
Push the workflow to GitHub
Your ci.yml workflow file exists locally but GitHub doesn't know about it yet. Pushing it to the remote repository is what activates the pipeline.
- Stage, commit, and push your workflow file by running these commands in Git Bash:
git add .github/
git commit -m "Add CI/CD pipeline"
git push
What do these commands do?
- git add .github/ stages the entire .github/workflows folder (including your ci.yml file) for commit.
- git commit -m "Add CI/CD pipeline" saves the staged changes with a descriptive message.
- git push sends the commit to GitHub, which triggers the workflow because the file now exists in the .github/workflows directory on the main branch.
- Confirm the push completes without errors. You should see output ending with a line like main -> main in your terminal.
Push failed or rejected?
- If you see a "rejected" error, make sure you ran git pull origin main first to sync any remote changes, then try git push again.
- If you see an authentication error, confirm your GitHub credentials are configured. You may need to sign in via the browser prompt that appears.
Help me fix a git push error in my CI/CD pipeline project.
Watch the pipeline run
The moment your push lands on GitHub, the workflow triggers automatically. You can watch it execute in real time from the Actions tab.
- Switch to your browser and go to your hello-devops repository on GitHub.
- Click the Actions tab at the top of the repository page.
- You should see a workflow run listed. A yellow dot means it is still in progress. A green checkmark means it completed successfully.
- Click on the run name (it matches your commit message, "Add CI/CD pipeline") to see the workflow run summary.
- Click on the test-and-build job to expand its detailed logs.
What should I see in the logs?
Each step in your workflow appears as an expandable section. When everything passes, you will see:
- Set up Python installs Python 3.14 on the runner.
- Install dependencies runs pip install from your requirements.txt.
- Run tests shows pytest output with "2 passed" in the log.
- Build Docker image builds your container from the Dockerfile.
- Confirm all steps show green checkmarks and the run is marked as successful.
Seeing a red X on your workflow run?
- Click the failed step to expand its logs. The error message tells you exactly what went wrong.
- If the "Run tests" step fails, double check that your test_app.py file was committed and pushed. Run git status locally to verify.
- If the "Build Docker image" step fails, confirm your Dockerfile and requirements.txt are both in the repository.
Help me debug a failed GitHub Actions workflow run.
Make a change and watch the pipeline trigger again
A pipeline that runs once is good. A pipeline that runs on every push is powerful. Let's prove yours triggers automatically by adding a new feature to your app.
- Switch to VS Code and open app.py.
- Find the health() function. You will add a new route directly below it, above the if __name__ block.
- Add a new /version route by copying this code below the health() function:
@app.route("/version")
def version():
return jsonify({"version": "1.0.0"})
What does this route do?
This adds a /version endpoint that returns the app's current version as JSON. In real applications, version endpoints help operations teams confirm which version of code is deployed.
- Save app.py.
✔️ Awesome, I've got everything!
Great. Make sure you saved the file before moving on.
ⓧ I'd like to double check the full code
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/")
def home():
return "Hello, DevOps!"
@app.route("/health")
def health():
return jsonify({"status": "healthy"})
@app.route("/version")
def version():
return jsonify({"version": "1.0.0"})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Now you need a test for the new route. Every feature deserves a test so the pipeline can catch breakages automatically.
- Open test_app.py in VS Code.
- Add the following test function at the bottom of the file, below the test_health() function:
def test_version():
client = app.test_client()
response = client.get("/version")
assert response.status_code == 200
assert response.get_json() == {"version": "1.0.0"}
What does this test check?
- It creates a test client and sends a GET request to /version.
- It asserts the response status code is 200 (success).
- It asserts the JSON body matches {"version": "1.0.0"} exactly.
- Save test_app.py.
✔️ Awesome, I've got everything!
Great. Make sure you saved the file before moving on.
ⓧ I'd like to double check the full code
from app import app
def test_home():
client = app.test_client()
response = client.get("/")
assert response.status_code == 200
assert b"Hello, DevOps!" in response.data
def test_health():
client = app.test_client()
response = client.get("/health")
assert response.status_code == 200
assert response.get_json() == {"status": "healthy"}
def test_version():
client = app.test_client()
response = client.get("/version")
assert response.status_code == 200
assert response.get_json() == {"version": "1.0.0"}
- Verify your tests pass locally before pushing by running this command:
pytest
- You should see 3 passed in green in your terminal output.
Not seeing 3 passed?
- If you see "2 passed", your test_version() function might not be saved in test_app.py. Check that you saved the file.
- If you see a failure in test_version, confirm the route in app.py returns {"version": "1.0.0"} exactly (check your quotes and spelling).
Help me fix my failing pytest tests.
- Now push your changes to GitHub by running these commands:
git add .
git commit -m "Add version endpoint"
git push
- Switch back to your browser and click the Actions tab on your hello-devops repository.
- You should see a new workflow run appear (triggered by your "Add version endpoint" commit).
- Wait for it to complete and confirm all steps show green checkmarks. The "Run tests" step now shows "3 passed".
Why does this matter?
You just proved that your pipeline is truly automated. Every time you push code, GitHub Actions re-runs your entire test suite and rebuilds the Docker image. If anything breaks, the pipeline catches it before the code can do damage.
This is Continuous Integration in action. Code is continuously tested and validated as part of the normal development workflow.
Secret mission
Add a Deployment Gate
In real DevOps, you never deploy code that has not passed tests. Your pipeline currently tests and builds in one job. Can you add a second job that simulates deployment, but only runs when tests pass?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely for free. Your GitHub repository is public with unlimited Actions minutes, and everything else runs locally. There are no ongoing costs to worry about.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Local hello-devops project folder (includes virtual environment, code files, and Dockerfile).
- GitHub repository hello-devops (public, with GitHub Actions workflow).
- Docker image hello-devops (built locally).
Keep everything running
No action needed. Choose this if you want to keep your repository visible on GitHub or plan to extend the project further.
- Your GitHub repository stays public and accessible.
- Your local hello-devops folder stays on your Desktop.
- The Docker image remains cached locally and uses some disk space.
Pause - I'll come back to this later
Free up local disk space while keeping your GitHub repository intact so you can clone it again later.
- Remove the local Docker image by running this command in Git Bash:
docker rmi hello-devops
- Your GitHub repository stays online with your full commit history and pipeline configuration.
- You can re-clone and rebuild anytime with git clone and docker build.
Delete - I don't want to use this again
Remove all project resources and start fresh.
- Remove the local Docker image:
docker rmi hello-devops
- Deactivate the virtual environment and delete the local project folder:
deactivate
cd ~/Desktop
rm -rf hello-devops
- Delete the GitHub repository:
- Go to your hello-devops repository on GitHub.
- Click Settings in the repository navigation bar.
- Scroll down to the Danger Zone section.
- Click Delete this repository.
- Click I want to delete this repository.
- Click I have read and understand these effects.
- Type the repository name to confirm, then click Delete this repository.
Nice Work!
Nice Work!
You just built a fully automated CI/CD pipeline using GitHub Actions. Every time you push code, your pipeline automatically tests it and builds a Docker image.
Here's what you accomplished:
- Built and containerized a Python Flask web application with Docker, so it runs identically on any machine.
- Created a CI/CD pipeline with GitHub Actions that automatically runs tests and builds your Docker image on every push to main.
- Wrote automated tests with pytest and configured workflow triggers, jobs, and the .github/workflows directory to automate your entire DevOps process.
- Secret Mission: Added a deployment gate using the needs keyword to create job dependencies that prevent broken code from deploying.
Ready to quiz yourself?