Assignment 3: Deployment and CI/CD¶
| Possible Points | Due Date |
|---|---|
| 50 pts | Wednesday, September 30th - 11:59pm |
Overview¶
Getting code to run on your own machine is the easy half. This assignment is about the other half: getting it onto a public URL, automatically, with something standing between a broken commit and your users.
After this assignment you should be able to:
- Deploy a Node service to a cloud provider and reach it from anywhere
- Read a CI workflow and say exactly what it runs and when
- Explain the difference between continuous integration, delivery, and deployment
- Configure a deployment as code, in a reviewable file, rather than by clicking in a dashboard
- Verify that a deploy landed, rather than assuming it did
You will work on Jokebox, a small joke API with a web UI. The app itself is deliberately simple. The pipeline around it is the point.
Using AI Coding Assistants
You are welcome to use AI assistants on this assignment. If you do, you must record it: add an entry to your Collaboration Log noting which tool you used and what for. Review the full Policy on the Use of AI Coding Tools before you start. You are responsible for anything an AI tool produces, so read and verify it.
Prerequisites¶
- Node.js 24 or newer (
node --versionto check) - A GitHub account, already authenticated for
giton your machine - A free Render account, which you will create during the tutorial
- The Git workflow from Assignment 1: branch, commit, pull request, merge
Getting your repository¶
Your repository comes from UCF Code Classroom, the same app you used for Assignments 1 and 2. You are already enrolled, so there is no new link to accept: sign in, open Assignment 3, and your repository will be there.
This is an individual assignment. Everyone deploys their own instance.
If your repository is not there
Post on Ed Discussions. Do not create your own repository or fork Jokebox yourself - Code Classroom collects the repository it created for you, and work anywhere else will not be picked up.
Once you have it, get it running:
git clone https://github.com/UCF-CEN-5016/<your-assignment-3-repo>.git
cd <your-assignment-3-repo>
npm ci
npm run dev
Open the URL it prints. You should see the Jokebox UI, and be able to pull up a joke and reveal its punchline.
npm ci, not npm install
npm ci installs exactly the versions pinned in package-lock.json.
That is what CI runs and what Render runs, so it is the only way to be sure
that "it works on my machine" tells you anything about whether it will work
anywhere else. Read README.md in the repository for a tour of the layout.
Some Definitions¶
Continuous Integration (CI) is the practice of automatically building and testing the codebase every time a change is proposed, so that a change which breaks something is caught at the moment it is introduced rather than weeks later.
Continuous Delivery means every change that passes CI is ready to release - the release itself is still a human decision.
Continuous Deployment removes that last manual step: anything that passes the pipeline goes to production automatically.
The distinction people usually blur is the one between delivery and deployment, and it is worth keeping straight: one ends with a button somebody chooses to press, the other has no button. By the end of Part 2 you will be running continuous deployment, which is why Part 4 asks you to prove the gate in front of it actually works.
If you want to read further, GitLab's CI/CD overview is a good summary.
Part 1: Run it locally and read the pipeline (5 pts)¶
Before deploying anything, get the project green on your own machine and work out what the automation already does.
Both should pass on a fresh clone. Then open .github/workflows/ci.yml and read
it. You do not need to change it yet, but you should be able to answer: what
events trigger it, what commands does it run, and what happens to a pull request
whose tests fail?
Deliverable: a screenshot of npm test passing locally.
Part 2: Deploy to Render (15 pts)¶
Follow the deployment tutorial. It walks you through creating a Render account, deploying Jokebox from your repository, and turning on the setting that makes Render wait for CI before it ships anything.
When you are done you should have a public URL serving Jokebox, and
https://<your-app>.onrender.com/healthz should return JSON reporting
"status": "ok" along with the commit it was built from.
Deliverables: your live URL, and a screenshot of /healthz in a browser.
Part 3: Ship a feature through the pipeline (15 pts)¶
Now use the pipeline for what it is for. The UI has a category dropdown that does not filter anything yet - the API has no way to ask for a random joke from one category. You are going to add it.
How the app fits together¶
Not everyone in this class has built a web application before, so before you change anything, here is the shape of the thing.
There are two programs, not one. A browser on someone's laptop draws the page; a server somewhere else holds the data and answers questions about it. They are separate programs, possibly thousands of miles apart, and they communicate only by sending messages over HTTP. Every time you press "New joke", this round trip happens:
%% Shapes are left transparent on purpose. Material renders mermaid into a
%% closed shadow root and colours the label text from the active palette, so a
%% fixed fill would end up pale-on-pale in dark mode. Letting the page show
%% through means the text always sits on the page's own background, in both
%% themes and on GitHub.
flowchart TB
subgraph browser["BROWSER"]
UI["public/index.html + styles.css<br>structure and styling"]
MAIN["public/main.js<br>asks for jokes, puts them on the page"]
end
subgraph server["SERVER - Node + Express"]
APP["src/app.js<br>matches the URL to a handler"]
ROUTES["src/routes/jokes.js<br>reads the request, picks a status code"]
SVC["src/services/jokeService.js<br>the logic: pick, filter, find"]
DATA[("src/data/jokes.json")]
APP --> ROUTES --> SVC --> DATA
end
MAIN -- "(1) GET /api/jokes/random" --> APP
SVC -. "(2) the joke, as JSON" .-> MAIN
classDef file fill:transparent,stroke:#b08d1e,stroke-width:1.5px
class UI,MAIN,APP,ROUTES,SVC,DATA file
style browser fill:transparent,stroke:#9aa0ae,stroke-dasharray:4 3
style server fill:transparent,stroke:#9aa0ae,stroke-dasharray:4 3
public/main.js, running in the browser, callsfetch("/api/jokes/random"), which sends an HTTP request across the network.- On the server,
app.jslooks at the URL and hands the request to the matching handler insrc/routes/jokes.js. A handler's job is deliberately narrow: read the request, call something, pick a status code. - That handler calls
randomJoke()insrc/services/jokeService.js, which does the real work againstsrc/data/jokes.json. - The joke travels back as JSON - just text, shaped like
{"setup": "...", "punchline": "..."}- andmain.jsputs it into the page.
The browser never touches jokes.json. It only ever sees what the API chose to
put in the reply, which is exactly why GET /api/jokes can withhold punchlines:
they are simply never sent.
You can watch the server half by itself, with no browser involved at all:
curl is just a browser with no opinions about drawing. If that returns a joke,
the server works, and anything wrong is in the browser half.
Why the server code is split into routes, services, and data
It would all fit in one file. It is split because the pieces have different jobs and very different testability.
src/services/ holds plain functions over plain data - no req, no res,
no Express. You can call them directly, which is why the unit tests in
tests/jokeService.test.js need no server and run in milliseconds.
src/routes/ is the thin HTTP skin over those functions. And src/app.js
never calls listen() - only src/server.js does - which is what lets the
API tests import the app and drive it in-process without binding a port.
When you add the filter below, notice that the change lands in three different layers, and that each layer stays responsible for its own job.
What to change¶
Your change touches three of those layers: the service already supports it, the route needs to pass it through, and the browser needs to ask for it.
Work on a branch, open a pull request, let CI run, and merge only once it is green.
- Service layer.
randomJoke()insrc/services/jokeService.jsalready accepts acategoryoption. Read it and confirm you understand what it does when the category matches nothing. - Route. Change
GET /api/jokes/randominsrc/routes/jokes.jsto read an optionalcategoryquery parameter and pass it through. If a category is supplied and matches no jokes, respond 404 with a JSONerror, rather than returning an empty body. - UI. In
public/main.js, find theTODO(assignment)comment inloadJoke()and send the selected category through as a query parameter. Selecting a category should now change which jokes appear. - Tests. Add at least two tests in
tests/routes/jokes.test.js: one that/api/jokes/random?category=techonly ever returns a tech joke, and one that an unknown category returns 404.
Your change must pass npm run lint and npm test locally before you push.
Query parameters in Express
req.query.category holds the value of ?category=..., and is undefined
when the caller did not supply one. Passing undefined through to
randomJoke() gets you the unfiltered behaviour for free - which is the
reason the option was written to be optional.
When does the change show up on Render?
Only after the merge. Render is connected to your main branch and
nothing else, so commits on a pull-request branch are never deployed, even
once CI is green on them. When you merge, the push trigger in ci.yml
runs lint and tests on main, and Render waits for that run to pass before
it builds and deploys. Until then your live app is still running the Part 2
code.
Repeats are fine
randomJoke() picks uniformly at random, so pressing "New joke" may show
the same joke twice in a row, and with a category selected this will happen
more often because the pool is smaller. That is the expected behaviour and
you do not need to prevent it.
Deliverables: the merged pull request, and your deployed app filtering by category.
Part 4: Prove the gate actually works (15 pts)¶
A deployment gate you have never seen stop anything is a gate you are merely hoping is connected. In this part you break the build on purpose and watch what the pipeline does about it.
-
Break something on a branch. Create a branch called
break-the-build. Change the application code so that an existing test fails - for example, makeGET /api/jokesleak punchlines, or have/healthzreport something other thanok.Break the code, not the test
Change application code so a test legitimately fails. Editing the test to make it fail, or deleting one, does not demonstrate anything and gets no credit.
-
Open a pull request from that branch into
main. Do not merge it. - Watch CI fail. Screenshot the pull request showing the failed check, and open the failing run and read the log until you can say which assertion failed and why.
-
Confirm nothing deployed. Check your Render dashboard and your live
/healthz. The commit it reports should still be the one from Part 3. This is the gate doing its job: a broken commit exists on GitHub and never reached your users.Which gate are you actually testing?
Two things stand between this commit and your users, and it is worth keeping them separate. The first is the CI check on the pull request itself: the red X that tells anyone looking at the PR not to merge it. That is the gate this part has you watch directly. The second is Render's After CI Checks Pass setting from Part 2, which only comes into play if a broken commit does land on
main, for example through a direct push or because somebody merged a red PR anyway. Your PR never touchesmain, so this part does not trip that second gate, and you should not try to: putting broken code onmainon purpose is exactly the habit the pipeline exists to break. Checking/healthzconfirms the outcome both gates are there to guarantee.- Close the pull request without merging, and delete the branch.
- Write it up. Add a file
REFLECTION.mdto your repository, 200-400 words, answering: - What failed, and how did you work that out from the CI log?
- What would have reached your users if
maindeployed on every push with no checks? - Jokebox deploys automatically once CI is green. Name one change you would not want shipped that way, and say what you would add to the pipeline to catch it.
Deliverables: the screenshot of the failed check, the screenshot or JSON of
/healthz still on the old commit, and REFLECTION.md committed to main.
Extra Credit (5 pts)¶
Add a post-deploy smoke test. The repository ships scripts/smoke.js, which
takes a base URL, checks that a deployed instance is really serving, and exits
non-zero if it is not:
For the extra credit, add a GitHub Actions workflow that runs it against your live URL on a schedule (say, daily) and fails if the app is down. Put the URL in a repository variable rather than hardcoding it.
This is the piece Parts 1-4 leave out. CI proves the code was good when it was merged; only something that talks to the running deployment can tell you the deployment is still up.
Scheduled run failing on a cold start? Run git pull
A scheduled run nearly always finds a free Render app asleep, and the
original smoke.js failed on the first response from a sleeping service.
The current version waits up to three minutes for the app to wake before
checking it.
On 25 September the fix was pushed to main in every Assignment 3
repository, as a commit that changes only scripts/smoke.js. It did not
redeploy your app, so /healthz still reports your own commit. Pull it
before your next push, or git push will be rejected:
--no-rebase merges it with any commits you have not pushed yet, rather
than stopping to ask how to combine them.
Turning in the Assignment & Grading Rubric¶
Submit on Webcourses:
- The URL of your deployed app
- The URL of your GitHub repository
- Screenshot:
npm testpassing locally (Part 1) - Screenshot:
/healthzon your deployed app (Part 2) - Screenshot: the failed CI check on your
break-the-buildpull request (Part 4) - Screenshot or JSON:
/healthzstill reporting the pre-break commit (Part 4) - Your AI collaboration log, if you used AI tools
Everything else is read from your repository, so make sure your Part 3 work is
merged into main and REFLECTION.md is committed before the deadline.
The grading for this assignment breaks down as follows:
- Part 1: Running Locally & Reading the Pipeline - (5 points)
- Part 2: Deploying to Render - (15 points)
- Part 3: Shipping a Feature Through the Pipeline - (15 points)
- Part 4: Proving the Gate Works - (15 points)
- Extra Credit: Scheduled Smoke Test - (5 points)
Render's free tier sleeps
A free Render service spins down after about 15 minutes of inactivity, and the next request has to wait for it to wake - which can take the better part of a minute. A slow first response is expected and is not a broken deploy. Give it a moment before you conclude something is wrong, and mention it if you show your app to someone else.