If you have made it this far, it is because at some point you have lived through — or been told about — a scene like this one: Friday afternoon, someone builds the project on their laptop, uploads the files to a server and crosses their fingers. This lesson is the starting point for leaving that scene behind. We are going to define precisely what the acronym CI/CD means, distinguish the three practices hiding behind it (because there are three, not two) and settle the vocabulary we will use throughout the course. This is not a lesson about tools or configuration: it is the lesson that makes everything else make sense. If you confuse Continuous Delivery with Continuous Deployment — the industry's most common mistake — the rest of the course will sound like noise. By the end, you will be able to name every piece of the process and you will understand why the team behind our example project, Reservalia, has a serious problem.
Contents
- The problem CI/CD sets out to solve
- What continuous integration (CI) is
- What continuous delivery is
- What continuous deployment is
- The two "CD"s head to head
- The full flow: from commit to production
- Core vocabulary for the course
- Common mistakes and tips
- Exercises
- Conclusion
- The problem CI/CD sets out to solve
1.1. Where Reservalia starts
Throughout the course we will work on a fictional project called Reservalia, a SaaS appointment booking platform for small businesses: hair salons, dental clinics, car repair shops. The team is small: Marta (tech lead), Diego (backend developer) and Nuria (SRE). We will look at it in detail in lesson 01-04; for now all that matters is how they work today:
- Diego builds on his laptop. The version of Node he has installed is not exactly the one on the server.
- He uploads the resulting files over SFTP to a single production server.
- He runs the database migrations by hand, pasting SQL into a
psqlconsole. - Tests are run "whenever someone remembers".
- All of this happens on Friday afternoons and takes about 3 hours.
- The result: two serious production incidents last quarter.
This process is not bad through carelessness: it is bad by design. Every one of those steps depends on one specific person, at one specific moment, not making a mistake. And people do make mistakes.
1.2. Integration hell
There is a second problem, less visible but just as expensive. Imagine Marta and Diego each work on their own branch for two weeks:
- Marta rewrites the email notification system.
- Diego changes the appointment data model to support recurring bookings.
Neither of them touches the other's work, apparently. But the day they try to merge both branches, conflicts appear in 40 files, the signature of a function they both use has changed, and Marta's tests fail because of a schema change Diego made. What should have been an afternoon turns into three days of "sorting out the integration".
That is integration hell: the cost of merging grows non-linearly with the time branches live apart.
graph LR
subgraph "Late integration: concentrated pain"
A1[Day 1] --> A2[Day 5] --> A3[Day 10] --> A4["Day 14<br/>MERGE<br/>3 days of conflicts"]
end
subgraph "Continuous integration: pain spread out"
B1["Day 1<br/>merge"] --> B2["Day 2<br/>merge"] --> B3["Day 3<br/>merge"] --> B4["Day 14<br/>merge<br/>10 minutes"]
end
The key intuition, which will come back again and again in this course: if something hurts, do it more often. Merging hurts because you do it rarely. Deploying hurts because you do it rarely. The counter-intuitive answer from CI/CD is to increase the frequency until each individual operation becomes trivial.
- What continuous integration (CI) is
Definition. Continuous integration is the practice whereby every team member integrates their work into the mainline at least once a day, and every integration is verified automatically by a build-and-test process that runs in a clean, shared environment.
There are three ideas inside that definition, and all three matter equally:
- Integrate often. Each person's code returns to the main branch within hours, not weeks. If your branch lives for three weeks, you are not doing CI even if you have a CI server configured.
- Verify automatically. Merging is not enough: an automated system builds, runs the tests and checks quality. Without automated verification, integrating often just means breaking the main branch more often.
- In a clean, shared environment. Not on Diego's laptop. On a machine that starts from scratch every time, so that "it works on my machine" stops being a valid sentence.
2.1. What CI is not
It is worth defusing three very widespread misunderstandings:
| Misunderstanding | Reality |
|---|---|
| "We have CI because we have a CI server" | If branches live for weeks, you have a build server, not continuous integration. |
| "CI means running tests" | Running tests is part of CI. CI is the practice of integrating; tests are the verification mechanism. |
| "CI is the tool's job" | CI is above all a team agreement: what happens when the build fails, how long a branch may live, who fixes what. |
2.2. The implicit CI contract
When a team genuinely adopts CI, it signs an unwritten contract:
- The main branch is always healthy. If the build is red, fixing it is the team's number one priority, above any feature.
- Nobody goes home with a broken build. The change is reverted and investigated calmly the next day.
- Every change is verified before it is integrated, not after.
Without that contract, automation turns into a traffic light everyone ignores. We will come back to this in module 2.
- What continuous delivery is
Definition. Continuous delivery is the practice of keeping software always in a deployable state, so that any version that has passed the pipeline can be taken to production at the push of a button, at any moment, with a known level of risk.
Continuous delivery takes continuous integration for granted and adds a layer on top: it is not enough for the code to build and pass the tests; there must be an artifact that is built, versioned, tested in production-like environments and ready to deploy.
The key phrase is "at the push of a button". In continuous delivery:
- The technical process of deploying is fully automated.
- The decision to deploy is still human.
That human decision is not a flaw in the system: it is a deliberate choice. There can be perfectly legitimate reasons not to deploy automatically:
- A marketing campaign that starts on Tuesday, with a feature that must not be visible before then.
- A regulatory compliance requirement demanding recorded approval.
- A team that does not yet trust its test suite enough.
What matters is that, when someone decides to deploy, there is no manual work to do: only to authorise it.
- What continuous deployment is
Definition. Continuous deployment is the practice whereby every change that passes the automated pipeline is deployed to production automatically, with no human intervention.
It is continuous delivery without the button. The manual gate is removed: if the pipeline is green, the change reaches users. No meetings, no deployment windows, no Friday afternoons.
This sounds reckless and, without solid foundations, it is. Continuous deployment only works if it rests on:
- A test suite the team genuinely trusts (module 2).
- Progressive deployment strategies — canary, blue-green — to limit the blast radius (module 3).
- Feature flags to separate "deploying code" from "enabling a feature" (module 3).
- Automatic rollback when metrics degrade (module 3).
- Monitoring that spots the problem before the customer does (module 3).
Note something important: continuous deployment is not a mandatory goal for every team. It is an option. A great many excellent organisations deliberately stop at continuous delivery. What is non-negotiable is the ability to deploy at any moment; whether you use it automatically or not is a decision that depends on context.
- The two "CD"s head to head
Here is the core of the lesson. The CI/CD acronym is deliberately ambiguous, and that causes constant confusion in interviews, documentation and team conversations.
| Aspect | Continuous integration (CI) | Continuous delivery | Continuous deployment |
|---|---|---|---|
| What it automates | Building and testing every integrated change | Everything in CI + packaging, versioning and deployment to earlier environments (dev, staging) | Everything in delivery + deployment to production |
| Where the manual gate sits | When merging to the main branch (code review) | Before production: someone presses "Deploy" | There is no manual gate |
| Who decides to deploy | Not applicable: CI does not deploy | A person (tech lead, product owner, release manager) | The pipeline: if it is green, it ships |
| Output of the process | A verified build | An artifact ready for production at any moment | A change already in production |
| Question it answers | "Does this change break anything?" | "Could we deploy right now?" | "Is it deployed yet?" |
| Prerequisite | Version control and automated tests | Solid CI + reproducible environments | Solid delivery + progressive deployment + monitoring + rollback |
| Risk if maturity is missing | Red builds that everyone ignores | Artifacts nobody dares to deploy | Production incidents every day |
One way of remembering it that works very well:
- Delivery = "we can deploy whenever we want" → the capability is automated, the decision is human.
- Deployment = "we deploy whenever the pipeline allows it" → the decision is automated too.
And an inclusion relationship worth committing to memory:
graph TD
CD2["Continuous Deployment"] --> CD1["Continuous Delivery"]
CD1 --> CI["Continuous Integration<br/>CI"]
CI --> VCS["Version control + automated tests"]
style CD2 fill:#c9e4ff,stroke:#333
style CD1 fill:#d9f2d9,stroke:#333
style CI fill:#fff2cc,stroke:#333
Read it from the bottom up: there can be no continuous deployment without continuous delivery, and no continuous delivery without continuous integration. Any attempt to skip a rung produces a fragile system. If Reservalia tried to deploy automatically to production tomorrow without reliable tests, it would not have two incidents per quarter: it would have two per week.
- The full flow: from commit to production
Let us look at the journey a change of Diego's will make over the whole course. This diagram is the mental map of the entire course:
flowchart LR
A["👤 Diego<br/>makes a commit"] --> B["📥 push / pull request<br/>to the repository"]
B -->|trigger| C{{"Pipeline started"}}
C --> D["🔨 Stage: Build<br/>compile, install dependencies"]
D --> E["🧪 Stage: Test<br/>unit, integration, lint"]
E --> F["📦 Artifact<br/>versioned Docker image"]
F --> G["🗄️ Artifact registry<br/>ECR"]
G --> H["🚀 Deployment to dev"]
H --> I["🚀 Deployment to staging"]
I --> J{"Manual gate?"}
J -->|"Yes → Continuous Delivery"| K["👤 Someone approves"]
J -->|"No → Continuous Deployment"| L
K --> L["🚀 Deployment to prod"]
L --> M["📊 Monitoring<br/>and feedback"]
M -.->|"if something fails"| N["⏪ Rollback"]
Read the diagram by identifying where each practice fits:
- From A to E (commit → build → test): this is continuous integration. We will build it in module 2.
- From F to I (artifact → registry → dev → staging): this is continuous delivery. Modules 2 and 3.
- The step J → L with no human intervention: this is continuous deployment. Module 3.
- M and N (monitoring and rollback): the safety net that makes all of the above possible. Module 3.
- Core vocabulary for the course
Let us now settle the terms. We will use them without defining them again, so this section is worth reading carefully. I have grouped them into families.
7.1. The "version control" family
| Term | Definition | Example in Reservalia |
|---|---|---|
| Repository | The store holding the source code and its complete history of changes. | github.com/reservalia/reservalia |
| Commit | A confirmed unit of change, with an author, a date, a message and a unique identifier (SHA). | a3f9c21 — "fix: validate appointment overlap" |
| Branch | A parallel line of development that can later be merged into another. | main, feat/recurring-bookings |
| Pull request / merge request | A proposal to merge one branch into another, opening up review and automated verification. | Diego's PR #142 towards main |
One detail we will use a great deal: the commit SHA is the canonical identity of a change. When we ask ourselves in module 3 "which exact version is in production?", the correct answer will never be "the latest one", but a specific SHA.
# The SHA uniquely identifies the state of the code.
# This command returns the short SHA of the current commit:
git rev-parse --short HEAD
# → a3f9c21
# And this one, the exact commit date in ISO 8601 format.
# We will use it in lesson 01-05 to calculate lead time.
git show -s --format=%cI HEAD
# → 2026-03-14T10:22:41+01:007.2. The "pipeline execution" family
| Term | Definition | Important nuance |
|---|---|---|
| Pipeline | The complete, automated sequence that takes a change from commit to its destination. | It is the umbrella concept: it contains stages, which contain jobs, which contain steps. |
| Trigger | The event that starts a pipeline run. | A push to a branch, opening a PR, a tag, a schedule (cron), a manual run. |
| Stage | A logical grouping of work inside the pipeline, usually sequential. | build → test → deploy. A stage normally waits for the previous one to finish. |
| Job | An independent unit of execution inside a stage. Several jobs in the same stage can run in parallel. | test-api and test-web run at the same time. |
| Step | A specific command or action inside a job, in order. | npm ci, then npm test. |
| Runner / agent | The machine (physical, virtual or container) where a job runs. | An ephemeral Ubuntu runner that is destroyed when it finishes. |
The hierarchy, visually:
graph TD
P["PIPELINE<br/>started by a trigger"]
P --> S1["STAGE: build"]
P --> S2["STAGE: test"]
P --> S3["STAGE: deploy"]
S2 --> J1["JOB: test-api<br/>on ubuntu runner"]
S2 --> J2["JOB: test-web<br/>on ubuntu runner"]
J1 --> ST1["step: npm ci"]
J1 --> ST2["step: npm run test"]
There is a distinction about runners that will matter a great deal later on:
- Ephemeral: created clean for every job and destroyed when it finishes. It guarantees repeatability. It is the default model in GitHub Actions.
- Persistent: a machine reused across runs. It is faster (it keeps caches) but it accumulates state, and that state is a classic source of builds that "only work the second time". Typical of old Jenkins installations.
7.3. The "output and destination" family
| Term | Definition | Example in Reservalia |
|---|---|---|
| Artifact | The packaged, versioned product of a build, ready to deploy or distribute. | The Docker image reservalia/api:a3f9c21 |
| Artifact registry | A versioned store where artifacts are published and retrieved from. | Amazon ECR |
| Environment | A deployed, runnable instance of the system, with its own configuration and its own data. | dev, staging, prod |
| Promotion | Taking the same already-built artifact from one environment to the next, without rebuilding it. | Promoting api:a3f9c21 from staging to prod |
| Reproducible build | The property whereby building the same commit always produces a functionally equivalent artifact. | Pinning Node 20.11.0 and using npm ci with package-lock.json |
The last two deserve elaboration, because they are the most widely misunderstood.
Promotion: build once, deploy many times. This is one of the golden rules of CI/CD. The artifact tested in staging must be the very same binary that reaches production, byte for byte. If you rebuild for production, you are deploying something nobody has ever tested.
graph LR
C["commit a3f9c21"] --> B["BUILD<br/>only once"]
B --> ART["artifact<br/>api:a3f9c21"]
ART --> D["dev"]
ART --> S["staging"]
ART --> P["prod"]
style ART fill:#d9f2d9,stroke:#333,stroke-width:2px
And what not to do:
graph LR
C["commit a3f9c21"] --> B1["build for dev"] --> D["dev"]
C --> B2["build for staging"] --> S["staging"]
C --> B3["build for prod ⚠️<br/>artifact never tested"] --> P["prod"]
style B3 fill:#ffd6d6,stroke:#c00,stroke-width:2px
If the artifact is the same in all three environments, how does the configuration change then? From the outside: environment variables and secrets injected at deployment time. The artifact does not know which environment it lives in.
# The SAME artifact, different configuration depending on the environment.
# In staging:
DATABASE_URL="postgres://reservalia@rds-staging:5432/reservalia"
LOG_LEVEL="debug"
# In production:
DATABASE_URL="postgres://reservalia@rds-prod:5432/reservalia"
LOG_LEVEL="info"Reproducible build. Two builds of the same commit giving the same result is not automatic: you have to earn it. The usual enemies are:
- Unpinned tool versions (plain
nodeinstead of[email protected]). - Dependencies with no lock file, which resolve to the latest available version.
- Installing with
npm install(which can modify the lock) instead ofnpm ci(which respects it strictly). - Timestamps or absolute paths baked into the artifact.
# ❌ Not reproducible: "^4.18.0" may resolve to 4.18.2 today and 4.19.0 tomorrow
npm install
# ✅ Reproducible: installs EXACTLY what package-lock.json says
# and fails if the lock and package.json are inconsistent
npm ciThis is exactly Diego's problem: he builds on his laptop, with his version of Node and his node_modules accumulated over months. His build is not reproducible, and that is why nobody can rebuild what is in production.
Common Mistakes and Tips
Mistake 1: believing that "CD" always means the same thing. When someone says "we have CI/CD", always ask: "is the production deployment triggered by a person or by the pipeline?". It is the only question that disambiguates. In a job interview, telling delivery apart from deployment and explaining why you would choose one or the other puts you ahead of most candidates.
Mistake 2: thinking CI/CD is a tool you install. GitHub Actions does not give you continuous integration, just as buying a pair of trainers does not make you fit. The tool executes; the practice is held up by the team. A team with three-week branches and a hugely expensive CI server is not doing CI.
Mistake 3: rebuilding the artifact for every environment. If the deploy to production compiles again, what reaches users is not what was validated in staging. Build once, promote the same artifact. We will formalise this in lesson 02-06.
Mistake 4: confusing deployment with release. Deploying is putting the code into production; releasing is enabling the feature for users. With feature flags you can deploy on Monday and enable on Thursday. They are two separate operations, and separating them reduces risk enormously. We will look at this in 03-05.
Mistake 5: using long-lived branches and calling it CI. If your branch has 60 commits and two weeks of life, you will integrate badly even with a thousand green tests. Integration frequency is the practice; tests are the mechanism.
Tip 1: start by measuring, not by automating. Before writing your first workflow, write down how long your deployment takes today and how many times a month it fails. Without a baseline you will not be able to show the investment was worth it. Lesson 01-05 is about exactly this.
Tip 2: the pipeline is production code. It lives in the repository, it is reviewed in pull requests and it is versioned like everything else. A .github/workflows/ci.yml edited by hand through a web interface is technical debt from minute one.
Tip 3: if it hurts, do it more often. This is the principle that sums up the entire course. Merges hurt because they are rare; deployments are scary because they are exceptional. Increasing frequency forces you to automate, and automating removes the pain.
Exercises
Exercise 1: classify the situations
For each situation, say whether it describes continuous integration, continuous delivery, continuous deployment or none of the three, and justify your answer in one sentence.
- The team has a server that runs the tests every night at 3:00 against the
mainbranch. Feature branches are merged every three weeks. - Every pull request triggers a build and tests in 6 minutes. If they are green, it can be merged. Deployments are still manual, over SFTP.
- On merging to
main, the pipeline builds a Docker image, deploys it to staging and runs smoke tests. Marta presses "Deploy to production" whenever she considers it appropriate. - On merging to
main, the pipeline deploys to production for 5% of traffic, watches errors for 10 minutes and, if all goes well, completes the deployment. Nobody intervenes. - Diego builds on his laptop and uploads over SFTP on Fridays.
Exercise 2: spot the breaks in repeatability
The following pseudo-script is Diego's manual process. Identify at least four reasons why the resulting artifact is not reproducible, and propose a fix for each one.
#!/bin/bash
# deploy-friday.sh — Reservalia's current process
cd ~/projects/reservalia/apps/api
git pull
npm install
npm run build
echo "Version: $(date)" > dist/VERSION.txt
sftp diego@prod-server <<< "put -r dist/* /var/www/api/"
psql -h rds-prod -U admin -f migrations/latest.sqlExercise 3: draw the target flow
Write a mermaid diagram of Reservalia's target pipeline with these conditions, using the terms trigger, stage, job, artifact, environment and promotion correctly:
- It is triggered when merging to
main. - A verification stage with two parallel jobs:
test-apiandtest-web. - If both pass, one artifact is built, tagged with the commit SHA.
- The artifact is deployed to
stagingautomatically. - It is promoted to
prodonly after manual approval from Marta.
Solutions
Solution to Exercise 1
- None of the three. This is a nightly build. There is test automation, but branches live for three weeks: this is not continuous integration, it is late integration verified overnight. Verification arrives up to 21 days after the change that broke it.
- Continuous integration. Every change is verified automatically before it is integrated and the feedback cycle takes minutes. There is no continuous delivery because there is no deployable artifact and no automated deployment: deploying is still a manual process.
- Continuous delivery. The whole technical path to production is automated (build, artifact, staging, smoke tests) and only one manual gate remains: Marta's decision. This is the canonical example of delivery.
- Continuous deployment. There is no human intervention between the merge and production. The essential safety net is there too: progressive deployment (5% canary) and automated verification before completing.
- None of the three. This is manual deployment with late integration. It is Reservalia's starting point.
Solution to Exercise 2
| # | Problem | Why it breaks repeatability | Fix |
|---|---|---|---|
| 1 | The build happens in ~/projects/... on Diego's laptop |
The environment accumulates state: old node_modules, unversioned files, local variables, a personal Node version |
Build on an ephemeral runner that clones the repository from scratch |
| 2 | git pull without pinning a commit |
You do not know which exact commit was built; if someone pushes during the process, something else gets built | Check out a specific SHA and use it as the artifact tag |
| 3 | npm install |
It can resolve to newer versions within the semver ranges and modify package-lock.json |
Use npm ci, which installs exactly the lock and fails if there are inconsistencies |
| 4 | Unpinned Node version | Node 20.9 and Node 20.11 can produce different artifacts | Pin the version in .nvmrc / engines and in the pipeline |
| 5 | echo "Version: $(date)" |
The timestamp means two builds of the same code produce different artifacts | Tag with the commit SHA, not with the date |
| 6 | sftp put -r dist/* |
It is an incremental deployment: files deleted in the code stay alive on the server. The server accumulates history | Deploy a complete immutable artifact (container image) |
| 7 | psql -f migrations/latest.sql by hand |
There is no control over which migrations have been applied, no guaranteed order and no way to revert | A versioned migration tool, run by the pipeline (lesson 04-06) |
Four would have been enough; if you found more, that is an even better sign.
Solution to Exercise 3
flowchart TD
T(["TRIGGER: push / merge to main"]) --> V
subgraph V["STAGE: verify"]
direction LR
J1["JOB: test-api"]
J2["JOB: test-web"]
end
V -->|"both green"| B
subgraph B["STAGE: build"]
J3["JOB: build<br/>docker build -t reservalia/api:$SHA"]
end
B --> ART[("ARTIFACT<br/>reservalia/api:a3f9c21<br/>in ECR")]
ART -->|"automatic deployment"| ENV1["ENVIRONMENT: staging"]
ENV1 --> GATE{"manual gate:<br/>Marta approves"}
GATE -->|"PROMOTION<br/>of the same artifact"| ENV2["ENVIRONMENT: prod"]
style ART fill:#d9f2d9,stroke:#333,stroke-width:2px
style GATE fill:#fff2cc,stroke:#333
Points that had to appear and are worth checking in your version:
- The trigger is a repository event, not a manual action.
test-apiandtest-webare parallel jobs inside the same stage, not separate stages.- A single artifact is built, tagged with the SHA (not with the date, nor with "latest").
- What goes to production is the same artifact that was validated in staging: that is promotion, not a rebuild.
- The manual gate places this pipeline in continuous delivery. Remove that gate and it would be continuous deployment.
Conclusion
In this lesson we have laid the foundations of the course:
- The enemy has two faces: integration hell (merging late and all at once) and manual deployment (processes that depend on one person not making a mistake). Reservalia suffers from both.
- Continuous integration means integrating often and verifying automatically in a clean environment. It is a team practice before it is a tool.
- Continuous delivery keeps software always deployable: the process is automated and the decision is human.
- Continuous deployment removes that last manual gate, and it is only sensible with reliable tests, progressive deployment, monitoring and rollback.
- The three stack up: no deployment without delivery, and no delivery without CI.
- And we have a shared vocabulary: repository, commit, branch, trigger, pipeline, stage, job, runner, artifact, environment, promotion and reproducible build. Two golden rules we will repeat until the very end: build once and promote the same artifact, and pin versions so that the build is reproducible.
You now know what CI/CD is. Marta's logical question when she presents this to her team will be a different one: "and what does this give us, and what is it going to cost us?". Because setting up a pipeline is not free: it consumes configuration time, maintenance and execution minutes. In the next lesson, Benefits of CI/CD, we will look honestly at both sides of the scales: what a team like Reservalia's gains, how each benefit is measured, and in which situations CI/CD gives back less than it costs.
CI/CD Course: Continuous Integration and Deployment
Module 1: Introduction to CI/CD
- Basic CI/CD Concepts
- Benefits of CI/CD
- Popular CI/CD Tools
- The Course Project: the Application We Are Going to Automate
- DORA Metrics: How Software Delivery Is Measured
Module 2: Continuous Integration (CI)
- Introduction to Continuous Integration
- Setting Up a CI Environment
- Build Automation
- Automated Testing
- Code Quality and Static Analysis
- Artifacts, Versioning and Promotion
- Integration with Version Control
Module 3: Continuous Deployment (CD)
- Introduction to Continuous Deployment
- Deployment Automation
- Infrastructure as Code and Reproducible Environments
- Deployment Strategies
- Feature Flags, Rollback and Failure Recovery
- Monitoring and Feedback
Module 4: Advanced CI/CD Practices
- CI/CD Pipelines
- Dependency Management
- Security in CI/CD
- Scalability and Performance
- Pipeline as Code: Templates, Reuse and Testing the Pipeline
- Databases in the Pipeline: Safe Migrations
Module 5: Implementing CI/CD in Real Projects
- Case Study: Web Project
- Case Study: Mobile Application
- Case Study: Microservices
- Case Study: Modernising a Legacy Project
Module 6: Tools and Technologies
- Jenkins
- GitLab CI/CD
- CircleCI
- Travis CI
- Docker and Kubernetes
- GitHub Actions in Depth
- Comparison and Criteria for Choosing a Tool
Module 7: Practical Exercises
- Exercise 1: Setting Up a Basic Pipeline
- Exercise 2: Integrating Automated Tests
- Exercise 3: Deploying to a Production Environment
- Exercise 4: Monitoring and Feedback
- Exercise 5: Hardening the Pipeline with Security and Secrets
- Final Project: A Complete End-to-End Pipeline
