Project Introduction
Even before you starting writing first line in your CI/CD workflow, it is always to have general idea about the project which will better shape you to write better pipeline that has less chances to do things wrong. Everything you do after this chapter depends on this one.
The project is intentionally simple because practice gets much easier when the project itself is small enough that you can understand every moving part. Before we start building CI/CD for project, let’s first make sure we know exactly what the pipeline will be testing.
This is a small Python client-server project, made available at my own repo. The application is intentionally simple: one program waits for a connection and sends a file, while another connects to it and receives that file. That gives us just enough code to demonstrate testing and CI/CD without getting buried in application complexity.
Fork and Clone The Project
The Zeroth towards writing any workflow is making the project available as your local repository. So, fork and then clone (maybe give a star too) the devstuffs-building-real-ci-pipelines repository.
Kudos to the OG creators: This project is replicated from The Algorithms/Python/file_transfer. The main idea behind choosing this project is to choose random and easy project from the internet to get started and being luckly we don’t have to write tests by hand โ it’s already there.
The Project Structure
The fork looks like this:
.
โโโ .github
โย ย โโโ actions
โย ย โย ย โโโ setup-python-env
โย ย โย ย โโโ action.yml
โย ย โโโ workflows
โย ย โโโ ci.yml
โย ย โโโ matrix-tests.yml
โโโ .gitignore
โโโ README.md
โโโ __init__.py
โโโ mytext.txt
โโโ receive_file.py
โโโ requirements.txt
โโโ scripts
โย ย โโโ summarize_report.py
โโโ send_file.py
โโโ tests
โโโ __init__.py
โโโ test_send_file.pyClone the repository and intentionally delete the following because we will be learning these all in this section of Building Real CI Pipelines:
git clone https://github.com/mragochar/devstuffs-building-real-ci-pipelines.git
cd devstuffs-building-real-ci-pipelines/
rm -r .github scripts requirements.txttreeNow you are ready to start:
.
โโโ README.md
โโโ __init__.py
โโโ mytext.txt
โโโ receive_file.py
โโโ send_file.py
โโโ tests
โโโ __init__.py
โโโ test_send_file.pyThere are really only three pieces we care about right now:
send_file.pyโ the serverreceive_file.pyโ the clienttests/test_send_file.pyโ the automated test
mytext.txt is the file transferred between the two programs.
The Small Change In The Fork
The fork makes one functional change to the test file.
Originally, the tests/test_send_file.py imported the function like this:
from file_transfer.send_file import send_fileIt was changed to:
from send_file import send_fileWhy?
We are going to run the test from the root of this project, where send_file.py is directly available as a module. Python includes this path (working directory) already in its module search path (sys.path), so the test can import send_file directly.
This also makes the example convenient for the CI pipeline we are about to build: the repository root is the working directory, and the test command can run against the tests/ directory from there.
Note
This is a small project-specific change, not a general rule that one import style is always better than the other. The important thing is that the import matches the way the project is being executed and packaged.
Understanding The Application
Before automating anything, let’s run the application manually. CI/CD will eventually do some of this work automatically, so you should first see what the application actually does.
Client-Server Model
Think of the two Python files as two ends of a network connection:
flowchart LR
A["receive_file.py<br/>Client"] -->|"connects to TCP port 12312"| B["send_file.py<br/>Server"]
B -->|"sends mytext.txt"| A
A -->|"writes received data"| C["Received_file"]
send_file.py creates a socket, listens on TCP port 12312, and waits for clients. When a client connects, it sends the contents of mytext.txt.
receive_file.py acts as the client. It connects to port 12312, receives the data, and writes it to a file named Received_file.
There is one detail worth making clear because the words server and client can otherwise make the behavior sound more complicated than it is.
The server stays alive and keeps listening for connections. It doesn’t shut down after serving one client.
The individual client connection does not stay open forever. Once the server has finished sending the file, that connection is closed. The client also closes its side after successfully receiving the file.
So the lifecycle looks like this:
sequenceDiagram
participant S as send_file.py
participant C as receive_file.py
S->>S: Listen on port 12312
C->>S: Connect
S->>C: Send mytext.txt
S-->>C: Close client connection
C->>C: Save as Received_file
C-->>S: Client connection closed
Note over S: Server continues listening
That distinction matters: the server keeps serving, while each client connection is temporary.
See It In Action
Let’s run the application manually before worrying about automation.
1. Start The Server
Open one terminal in the project directory and start the server:
python3 send_file.pyYou should see:
Server listening....At this point, the server is waiting for a client.
2. Start The Client
Open another terminal in the same project directory and run:
python3 receive_file.pyThe client should show something similar to:
file.py
File opened
Receiving data...
Successfully received the file
Connection closedMeanwhile, the server will report the connection and the data it sent:
Got connection from ('127.0.0.1', 35282)
Server received: data = b'Hello server!'
Sent b'Hello\nThis is sample data\n\xc2\xabk\xc3\xbc\xc3\x9f\xc3\xae\xc2\xbb\n\xe2\x80\x9c\xd0\x8c\xcf\x8d\xd0\x91\xd0\x87\xe2\x80\x9d\n\xf0\x9f\x98\x80\xf0\x9f\x98\x89\n\xf0\x9f\x98\x8b\n'
Done sendingThe exact client port, such as 35282, can be different on every run. It is the temporary source port chosen for that client connection.
The important part is the sequence:
- The server starts listening.
- The client connects.
- The server sends
mytext.txt. - The server finishes that client connection.
- The client writes the received data to
Received_file. - The server remains available for another client.
3. Verify The Result
The simplest way to inspect the received file is:
cat Received_fileYou should get the same contents as mytext.txt:
Hello
This is sample data
ยซkรผรรฎยป
โะฯะะโ
๐๐
๐We can verify that this isn’t merely visually the same file.
Use diff:
diff mytext.txt Received_fileIf the files are identical, diff produces no output.
That gives us a useful definition of success for this application:
The client successfully receives the file when
Received_fileis identical tomytext.txt.
This is exactly the sort of behavior we want automated tests to verify. Instead of manually starting programs and looking at their output every time someone changes the code, we can let a test check the important behavior for us.
How Testing Looks
The project already contains a test under tests/.
We use pytest to run it. If you don’t have it installed yet:
pip3 install pytestThen run the tests from the root of the project:
python3 -m pytest tests/A successful run looks like this:
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-9.1.1, pluggy-1.6.0
rootdir: /home/devstuffs/building-real-ci-pipelines
collected 1 item
tests/test_send_file.py . [100%]
============================== 1 passed in 0.05s ===============================The important line is:
1 passedThere is currently only one test, so the test suite is deliberately tiny. That’s fine. We aren’t trying to build a sophisticated Python application here.
The application gives us something much more useful for learning CI/CD: a small, reproducible project with an automated test that can tell us whether a change still passes.
That is the foundation our pipeline will use.
Why Start Here?
CI/CD is not magic that somehow makes an application reliable. A pipeline simply automates steps that you could otherwise perform manually.
For this project, we already have the essential ingredients:
flowchart LR
A["Developer changes code"] --> B["Run automated test"]
B --> C{"Test passes?"}
C -->|"Yes"| D["Safe to continue pipeline"]
C -->|"No"| E["Stop and report failure"]
Right now, you have to remember to run:
python3 -m pytest tests/After we build the CI/CD pipeline, the automation will run that command for us when a change reaches the repository.
That is the point of this exercise. The application is intentionally simple so that we can concentrate on the pipeline itself rather than spending the lesson debugging a complicated application.
Tip
When learning CI/CD, a small project is often better than a realistic one. If the application has only a few moving parts, you can clearly see what the pipeline is doing, why it failed, and what each stage is actually checking.