Build the mental model
This capstone chains together nearly everything the last two chapters covered into the single pipeline professional teams actually run, start to finish.
- Issue -- captures why the work matters before code exists
- Branch -- isolates the work off main until it's ready
- Commits -- build the change up in small, reviewable steps
- Pull request -- makes the change visible to the team
- CI -- an automated, objective first check
- Code review -- human judgment a machine can't provide
- Merge (--no-ff) -- lands the change, preserving branch history
- Tag + Release -- names and ships the exact state that shipped
Merging with --no-ff instead of letting Git fast-forward keeps the feature branch's commits visible as a distinct unit inside main, rather than flattening the work into one anonymous change.
| Stage | Why it exists |
|---|---|
| Issue | Records intent and context before any code is written |
| CI | Objective, automated check before a human spends review time |
| Code review | Human judgment on correctness, style, and missed edge cases |
| Tag + Release | A permanent, meaningful name for exactly what shipped |
Semantic Versioning: MAJOR.MINOR.PATCH
Bump MAJOR for breaking changes, MINOR for backward-compatible new features (like this lesson's farewell() function), and PATCH for backward-compatible bug fixes.
ISSUE TO RELEASE: THE FULL PROFESSIONAL PIPELINE
------------------------------------------------
1. ISSUE Someone opens issue #42 on GitHub describing
(GitHub) the needed change. No local git involved.
|
v
2. BRANCH git checkout -b feature/add-farewell-function
(local) Isolates the work away from main.
|
v
3. COMMITS git commit (pinned author + timestamp, x2)
(local) Small, focused, well-described changes.
|
v
4. PUSH git push -u origin feature/add-farewell-function
(local) Branch now exists on the shared/GitHub repo.
|
v
5. PULL REQUEST Open a PR: feature-branch -> main (GitHub website)
(GitHub)
|
v
6. CI GitHub Actions runs the test suite automatically
(GitHub) on every push to the PR. Pass/fail shown inline.
|
v
7. CODE REVIEW A teammate reads the diff, comments, approves
(GitHub) (or requests changes -- loop back to step 3).
|
v
8. MERGE git merge --no-ff feature/add-farewell-function
(local) Keeps the branch's commits visible in history.
|
v
9. TAG git tag -a v1.1.0 -m "Release v1.1.0 ..."
(local) Semantic version: MAJOR.MINOR.PATCH.
|
v
10. RELEASE git push origin v1.1.0, then "Draft a new release"
(GitHub) from that tag, with release notes, published.Connect it to a real scenario
Open an issue (GitHub)
Someone opens issue #42 describing the needed feature. There's no local git equivalent -- issues live only on GitHub's servers.
Branch for the work
git checkout -b feature/add-farewell-function, named for the issue it addresses.
Commit in small, pinned steps
Make the change across two focused commits, each with a real pinned author and timestamp, then push the branch.
Open a pull request (GitHub)
Open a PR from your branch into main, describing the change and referencing issue #42.
CI runs automatically (GitHub)
Pushing the branch triggers GitHub Actions to run the test suite, reporting pass or fail directly on the pull request.
Code review (GitHub)
A teammate reads the diff, leaves comments, and approves it -- or requests changes, sending you back to step 3.
Merge with --no-ff
Once CI is green and review is approved, git merge --no-ff feature/add-farewell-function keeps the branch's commits visible in main's history.
Tag the release
git tag -a v1.1.0 on the merge commit, following semantic versioning, then push the tag.
Publish the release (GitHub)
On the Releases page, choose tag v1.1.0, write release notes, and publish -- this is the moment the release becomes something users can find and download.
Try the working example
git init --bare /tmp/project.git
git clone /tmp/project.git /tmp/work
cd /tmp/work
export GIT_AUTHOR_NAME="Thuta Learner"
export GIT_AUTHOR_EMAIL="learner@example.com"
export GIT_COMMITTER_NAME="Thuta Learner"
export GIT_COMMITTER_EMAIL="learner@example.com"
export GIT_AUTHOR_DATE="2026-03-01T09:00:00"
export GIT_COMMITTER_DATE="2026-03-01T09:00:00"
cat > app.py <<'EOF'
def greet(name):
return f"Hello, {name}!"
if __name__ == "__main__":
print(greet("World"))
EOF
git add app.py
git commit -m "Initial commit: v1.0.0 baseline app"
git push origin main
# 1. Issue #42 opened on GitHub: "Add a farewell() function" (web UI only)
# 2. Branch, isolated for this issue's work
git checkout -b feature/add-farewell-function
# 3. Two focused, pinned commits
export GIT_AUTHOR_DATE="2026-03-01T10:00:00"
export GIT_COMMITTER_DATE="2026-03-01T10:00:00"
cat >> app.py <<'EOF'
def farewell(name):
return f"Goodbye, {name}!"
EOF
git add app.py
git commit -m "Add farewell() function (closes #42)"
export GIT_AUTHOR_DATE="2026-03-01T11:00:00"
export GIT_COMMITTER_DATE="2026-03-01T11:00:00"
cat >> app.py <<'EOF'
if __name__ == "__main__":
print(farewell("World"))
EOF
git add app.py
git commit -m "Call farewell() in main block for demo output"
# 4. Push the branch -- this is what triggers CI and lets you open a PR
git push -u origin feature/add-farewell-function
# 5. PR opened on GitHub: feature/add-farewell-function -> main (web UI)
# 6. CI runs the test suite on the PR automatically (GitHub Actions)
# 7. A teammate reviews the diff and approves it (GitHub web UI)
# 8. Merge, preserving the feature branch as a visible unit in history
git checkout main
export GIT_AUTHOR_DATE="2026-03-01T14:00:00"
export GIT_COMMITTER_DATE="2026-03-01T14:00:00"
git merge --no-ff feature/add-farewell-function -m "Merge pull request #7 from feature/add-farewell-function
Add farewell() function (closes #42)"
git push origin main
# 9. Tag the release commit with a semantic version
export GIT_AUTHOR_DATE="2026-03-01T14:15:00"
export GIT_COMMITTER_DATE="2026-03-01T14:15:00"
git tag -a v1.1.0 -m "Release v1.1.0: add farewell() function"
git push origin v1.1.0
# 10. On GitHub: Releases -> Draft a new release -> choose tag v1.1.0 ->
# write release notes -> Publish release (web UI)The git-mechanics half of this pipeline produces a fully reproducible history. git log --oneline --graph on main after the merge shows:
* c9c7c8c Merge pull request #7 from feature/add-farewell-function
|\
| * a4f106a Call farewell() in main block for demo output
| * c3aea86 Add farewell() function (closes #42)
|/
* 4a2cea7 Initial commit: v1.0.0 baseline app
Pushing main after the merge reports:
4a2cea7..c9c7c8c main -> main
Pushing the tag reports:
* [new tag] v1.1.0 -> v1.1.0
And git show v1.1.0 --stat confirms the tag points at the merge commit, with the tagger identity and message intact:
tag v1.1.0
Tagger: Thuta Learner <learner@example.com>
Release v1.1.0: add farewell() function
commit c9c7c8c...
Merge: 4a2cea7 a4f106a
On GitHub itself, the issue, pull request, CI check runs, review comments, and the published Release page are real UI elements you would see and click through -- but they have no terminal output to capture, since they exist only as records on GitHub's servers.5-minute try-it
Open a second, smaller issue (#43): 'Bump the version wherever it's hardcoded.' Branch off main (which already contains v1.1.0's merge commit), add a VERSION file containing 1.1.1, commit, merge with --no-ff again, and tag the result v1.1.1, following semantic versioning's rule that a small addition with no breaking changes only bumps the patch number. Confirm with git tag --list that both v1.1.0 and v1.1.1 now exist.
One important caution
Tagging and releasing main before CI has actually passed and the pull request has been reviewed, shipping a release built on unverified code.
Reusing or moving a published tag (like re-tagging v1.1.0 after finding a bug) instead of bumping to a new version -- published tags should be treated as permanent and immutable.
GitHub Docs: Managing releases in a repository — Git & GitHub