
I recently shipped ContactLink, a simple link-in-bio project, and used it as a testing ground to implement a robust and professional Git workflow. This setup automates versioning and changelog generation using semantic-release driven by disciplined commit messages enforced by commitlint. It’s a game-changer for maintaining clean release history and looks great in any professional portfolio!
Using Conventional Commits (enforced by commitlint) ensures that every commit message follows a standard format (e.g., feat: add new button, fix: correct typo). This structure is not just for neatness; it's the foundation for semantic-release, which automatically determines the next version number (major, minor, or patch) and generates the CHANGELOG.md based on your commits.
First, you need the right tools. Here are the core development dependencies you’ll need (as shown in my package.json):
npm install --save-dev @commitlint/cli @commitlint/config-conventional \
semantic-release @semantic-release/changelog @semantic-release/git huskyAfter installation, initialize Husky (a Git hooks manager) and set up the scripts:
npx husky initThis command creates the .husky/ directory. Now, you can add your Git hooks.
I use a commit-msg hook to run commitlint every time I try to commit, ensuring the message adheres to the Conventional Commits specification.
.husky/commit-msg
npx --no -- commitlint --edit $1commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat',
'fix',
'docs',
'style',
'refactor',
'perf',
'test',
'build',
'ci',
'chore',
'revert'
]
]
}
};This configuration makes sure commits start with an approved type (like feat for a new feature or fix for a bug). I also added a pre-commit hook to run linting before a commit can be created, keeping the codebase clean:
.husky/pre-commit
npm run lintThe magic happens in the CI/CD pipeline. My workflow runs semantic-release on every push to the main branch. This process does a few key things:
CHANGELOG.md file.package.json version.Here’s the GitHub Actions workflow:
.github/workflows/release.yml
name: release
on:
push:
branches:
- main
permissions:
contents: write
issues: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci
- name: Release
env:
GITHUB_TOKEN: ${{ secrents.GITHUB_TOKEN }}
run: npm run semantic-releaseCrucial Setup Note: Forsemantic-releaseto be able to commit the updatedCHANGELOG.mdand version files back to the repository, you must update your GitHub repository settings. Go to Settings -> Actions -> General -> Workflow permissions and ensure Read and write permissions is selected. Also, ensure you have an emptyCHANGELOG.mdfile in your repository to start.
The .releaserc.json file orchestrates which steps and plugins semantic-release should execute.
.releaserc.json
{
"branches": ["main", "master"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md"
}
],
[
"@semantic-release/npm",
{
"npmPublish": false
}
],
[
"@semantic-release/git",
{
"assets": ["CHANGELOG.md", "package.json", "package-lock.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
}
],
"@semantic-release/github"
]
}The key parts here are the @semantic-release/changelog and @semantic-release/git plugins, which handle updating the local files and committing them back, respectively. The commit message includes [skip ci] to prevent an infinite loop of releases triggering the workflow again!
Keep building, keep automating!