Lesson 5 - Guided Project: Run a Feature to Release Cycle
Welcome to the Guided Project
You have learned the pieces one at a time across this module — branching strategies, commit conventions, collaboration, and tags and releases — plus pull requests back in Module 5. In real work, those pieces are never used in isolation. They run together as a single repeating loop: branch, commit, push, review, merge, release. This guided project walks you through that whole loop once, end to end, on a small sample project, so the rhythm becomes muscle memory.
The sample is a tiny shopping-cart library called Shopfront. It is just two files — cart.py with the cart logic, and a README.md — already published on GitHub with one released version, v1.0.0. Your job is to ship a new feature (a percentage discount) and then cut a new release for it, exactly the way a professional team would.
By the end of this project, you will be able to:
- Start a feature from an up-to-date
mainand create a well-named branch using GitHub Flow - Commit the change with a Conventional Commits message and open a clear pull request
- Merge the pull request and bring
mainback up to date - Cut a release: bump the version with semantic versioning, push an annotated tag, and publish a GitHub Release
Follow along in your own copy of a small repo if you have one — every command below is real. Your commit hashes, dates, and PR and release URLs will differ from the examples; that is expected. Let’s run the cycle.
Stage 1: Start From an Up-to-Date Main
The golden rule of GitHub Flow is that every feature starts from the latest main. Branching off stale code is how you create needless merge conflicts before you have written a single line. So the first move is always the same: switch to main and pull.
$ git switch main
$ git pullWith main current, create the feature branch. Name it for what it does, with a short type prefix so anyone scanning the branch list knows at a glance this is a new feature. Here we are adding a discount, so feat/discount is clear and honest.
$ git switch -c feat/discount
Switched to a new branch 'feat/discount'You are now on an isolated branch, based on the freshest main, ready to work without touching anyone else’s code.
Stage 2: Make the Change and Commit It Well
The feature is a small helper: given a cart total and a percentage, return the discounted total. Add it to cart.py.
def discount(total, pct):
"""Return total reduced by pct percent (e.g. pct=10 takes 10% off)."""
return total * (1 - pct / 100)Stage the file and commit it. This is where the work from Lesson 2 pays off: a Conventional Commit message. The feat type announces a new feature, the (cart) scope says which part of the project changed, and the description is a short, imperative summary.
$ git add cart.py
$ git commit -m "feat(cart): add percentage discount"That one line is doing real work. The feat type is exactly what release tooling reads to decide that the next version needs a minor bump — a fact you will use in Stage 5.
Stage 3: Push the Branch and Open a Pull Request
Your commit lives only on your machine until you push it. Push the branch and set its upstream with -u so future git push and git pull calls on this branch need no arguments.
$ git push -u origin feat/discountNow open a pull request. The fastest path is the GitHub CLI — gh pr create lets you title and describe the PR without leaving the terminal. Give it a human-readable title and a body that says what the change does and why a reviewer should care.
$ gh pr create --base main --head feat/discount \
--title "Add percentage discount" \
--body "Adds a discount(total, pct) helper to the cart library."gh prints the URL of the new pull request when it finishes — open that link to see your PR on GitHub. If you would rather use the web interface, GitHub shows a “Compare & pull request” button on the repository page right after you push a new branch; clicking it opens the same form with the title and body fields ready to fill in.
A good description is not busywork. It is the first thing your reviewer reads, and months from now it is the record of why this change existed.
Stage 4: Review and Merge
A pull request is a request, not a command — the point is review. A teammate reads the diff, asks questions, maybe requests a change, and eventually approves. On a solo project you can review your own diff in the PR’s “Files changed” tab, which is still worth doing: seeing your work as a reviewer would often catches mistakes the author misses.
Once the PR is approved, merge it. In the web interface this is the “Merge pull request” button at the bottom of the PR. From the terminal, gh does the same thing — and --squash collapses the branch’s commits into a single tidy commit on main:
$ gh pr merge --squashThe feature is now part of main on GitHub. The branch has done its job and can be deleted (GitHub offers a “Delete branch” button right after the merge).
Stage 5: Cut the Release
A merged feature is shipped to main, but a release is a deliberate, named snapshot that users and other developers can depend on. Cutting one is the final stage of the loop.
First, bring your local main up to date so it includes the merge you just made on GitHub.
$ git switch main
$ git pullNow decide the version number. The current release is v1.0.0. Semantic versioning reads a version as MAJOR.MINOR.PATCH: you bump MAJOR for breaking changes, MINOR for new backward-compatible features, and PATCH for bug fixes. You added a new feature without breaking anything — that is a MINOR bump, so the next version is v1.1.0. (This is the same feat signal from Stage 2, now turned into a number.)
Create an annotated tag for the release. Annotated tags (-a) store a message, an author, and a date, which is exactly what a release marker should carry — unlike a lightweight tag, which is just a bare pointer.
$ git tag -a v1.1.0 -m "Add discount support"
$ git tag
v1.0.0
v1.1.0The tag exists locally. Tags are not sent by a normal git push, so push this one explicitly.
$ git push origin v1.1.0
To github.com:datatweets/shopfront.git
* [new tag] v1.1.0 -> v1.1.0Finally, publish a GitHub Release built on that tag. A release wraps the tag with a title, notes, and (optionally) downloadable assets, and it is the page users actually visit to see what changed. With gh:
$ gh release create v1.1.0 --title "v1.1.0" --notes "Adds percentage discount support."gh prints the release’s URL when it finishes. In the web interface the same release is created from the repository’s “Releases” section: click “Draft a new release”, pick the existing tag v1.1.0, fill in the title and notes, and publish.
That is the complete cycle. You started from a fresh main, did the work on a named branch, committed it with a message tooling can read, opened and merged a pull request, and shipped a versioned, tagged, published release.
This is the whole loop in miniature
Branch off main, commit well, push, open a PR, review, merge, then tag and release — that is the entire professional rhythm real teams repeat constantly. The project sizes change and the team sizes change, but the loop does not. Once this sequence feels automatic, you can drop into almost any team’s workflow and know exactly what to do next.
Practice Exercises
Exercise 1: Ship a fix and choose the version
Suppose the discount helper has a bug — it crashes when pct is 0. You branch off main, fix it, commit, open a PR, and merge. Now you need to release. What commit type should the message use, and what should the new version number be, given the last release was v1.1.0?
Hint
Use the fix type, e.g. fix(cart): handle zero percent discount. A bug fix with no new features and no breaking changes is a PATCH bump under semantic versioning, so v1.1.0 becomes v1.1.1.
Exercise 2: Write the release notes
You are drafting the GitHub Release for v1.1.0. Instead of the one-line --notes used above, write a short notes body (two or three lines) that someone reading the release page would actually find useful.
Hint
Good notes summarize what changed from a user’s point of view, not the internal commit details. For example: a line introducing the new discount(total, pct) helper, a one-line example of calling it, and a note that there are no breaking changes since v1.0.0. Group changes under headings like “Added” or “Fixed” once you have several.
Exercise 3: Add a second feature and cut a MINOR release
Run the full loop a second time: add a clear() helper to cart.py that empties the cart, commit it with a Conventional Commit message, open and merge a PR, then cut the next release. Which version number do you tag?
Hint
Commit it as a feature, e.g. feat(cart): add clear to empty the cart. A new backward-compatible feature is a MINOR bump, so after v1.1.0 you tag v1.2.0 with git tag -a v1.2.0 -m "Add clear support", push it with git push origin v1.2.0, and publish the release.
Summary
In this guided project you ran one complete feature-to-release cycle on the Shopfront sample. You started from an up-to-date main and created a well-named feature branch with GitHub Flow, made the change to cart.py, and committed it with a Conventional Commits message. You pushed the branch, opened a pull request with a clear description, and merged it after review. Then you cut a release: you used semantic versioning to pick the new version, created an annotated tag, pushed the tag explicitly, and published a GitHub Release. Every stage drew on a different lesson, and together they form the single loop teams repeat for every change they ship.
Key Concepts
- GitHub Flow loop — branch off a fresh
main, commit, push, PR, review, merge. - Conventional Commit —
type(scope): summary; thefeat/fixtype drives the version bump. - Semantic versioning —
MAJOR.MINOR.PATCH; features bump MINOR, fixes bump PATCH. - Annotated tag + GitHub Release — a tag carries the release marker; the Release wraps it with title and notes for users.
Why This Matters
Knowing each Git command on its own is not the same as being able to ship. What teams actually need is someone who can take a change from idea to published release without dropping a step — pulling first, naming the branch sensibly, writing a message tooling can read, getting it reviewed, and tagging the result so others can depend on it. Running the loop once, deliberately, turns four separate lessons into one fluent skill you can repeat on any project. From here, the next module steps beyond the happy path into editing history and recovering work when the loop goes sideways.
Next Steps
Continue to Module 7 - Rewriting and Recovering History
Rebase, interactive rebase, cherry-pick, and recovering lost work with reflog and bisect.
Back to Module Overview
Return to the Team Workflows module overview
Continue Building Your Skills
You can now run a full professional cycle — from a fresh branch all the way to a published, versioned release — without losing the thread between stages. That loop is the backbone of day-to-day work on any team. Next you will learn how to reshape and rescue history: rewriting commits cleanly, moving work between branches, and recovering changes that seemed lost for good.