4/19/2022 Updates. I published a second post according to this one: Add lint-staged to SvelteKit Project
Recently I spent a lot of time improving our internal SvelteKit project by aligning coding style & repo quality, especially the commit message and versioning.
In this post, I will share how I added Conventional Commits and SemVer standards to our project and walk you through the setup of commitlint, commitlizen, standard-version, and husky.
What are Conventional Commits & SemVer?
Here, I copy the summary from each website. You can find more via the link.
Conventional Commits
The Conventional Commits specification is a lightweight convention on top of commit messages. It provides an easy set of rules for creating an explicit commit history, which makes it easier to write automated tools on top of. This convention dovetails with SemVer, by describing the features, fixes, and breaking changes made in commit messages.
The commit message should be structured as follows:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
/* Example */
feat(landing): add register button and authentication
SemVer - Semantic Versioning
Given a version number MAJOR.MINOR.PATCH, increment the:
MAJOR version when you make incompatible API changes
MINOR version when you add functionality in a backward-compatible manner
PATCH version when you make backward-compatible bug fixes
Additional labels for pre-release and build metadata are available as extensions to the MAJOR.MINOR.PATCH format.
How to adopt in your existing SvelteKit (or any other JavaScript) Project?
You can find the demo repo here: conventional-commits-sveltekit.
commitlint
First, install commitlint
, a linting tool to check if your commit messages meet the conventional commit format:
# Install commitlint cli and conventional config
pnpm add -D @commitlint/config-conventional @commitlint/cli
# Configure commitlint to use conventional config
echo "module.exports = {extends: ['@commitlint/config-conventional']}" > commitlint.config.cjs
Use .cjs instead of .js because
"type": "module"
is set inpackage.json
in SvelteKit project.
Now let's test if commitlint works (no prompt if linting passed):
๐ถ husky
It's recommended to use husky
with commitlint
, a handy git hook helper, to check your commit message before making any git commit
.
Will add more git hooks later.
# Install husky
pnpm add -D husky
# Active hooks
pnpx husky install
# Add hook
npx husky add .husky/commit-msg 'pnpx --no -- commitlint --edit $1'
Let's make a git commit
:
husky
would abort the commit if it didn't pass the commitlint
commitizen
It isn't very pleasant if we need to type the valid commit message every time.
commitizen
is a CLI tool to help us fill out any required commit fields at commit time.
# Install commitizen,
# @commitlint/cz-commitlint (adapter), and
# inquirer (peer dependency of @commitlint/cz-commitlint)
pnpm add -D commitizen @commitlint/cz-commitlint inquirer
Simply use pnpm cz
instead of git commit
when committing. Or add a pnpm script to your package.json
:
...
"scripts": {
"commit": "cz"
}
Let's give it a try:
We use
@commitlint/cz-commitlint
as a commitizen adapter to ensure the commit adheres to the commit convention configured incommitlint.config.cjs
.
standard-version
We've configured commitlint, husky, and commitizen to easily create and validate our commit message.
Now, let's see how we can automate versioning and changelog generation using standard-version
.
# Install standard-version
pnpm add -D standard-version
Add pnpm script to your package.json
.
...
"scripts": {
"release": "standard-version --no-verify"
// use --no-verify to skip git hooks we'll introduce later
}
Here is how standard-version
works:
- Retrieve the current version of your repository by looking at
packageFiles
, falling back to the lastgit tag
.bump
the version inbumpFiles
based on your commits.- Generates a
changelog
based on your commits (uses conventional-changelog under the hood).- Creates a new
commit
including yourbumpFiles
and updatedCHANGELOG.md
.- Creates a new
tag
with the new version number.
Let's try:
By default, CHANGELOG.md
would only show commits belonging to feat
or fix
, but I like to have every commit in my changelog and add emojis.
We can customize it by creating .versionrc
under your project root folder. Here is my setting:
{
"types": [
{
"type": "feat",
"section": "โจ Features"
},
{
"type": "fix",
"section": "๐ Bug Fixes"
},
{
"type": "chore",
"hidden": false,
"section": "๐ Chores"
},
{
"type": "docs",
"hidden": false,
"section": "๐ Documentation"
},
{
"type": "style",
"hidden": false,
"section": "๐ Styling"
},
{
"type": "refactor",
"hidden": false,
"section": "โป๏ธ Code Refactoring"
},
{
"type": "perf",
"hidden": false,
"section": "โก๏ธ Performance Improvements"
},
{
"type": "test",
"hidden": false,
"section": "โ
Testing"
}
]
}
You can see more options in Conventional Changelog Configuration Spec.
๐ถ husky - more git hooks
Before we push commits and tags, we can consider adding more git hooks to improve our code quality. Like formatting, linting, and testing.
Maybe you've already set "Format On Save", run linting, or unit testing in watch mode, but it's a personal preference. To ensure a better code quality and efficient collaboration, we can add git hooks like pre-commit
, and pre-push
to format, lint, and test before opening a PR.
Below we'll take the SvelteKit skeleton project as an example. I choose Typescript, Prettier, ESLint, and Playwright.
First, add formatting and linting at pre-commit
stage:
# Add pre-commit hook
npx husky add .husky/pre-commit 'pnpm format && pnpm lint && git add .'
Second (optional), add svelte-check
at pre-push
stage:
# Add pre-push hook
npx husky add .husky/pre-push 'pnpm check'
I didn't put
playwright test
here because I think it's better to do the e2e test in CI. If you're using a test runner like jest, uvu, or vitest, you may consider running unit/ component tests in thepre-commit
orpre-push
stage.
Caveats
git hooks
is not a silver bullet. Your colleague can still "bypass" it in specific ways, but it'd be good to use and align with your team.
Some argue that "Run linting and testing in
git hooks
is just a drag on productivity and can't force anything. Why do this if we've already had them in CI? It's even useless if your tests didn't run in a production-like environment."
That might be true and depends on your team and your focus.
We use it anyway while thinking about the "Shift-Left" approach in DevOps.
Wrap up
Thank you for your reading! ๐
We've walked through the steps to adopt Conventional Commits and SemVer in your project. Also, there are lots of different commitizen adapters and commitlint configurations you can try.
I tried gitmoji and followed Make everyone in your project write beautiful commit messages using commitlint and commitizen by Sohan Dutta, but end up using the setting in this article because the prior is not compatible to changelog generation of standard-version
(Ref: Issue #859)
My team and I are still working on our first CI/ CD pipeline, and this article is our first step to improve our workflow.
Please kindly share your thoughts and experience. Love to learn from you!
You can find me on Twitter: @davipon
Useful resources:
Continuous Delivery by Dave Farley (Full of wisdom)
Master the Code Review by Curtis Einsmann (I haven't finished yet, but the content is so great that I need to share it here!)