DEV Community

Alex Lebed 🐧
Alex Lebed 🐧

Posted on

Automating Version Updates in Node.js with Git Hooks

For rapid development, I discovered it’s helpful to update the version with every commit. This approach uses commit message prefixes to automatically bump patch, minor, or major versions, saving valuable time. However, it turned out to be trickier than I initially expected, taking me a few hours to get it right.

I’ve tried automating version updates with typical libraries like standard-version and various release tools. While these tools promise a lot, I encountered constant errors — whether it was misconfigured CI/CD pipelines or unexpected local behavior. After much trial and error, I finally found a solution that worked like a charm: jusky + Git hooks. I’m sharing it here in the hope it helps someone else.


The Problem: Version Updates That Stick with Commits

My goal was simple yet tricky: I wanted to automatically bump the version in package.json based on my commit messages—following the Conventional Commits format like feat:, fix:, or BREAKING CHANGE:. More importantly, I needed this version update to be part of the same commit as my changes. Tools like standard-version are fantastic for creating releases, but they’re not designed for per-commit updates. They often require intricate configurations, especially for CI/CD, which felt like overkill for my local development needs. I wanted something simpler, reliable, and tied directly to my Git workflow.


Why Git Hooks?

Enter Git hooks—small scripts that run at specific points in the Git lifecycle, like before or after a commit. They’re lightweight, built into Git, and perfect for customizing your workflow. For my version update challenge, two hooks stood out:

  • commit-msg: Runs after you write a commit message, letting you analyze it and make changes before the commit is finalized.
  • post-commit: Runs after the commit is created, allowing you to tweak it further.

Using these hooks together, I devised a solution that updates the version and includes it in the commit—all without external dependencies.


The Solution: A Two-Hook Approach

Here’s how I made it work with a two-step process using commit-msg and post-commit hooks. I’ll share the scripts so you can try it yourself.

Step 1: The commit-msg Hook

This hook reads your commit message, decides how to bump the version, updates package.json, and stages it. It also adds a temporary marker to signal that the commit needs refinement.

Create a file at .git/hooks/commit-msg and add this script (make it executable with chmod +x .git/hooks/commit-msg):

#!/bin/bash

# Read the commit message
COMMIT_MSG_FILE=$1
COMMIT_MSG=$(cat "$COMMIT_MSG_FILE")

# Function to bump version (simplified for this example)
bump_version() {
  CURRENT_VERSION=$(node -p "require('./package.json').version")
  IFS='.' read -r -a VERSION_PARTS <<< "$CURRENT_VERSION"
  MAJOR=${VERSION_PARTS[0]}
  MINOR=${VERSION_PARTS[1]}
  PATCH=${VERSION_PARTS[2]}

  if echo "$COMMIT_MSG" | grep -q "^BREAKING CHANGE:"; then
    MAJOR=$((MAJOR + 1))
    MINOR=0
    PATCH=0
  elif echo "$COMMIT_MSG" | grep -q "^feat:"; then
    MINOR=$((MINOR + 1))
    PATCH=0
  elif echo "$COMMIT_MSG" | grep -q "^fix:"; then
    PATCH=$((PATCH + 1))
  else
    exit 0 # No version bump needed
  fi

  NEW_VERSION="$MAJOR.$MINOR.$PATCH"
  # Update package.json
  node -e "const pkg = require('./package.json'); pkg.version = '$NEW_VERSION'; require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2));"
}

# Check if commit message matches Conventional Commits
if echo "$COMMIT_MSG" | grep -q -E "^(feat|fix|BREAKING CHANGE):"; then
  bump_version
  git add package.json
  # Append a marker to the commit message
  echo "$COMMIT_MSG" > "$COMMIT_MSG_FILE"
  echo "[amend-package-json]" >> "$COMMIT_MSG_FILE"
fi
Enter fullscreen mode Exit fullscreen mode

Step 2: The post-commit Hook

This hook checks for the marker, removes it, and amends the commit to include the updated package.json with a clean message.

Create .git/hooks/post-commit and add (make it executable too):

#!/bin/bash

# Get the latest commit message
COMMIT_MSG=$(git log -1 --pretty=%B)

# Check for the marker
if echo "$COMMIT_MSG" | grep -q "\[amend-package-json\]"; then
  # Remove the marker from the message
  CLEAN_MSG=$(echo "$COMMIT_MSG" | sed 's/\[amend-package-json\]//')
  # Amend the commit with the updated package.json and clean message
  git commit --amend --no-verify -m "$CLEAN_MSG"
fi
Enter fullscreen mode Exit fullscreen mode

How It Works

Here’s the flow in action:

  1. You run git commit -m "feat: add new feature".
  2. The commit-msg hook:
    • Detects feat:, bumps the version (e.g., 1.0.0 to 1.1.0).
    • Updates and stages package.json.
    • Adds [amend-package-json] to the commit message.
  3. The initial commit is created with the marker (e.g., feat: add new feature [amend-package-json]).
  4. The post-commit hook:
    • Spots the marker.
    • Amends the commit, removing the marker and including the staged package.json.
  5. The final commit contains your changes, the updated package.json, and a clean message: feat: add new feature.

Why a Single Git Hook Won’t Work—and the Issues I Encountered

When I set out to automate version updates in package.json based on commit messages, I hoped a single Git hook could handle it all. However, I hit a wall due to how Git’s commit process works. I first tried the commit-msg hook, which runs after the commit message is written but before the commit is finalized—it seemed ideal for analyzing the message and updating the version. The problem? At that point, the commit is already in progress, so any changes I staged, like an updated package.json, wouldn’t be included in that commit; they’d just sit there for the next one. I experimented with tricks to force it in, but nothing worked. The pre-commit hook was no better—it runs before the message is even written, so I couldn’t base the version bump on it. After wrestling with these limitations and a lot of trial-and-error, I realized one hook couldn’t cut it. I needed two: commit-msg to update and stage the file, and post-commit to amend the commit, ensuring the version update landed in the same commit as intended. It was a frustrating journey, but this combo finally did the trick!

Testing and Validation

To ensure it works, I tested it thoroughly:

  • feat: add something → Minor version bump (e.g., 1.0.0 to 1.1.0).
  • fix: tweak bug → Patch bump (e.g., 1.1.0 to 1.1.1).
  • BREAKING CHANGE: overhaul system → Major bump (e.g., 1.1.1 to 2.0.0).

After each commit, I checked git log to confirm the message was clean and git diff to verify package.json was included. It worked like a charm every time.


A Note on Edge Cases

This solution assumes single commits. For merge commits or rebases, you might need to tweak the hooks (e.g., skip version bumps during merges). I’ll leave that as an exercise for you, but the core idea remains solid.


Top comments (0)