Learning Objectives
By the end of this module, you will be able to:
- Explain Semantic Versioning (semver) and how MAJOR.MINOR.PATCH communicates the impact of a change
- Write commit messages following the Conventional Commits specification
- Distinguish between commit types (
feat,fix,docs,chore, etc.) and understand their impact on versioning - Use Git tags to mark releases — both lightweight and annotated — and push them to remotes
- Set up automated changelog generation with tools like standard-version and release-please
- Create GitHub/GitLab releases from tagged commits
1. Semantic Versioning (semver)
In 2011, Tom Preston-Werner (co-founder of GitHub) formalized a convention for versioning software. It became one of the most widely adopted standards in the industry.
The Three Numbers
MAJOR . MINOR . PATCH
2 . 4 . 1
│ │ └── Bug fixes (backwards compatible)
│ └────────── New features (backwards compatible)
└────────────────── Breaking changes (NOT backwards compatible)
The Rules
| Change Type | Version Bump | Example | Signal to Users |
|---|---|---|---|
| Bug fix (backwards compatible) | PATCH | 2.4.1 → 2.4.2 | Safe to upgrade immediately |
| New feature (backwards compatible) | MINOR | 2.4.2 → 2.5.0 | Safe, but review new features |
| Breaking change | MAJOR | 2.5.0 → 3.0.0 | Review carefully, may need code changes |
When you bump:
- PATCH: increment the last number (
2.4.1→2.4.2) - MINOR: increment the middle number, reset patch to 0 (
2.4.2→2.5.0) - MAJOR: increment the first number, reset minor and patch to 0 (
2.5.0→3.0.0)
What "Breaking" Means
A breaking change modifies the public API in a way that existing users' code may stop working:
- Removing or renaming a function/endpoint
- Changing a function's signature (parameters, return type)
- Changing default behavior
- Removing a configuration option
- Changing the format of output data
A non-breaking change is backwards compatible — existing code continues to work:
- Adding a new function/endpoint
- Adding an optional parameter with a default value
- Fixing a bug (unless people depended on the buggy behavior)
- Internal refactoring with no API changes
The 0.x.x Special Case
Versions starting with 0 (e.g., 0.3.1) are considered pre-release. The API is not yet stable, and breaking changes can happen in any release without bumping to 1.0.0. Once you publish 1.0.0, you're signaling that the API is stable and semver rules apply strictly.
Pre-release and Build Metadata
1.2.3-alpha.1 ← pre-release identifier
1.2.3-beta.2 ← another pre-release
1.2.3-rc.1 ← release candidate
1.2.3+build.456 ← build metadata (ignored in versioning)
1.2.3-alpha.1+001 ← both combined
Pre-release versions have lower precedence than the normal version: 1.0.0-alpha < 1.0.0.
Why semver Matters
Scenario: You depend on library "auth-utils" at version 2.4.1
New version available: 2.4.2 (patch)
→ Bug fix only. Upgrade without worry.
New version available: 2.5.0 (minor)
→ New features added. Your code still works. Upgrade when convenient.
New version available: 3.0.0 (major)
→ Breaking changes! Read the changelog. Test thoroughly before upgrading.
Semver gives users a signal about the risk of upgrading. It requires discipline from maintainers to classify changes correctly.
2. Conventional Commits Specification
Conventional Commits originated from the AngularJS contribution guidelines and has become a widely adopted standard. It adds structure to commit messages that is both human and machine readable.
The Format
<type>(<optional scope>): <description>
<optional body>
<optional footer(s)>
A Complete Example
feat(auth): add OAuth2 login support
Implements Google and GitHub OAuth2 providers.
Users can now sign in with their existing accounts
instead of creating new credentials.
The login page shows provider buttons above the
email/password form.
BREAKING CHANGE: the /api/login endpoint now returns
a different response shape with a "provider" field.
Closes #234
The Type
The type is mandatory and must be one of:
| Type | Description | Version Impact |
|---|---|---|
feat | A new feature | Bumps MINOR |
fix | A bug fix | Bumps PATCH |
docs | Documentation changes only | No version bump |
style | Formatting, whitespace, semicolons (no code change) | No version bump |
refactor | Code restructuring (no feature or fix) | No version bump |
perf | Performance improvement | Bumps PATCH |
test | Adding or fixing tests | No version bump |
build | Build system or external dependencies | No version bump |
ci | CI/CD configuration changes | No version bump |
chore | Maintenance tasks (no src or test change) | No version bump |
revert | Reverting a previous commit | Depends on reverted type |
Only feat and fix directly affect the version number. Other types are for organization and changelog clarity.
The Scope
The scope is optional and describes the section of the codebase affected:
feat(auth): add login form
fix(api): handle null response
docs(readme): update installation steps
refactor(database): extract query builder
Scopes are typically nouns: auth, api, ui, database, config, core, etc.
The Description
The description follows Git's native commit message guidelines:
- Imperative tone: "add feature" not "added feature"
- Lowercase first letter: "add" not "Add"
- No period at the end: "add login form" not "add login form."
- Concise: aim for under 72 characters total (including type and scope)
The Body
The body is optional and provides additional context:
feat: add email notification system
Integrates with SendGrid API to send transactional emails.
Supports templates for welcome, password reset, and
order confirmation emails.
Rate limiting is applied at 100 emails per minute
to comply with SendGrid's free tier.
Separate the body from the description with a blank line. The body can use full sentences, paragraphs, and bullet points.
The Footer
The footer is optional and is used for:
Breaking changes:
BREAKING CHANGE: the login endpoint now requires
a "provider" field in the request body.
Issue references:
Closes #123
Fixes #456
Refs #789
Multiple footers:
BREAKING CHANGE: removed deprecated v1 API endpoints
Reviewed-by: Alice Smith
Closes #234, #567
The ! Shorthand for Breaking Changes
You can mark a breaking change directly in the type line with !:
feat!: remove support for Node 12
BREAKING CHANGE: minimum Node version is now 16.
Or with a scope:
feat(api)!: change response format for /users endpoint
The ! is a visual alert. If present, you should still include BREAKING CHANGE: in the footer for a detailed description.
3. Conventional Commits + Semver Connection
The power of conventional commits is that version bumping becomes automatic — tools can read your commit messages and determine the correct version.
Commits since last release:
fix(auth): resolve session timeout issue → PATCH
docs: update API reference → (none)
feat(api): add /users/search endpoint → MINOR
chore: update dev dependencies → (none)
fix(ui): correct button alignment → PATCH
Highest impact: MINOR (from the feat commit)
Version bump: 2.4.1 → 2.5.0
If any commit has BREAKING CHANGE or !:
feat!: redesign authentication flow → MAJOR
fix: correct error messages → PATCH
Highest impact: MAJOR (from the breaking change)
Version bump: 2.5.0 → 3.0.0
The highest impact commit determines the version bump. A single breaking change overrides all minor and patch changes.
4. Git Tags for Releases
Tags are the mechanism Git uses to mark specific commits as releases. They're pointers that don't move (unlike branches).
Lightweight vs. Annotated Tags
Lightweight tag:
Just a pointer to a commit (like a branch that never moves)
No extra metadata
Annotated tag:
A full Git object with:
- Tagger name and email
- Date
- Message
- GPG signature (optional)
Always use annotated tags for releases. Lightweight tags are for personal bookmarks.
Creating Tags
# Annotated tag (recommended for releases)
git tag -a v1.0.0 -m "Version 1.0.0 - Initial release"
# Annotated tag with a longer message (opens editor)
git tag -a v2.0.0
# Lightweight tag (for personal use)
git tag v1.0.0-temp
# Tag a specific commit (not just HEAD)
git tag -a v1.0.0 abc1234 -m "Version 1.0.0"
# Create a tag with -m implies annotated (no -a needed)
git tag v1.0.0 -m "Version 1.0.0"Listing and Inspecting Tags
# List all tags
git tag
# List tags matching a pattern
git tag -l "v1.*"
git tag -l "v2.0.*"
# Show tag details
git show v1.0.0
# Show tag type (tag = annotated, commit = lightweight)
git cat-file -t v1.0.0
# List tags with dates
git tag -l --format='%(refname:short) %(creatordate:short)'Pushing Tags
Tags are not pushed by default. You must push them explicitly:
# Push a specific tag
git push origin v1.0.0
# Push all tags
git push origin --tags
# Push only annotated tags (recommended)
git push origin --follow-tags--follow-tags is the safest option: it pushes annotated tags that are reachable from the commits being pushed, skipping lightweight tags.
Deleting Tags
# Delete locally
git tag -d v1.0.0
# Delete from remote
git push origin --delete v1.0.0
# Delete multiple remote tags
git push origin --delete v1.0.0 v1.0.1 v1.0.2Checking Out a Tag
# Puts you in "detached HEAD" state
git checkout v1.0.0
# Better: create a branch from a tag
git checkout -b hotfix/v1.0.1 v1.0.0Configuring Auto-Push for Tags
# Always push annotated tags with git push
git config --global push.followTags trueWith this setting, git push automatically pushes reachable annotated tags alongside your commits.
5. Automated Changelog Generation
The real payoff of conventional commits: tools can automatically generate human-readable changelogs from your commit history.
standard-version (Now Deprecated — Use release-please)
standard-version was the go-to tool for automated releases. It's now in maintenance mode, but understanding it helps you understand the concept.
npm install --save-dev standard-versionWhat standard-version does in one command:
- Reads commit history since the last tag
- Determines the version bump (MAJOR/MINOR/PATCH) from commit types
- Updates
CHANGELOG.mdwith grouped, formatted entries - Bumps the version in
package.json - Creates a commit:
chore(release): 2.5.0 - Creates a Git tag:
v2.5.0
# First release
npx standard-version --first-release
# Subsequent releases (auto-determines version)
npx standard-version
# Force a specific bump
npx standard-version --release-as major
npx standard-version --release-as minor
npx standard-version --release-as patch
# Force a specific version
npx standard-version --release-as 3.0.0
# Preview without making changes
npx standard-version --dry-runGenerated Changelog
# Changelog
## [2.5.0](https://github.com/user/repo/compare/v2.4.1...v2.5.0) (2024-03-15)
### Features
* **auth:** add OAuth2 login support ([abc1234](https://github.com/user/repo/commit/abc1234))
* **api:** add user search endpoint ([def5678](https://github.com/user/repo/commit/def5678))
### Bug Fixes
* **ui:** correct button alignment on mobile ([ghi9012](https://github.com/user/repo/commit/ghi9012))
* **auth:** resolve session timeout issue ([jkl3456](https://github.com/user/repo/commit/jkl3456))release-please (Google's Recommended Replacement)
release-please is a GitHub Action that automates releases via pull requests:
# .github/workflows/release-please.yml
name: release-please
on:
push:
branches: [main]
permissions:
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
with:
release-type: nodeHow it works:
- You merge commits to
mainwith conventional commit messages - release-please opens/updates a "Release PR" automatically
- The Release PR contains the changelog diff and version bump
- When you merge the Release PR, it creates a GitHub Release and Git tag
semantic-release (Fully Automated)
For projects that want zero manual intervention:
npm install --save-dev semantic-release// .releaserc.json
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/github",
"@semantic-release/git"
]
}semantic-release runs in CI and automatically:
- Analyzes commits
- Determines the version
- Generates release notes
- Publishes to npm (if applicable)
- Creates a GitHub Release
- Tags the commit
6. Commitizen — Interactive Commit Helper
Commitizen provides an interactive prompt that guides you through creating properly formatted conventional commits.
Setup
# Install globally
npm install -g commitizen cz-conventional-changelog
# Configure (global)
echo '{ "path": "cz-conventional-changelog" }' > ~/.czrcOr per-project:
npm install --save-dev commitizen cz-conventional-changelog
# In package.json:
{
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
}
}Usage
Instead of git commit, run:
npx cz
# Or if installed globally:
czThe interactive prompt:
? Select the type of change you're committing:
feat: A new feature
fix: A bug fix
docs: Documentation only changes
style: Code style changes (formatting, semicolons)
refactor: Code change that neither fixes a bug nor adds a feature
perf: A code change that improves performance
test: Adding missing tests or fixing existing tests
build: Changes that affect the build system
ci: Changes to CI configuration files
chore: Other changes that don't modify src or test
revert: Reverts a previous commit
? What is the scope of this change? (optional): auth
? Write a short description: add OAuth2 login support
? Provide a longer description: (optional)
? Are there any breaking changes? No
? Does this change affect any open issues? No
✔ Commit created: feat(auth): add OAuth2 login support
Commitizen automatically:
- Lowercases the first letter of your description
- Removes trailing periods
- Formats the
BREAKING CHANGEfooter correctly - Adds
Closes #Nreferences
7. GitHub and GitLab Releases
Tags mark commits in Git. Releases are a hosting platform feature built on top of tags.
GitHub Releases
# Create a release using the GitHub CLI
gh release create v2.5.0 --title "v2.5.0" --notes "$(cat <<'EOF'
## What's New
### Features
- OAuth2 login support (#234)
- User search endpoint (#256)
### Bug Fixes
- Session timeout resolution (#278)
- Button alignment on mobile (#290)
### Breaking Changes
None
EOF
)"
# Create a release from an existing tag
gh release create v2.5.0 --title "v2.5.0" --generate-notes
# Create a draft release
gh release create v2.5.0 --draft --generate-notes
# Upload assets to a release
gh release upload v2.5.0 ./dist/app-v2.5.0.zip
# List releases
gh release list
# View a specific release
gh release view v2.5.0GitHub Auto-Generated Release Notes
GitHub can generate release notes from merged PRs:
gh release create v2.5.0 --generate-notesThis creates release notes grouped by PR labels (features, bug fixes, etc.). Configure label mappings in .github/release.yml:
# .github/release.yml
changelog:
categories:
- title: "🚀 Features"
labels: ["enhancement", "feature"]
- title: "🐛 Bug Fixes"
labels: ["bug", "fix"]
- title: "📚 Documentation"
labels: ["documentation"]
- title: "🧹 Maintenance"
labels: ["chore", "dependencies"]GitLab Releases
# Using the GitLab API (via curl)
curl --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
--data name="v2.5.0" \
--data tag_name="v2.5.0" \
--data description="Release notes here..." \
"https://gitlab.com/api/v4/projects/$PROJECT_ID/releases"GitLab also supports release creation in .gitlab-ci.yml:
release:
stage: deploy
script:
- echo "Creating release"
release:
tag_name: $CI_COMMIT_TAG
description: './CHANGELOG.md'
only:
- tagsCommand Reference
| Command | Description |
|---|---|
git tag v1.0.0 | Create a lightweight tag |
git tag -a v1.0.0 -m "message" | Create an annotated tag |
git tag -a v1.0.0 <sha> | Tag a specific commit |
git tag | List all tags |
git tag -l "v1.*" | List tags matching a pattern |
git show v1.0.0 | Show tag details and commit |
git tag -d v1.0.0 | Delete a local tag |
git push origin v1.0.0 | Push a specific tag |
git push origin --tags | Push all tags |
git push origin --follow-tags | Push only annotated reachable tags |
git push origin --delete v1.0.0 | Delete a remote tag |
git checkout v1.0.0 | Check out a tag (detached HEAD) |
git checkout -b branch v1.0.0 | Create a branch from a tag |
npx standard-version | Auto-bump version, changelog, commit, tag |
npx standard-version --dry-run | Preview without making changes |
npx cz | Interactive conventional commit prompt |
gh release create v1.0.0 | Create a GitHub release |
gh release create v1.0.0 --generate-notes | Create release with auto-generated notes |
Hands-On Lab
Setup
mkdir semver-lab && cd semver-lab
git init
# Initialize a minimal package.json for tooling
cat > package.json << 'EOF'
{
"name": "semver-lab",
"version": "0.0.0",
"description": "Conventional commits and semver lab",
"main": "index.js",
"scripts": {}
}
EOF
cat > index.js << 'EOF'
function calculator(operation, a, b) {
switch (operation) {
case 'add': return a + b;
case 'subtract': return a - b;
default: throw new Error('Unknown operation');
}
}
module.exports = { calculator };
EOF
git add .
git commit -m "chore: initial project setup"Part 1: Writing Conventional Commits
# Feature commit
cat > index.js << 'EOF'
function calculator(operation, a, b) {
switch (operation) {
case 'add': return a + b;
case 'subtract': return a - b;
case 'multiply': return a * b;
default: throw new Error('Unknown operation');
}
}
module.exports = { calculator };
EOF
git add index.js
git commit -m "feat: add multiply operation to calculator"
# Bug fix commit
cat > index.js << 'EOF'
function calculator(operation, a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Arguments must be numbers');
}
switch (operation) {
case 'add': return a + b;
case 'subtract': return a - b;
case 'multiply': return a * b;
default: throw new Error('Unknown operation: ' + operation);
}
}
module.exports = { calculator };
EOF
git add index.js
git commit -m "fix: validate inputs and improve error messages"
# Documentation commit
cat > README.md << 'EOF'
# Calculator
A simple calculator module supporting add, subtract, and multiply.
## Usage
```js
const { calculator } = require('./index');
calculator('add', 2, 3); // 5EOF
git add README.md git commit -m "docs: add README with usage instructions"
View the history
git log --oneline
**Checkpoint**: You should see three commits with `feat:`, `fix:`, and `docs:` prefixes. Each commit message should be lowercase, imperative, and without a period.
### Part 2: Creating Your First Tagged Release
```bash
# Tag the current state as v1.0.0
git tag -a v1.0.0 -m "Version 1.0.0 - Initial release
Features:
- Basic calculator with add, subtract, multiply
- Input validation
- Documentation"
# Verify the tag
git tag
git show v1.0.0
# Verify it's annotated (not lightweight)
git cat-file -t v1.0.0
# Should output: tag
Checkpoint: git tag shows v1.0.0. git show v1.0.0 displays the tag message, tagger info, and the commit details.
Part 3: Building Toward v1.1.0 (Minor Release)
# Add a new feature
cat > index.js << 'EOF'
function calculator(operation, a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Arguments must be numbers');
}
switch (operation) {
case 'add': return a + b;
case 'subtract': return a - b;
case 'multiply': return a * b;
case 'divide':
if (b === 0) throw new Error('Division by zero');
return a / b;
default: throw new Error('Unknown operation: ' + operation);
}
}
module.exports = { calculator };
EOF
git add index.js
git commit -m "feat: add divide operation with zero-check"
# Add a small fix
echo "## License\nMIT" >> README.md
git add README.md
git commit -m "docs: add license section to README"
# Tag as v1.1.0 (new feature = minor bump)
git tag -a v1.1.0 -m "Version 1.1.0
Features:
- Division operation with zero-division protection
Documentation:
- Added license section"
# View tags
git tag -l "v1.*"Checkpoint: git tag -l "v1.*" shows both v1.0.0 and v1.1.0.
Part 4: A Breaking Change (v2.0.0)
# Refactor to a different API shape (breaking change!)
cat > index.js << 'EOF'
class Calculator {
add(a, b) { return this.#validate(a, b) && a + b; }
subtract(a, b) { return this.#validate(a, b) && a - b; }
multiply(a, b) { return this.#validate(a, b) && a * b; }
divide(a, b) {
this.#validate(a, b);
if (b === 0) throw new Error('Division by zero');
return a / b;
}
#validate(a, b) {
if (typeof a !== 'number' || typeof b !== 'number') {
throw new TypeError('Arguments must be numbers');
}
return true;
}
}
module.exports = { Calculator };
EOF
git add index.js
git commit -m "feat!: refactor to class-based Calculator API
BREAKING CHANGE: the calculator() function has been replaced
with a Calculator class. Users must now instantiate the class:
Before: calculator('add', 2, 3)
After: new Calculator().add(2, 3)"
# Update docs for new API
cat > README.md << 'EOF'
# Calculator
A calculator module with a clean class-based API.
## Usage
```js
const { Calculator } = require('./index');
const calc = new Calculator();
calc.add(2, 3); // 5
calc.subtract(5, 3); // 2
calc.multiply(3, 4); // 12
calc.divide(10, 2); // 5License
MIT EOF
git add README.md git commit -m "docs: update README for new class-based API"
Tag as v2.0.0 (breaking change = major bump)
git tag -a v2.0.0 -m "Version 2.0.0
BREAKING CHANGES:
- Replaced calculator() function with Calculator class
- See README for migration guide
Documentation:
- Updated README with new API examples"
View all tags and the range between releases
git tag git log --oneline v1.1.0..v2.0.0
**Checkpoint**: `git log --oneline v1.1.0..v2.0.0` should show the breaking change commit and the docs update.
### Part 5: Generating a Changelog Manually
```bash
# Generate a basic changelog from commit history
echo "# Changelog" > CHANGELOG.md
echo "" >> CHANGELOG.md
for tag in $(git tag --sort=-version:refname); do
echo "## $tag" >> CHANGELOG.md
echo "" >> CHANGELOG.md
# Get the previous tag
prev_tag=$(git tag --sort=-version:refname | grep -A1 "^${tag}$" | tail -1)
if [ "$prev_tag" != "$tag" ]; then
range="${prev_tag}..${tag}"
else
range="$tag"
fi
# List commits grouped by type
git log --oneline "$range" 2>/dev/null | while read line; do
echo "- $line" >> CHANGELOG.md
done
echo "" >> CHANGELOG.md
done
cat CHANGELOG.md
git add CHANGELOG.md
git commit -m "docs: add generated changelog"
Part 6: Using git shortlog for Release Notes
# Summarize contributions between releases
echo "=== v1.0.0 to v1.1.0 ==="
git shortlog v1.0.0..v1.1.0
echo ""
echo "=== v1.1.0 to v2.0.0 ==="
git shortlog v1.1.0..v2.0.0
echo ""
echo "=== All time ==="
git shortlog -snCheckpoint: Each range should show the relevant commits grouped by author.
Challenge
- Add three more commits to the repo: one
feat, onefix, and oneperf - Determine what the next version should be based on semver rules
- Create the appropriate annotated tag
- Write a script that automatically determines the next version by parsing commit messages since the last tag (hint: check for
feat→ minor,fix→ patch,BREAKING CHANGEor!→ major) - If you have Node.js installed, try installing and running
commitizen(npx cz) for one of your commits
Common Pitfalls
| Pitfall | What Happens | Prevention |
|---|---|---|
| Using lightweight tags for releases | No metadata (who tagged, when, why); --follow-tags skips them | Always use git tag -a for releases |
| Forgetting to push tags | Tags exist locally but not on the remote | Use git push --follow-tags or configure push.followTags true |
| Wrong commit type | A breaking change tagged as fix → incorrect patch bump | Review the commit type rules; use commitlint to enforce |
| Squash-merging loses commit types | All conventional commits get squashed into one message | Ensure the squash commit message follows conventional format, or avoid squash merging |
| Emoji adapters breaking tooling | Changelog generators and linters may not understand emoji types | Stick to the standard text types unless your entire toolchain supports emojis |
| Starting at v1.0.0 too early | Frequent breaking changes force rapid major version bumps | Start at 0.1.0 and move to 1.0.0 only when the API is stable |
| Tagging the wrong commit | Release points to a pre-release state | Always tag after merging to main, never on a feature branch |
Pro Tips
-
push.followTags = trueis a must-have configuration. Add it globally so you never forget to push tags:git config --global push.followTags true. -
Use
--dry-runfirst. When runningstandard-versionorsemantic-releasefor the first time, use--dry-runto preview what will happen before making changes. -
Conventional commits improve
git log. Even without tooling, commit messages likefeat(auth): add loginare far more readable than "updated stuff" when scanning history withgit log --oneline. -
Enforce in CI, not just locally. Client-side hooks can be bypassed. Always validate commit messages in CI as a backstop. GitHub Actions can run commitlint on push events.
-
The scope is your friend. Consistent scopes (
auth,api,ui) make changelogs more useful andgit log --grep="auth"more powerful. -
git describeshows your position relative to the last tag:git describe --tags # v2.0.0-3-gabc1234 # = 3 commits after v2.0.0, currently at abc1234 -
Don't mix versioning strategies. Pick one tool (standard-version, release-please, semantic-release) and stick with it. Mixing manual tagging with automated tools causes version conflicts.
Quiz / Self-Assessment
- In semver
2.4.1, what does each number represent?
Answer
2= MAJOR — incremented for breaking (backwards-incompatible) changes4= MINOR — incremented for new features that are backwards compatible1= PATCH — incremented for backwards-compatible bug fixes
When MINOR is bumped, PATCH resets to 0. When MAJOR is bumped, both MINOR and PATCH reset to 0.
- What is the format of a conventional commit message?
Answer
type(optional-scope): description
optional body
optional footer(s)
Example: feat(auth): add OAuth2 login support. The type is mandatory and must be one of the defined types (feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert). The scope is optional and describes the affected section. The description uses imperative tone, lowercase, no period.
- Which conventional commit types trigger version bumps, and what kind?
Answer
feat→ bumps MINOR (new feature, backwards compatible)fix→ bumps PATCH (bug fix, backwards compatible)- Any type with
!orBREAKING CHANGEin the footer → bumps MAJOR
Other types (docs, style, refactor, test, build, ci, chore) do not trigger version bumps by default. The highest-impact commit since the last release determines the bump.
- What's the difference between a lightweight and annotated Git tag?
Answer
A lightweight tag is just a pointer to a commit — like a branch that doesn't move. It has no metadata. An annotated tag is a full Git object stored in the database, containing the tagger's name, email, date, a message, and optionally a GPG signature. Use git tag v1.0 for lightweight, git tag -a v1.0 -m "message" for annotated. Always use annotated tags for releases; --follow-tags only pushes annotated tags.
- Why aren't tags pushed with
git pushby default?
Answer
Tags are not pushed by default because they're considered local metadata by Git. Automatically pushing all tags could pollute the remote with personal bookmarks or test tags. You must explicitly push tags with git push origin <tagname>, git push --tags (all tags), or git push --follow-tags (annotated reachable tags only). Configure git config --global push.followTags true to auto-push annotated tags.
- How do you indicate a breaking change in a conventional commit?
Answer
Two ways (can be combined):
- Add
!after the type/scope:feat!: redesign APIorfeat(api)!: change response format - Add
BREAKING CHANGE:in the footer:
feat: redesign API
BREAKING CHANGE: the /api/v1 endpoints have been removed.
Use /api/v2 instead.
If using !, it's still recommended to include the BREAKING CHANGE: footer with a detailed description.
- What does
standard-versiondo when you run it?
Answer
In one command, standard-version:
- Reads commits since the last tag
- Determines the version bump from commit types (feat → minor, fix → patch, BREAKING CHANGE → major)
- Updates
CHANGELOG.mdwith formatted entries grouped by type - Bumps the version in
package.json - Creates a commit:
chore(release): X.Y.Z - Creates an annotated Git tag:
vX.Y.Z
You then push with git push --follow-tags to publish the release.
- What is the
0.x.xconvention in semver?
Answer
Versions starting with 0 (e.g., 0.3.1) indicate that the software is in initial development and the API is not yet stable. Breaking changes can happen in any release without bumping the major version. This gives maintainers flexibility during early development. Publishing 1.0.0 signals that the public API is stable and semver rules apply strictly from that point forward.
- How does
release-pleasediffer fromstandard-version?
Answer
standard-version runs locally or in CI and directly modifies files, commits, and tags. release-please is a GitHub Action that works through pull requests: it automatically creates/updates a "Release PR" containing the changelog and version bump. When you merge the Release PR, it creates the GitHub Release and tag. This gives you a review step before publishing. release-please is Google's recommended replacement for standard-version (which is now in maintenance mode).
- You have commits
fix: ...,docs: ...,feat: ..., andfix: ...since your last release atv3.2.1. What should the next version be?
Answer
The next version should be v3.3.0. The highest-impact commit is feat (MINOR bump). The fix commits would only warrant a PATCH bump, and docs doesn't trigger any bump. Since MINOR is the highest, the version goes from 3.2.1 → 3.3.0 (minor incremented, patch reset to 0). If any commit had BREAKING CHANGE or !, it would be 4.0.0 instead.