<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Alex Lebed 🐧</title>
    <description>The latest articles on Forem by Alex Lebed 🐧 (@lebed2045).</description>
    <link>https://forem.com/lebed2045</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F234082%2Fbe149278-2ca2-49dd-9cd7-ef80c30bdc75.jpg</url>
      <title>Forem: Alex Lebed 🐧</title>
      <link>https://forem.com/lebed2045</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/lebed2045"/>
    <language>en</language>
    <item>
      <title>Automating Version Updates in Node.js with Git Hooks</title>
      <dc:creator>Alex Lebed 🐧</dc:creator>
      <pubDate>Sat, 08 Mar 2025 00:05:39 +0000</pubDate>
      <link>https://forem.com/lebed2045/automating-version-updates-in-nodejs-with-git-hooks-5dfa</link>
      <guid>https://forem.com/lebed2045/automating-version-updates-in-nodejs-with-git-hooks-5dfa</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;I’ve tried automating version updates with typical libraries like &lt;code&gt;standard-version&lt;/code&gt; 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.&lt;/p&gt;




&lt;h3&gt;
  
  
  The Problem: Version Updates That Stick with Commits
&lt;/h3&gt;

&lt;p&gt;My goal was simple yet tricky: I wanted to automatically bump the version in &lt;code&gt;package.json&lt;/code&gt; based on my commit messages—following the Conventional Commits format like &lt;code&gt;feat:&lt;/code&gt;, &lt;code&gt;fix:&lt;/code&gt;, or &lt;code&gt;BREAKING CHANGE:&lt;/code&gt;. More importantly, I needed this version update to be part of the &lt;em&gt;same commit&lt;/em&gt; as my changes. Tools like &lt;code&gt;standard-version&lt;/code&gt; 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.&lt;/p&gt;




&lt;h3&gt;
  
  
  Why Git Hooks?
&lt;/h3&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;commit-msg&lt;/code&gt;&lt;/strong&gt;: Runs after you write a commit message, letting you analyze it and make changes before the commit is finalized.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;post-commit&lt;/code&gt;&lt;/strong&gt;: Runs after the commit is created, allowing you to tweak it further.&lt;/li&gt;
&lt;/ul&gt;

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




&lt;h3&gt;
  
  
  The Solution: A Two-Hook Approach
&lt;/h3&gt;

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

&lt;h4&gt;
  
  
  Step 1: The &lt;code&gt;commit-msg&lt;/code&gt; Hook
&lt;/h4&gt;

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

&lt;p&gt;Create a file at &lt;code&gt;.git/hooks/commit-msg&lt;/code&gt; and add this script (make it executable with &lt;code&gt;chmod +x .git/hooks/commit-msg&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# Read the commit message&lt;/span&gt;
&lt;span class="nv"&gt;COMMIT_MSG_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;
&lt;span class="nv"&gt;COMMIT_MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMMIT_MSG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Function to bump version (simplified for this example)&lt;/span&gt;
bump_version&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;CURRENT_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;node &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"require('./package.json').version"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'.'&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt; VERSION_PARTS &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CURRENT_VERSION&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;MAJOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VERSION_PARTS&lt;/span&gt;&lt;span class="p"&gt;[0]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
  &lt;span class="nv"&gt;MINOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VERSION_PARTS&lt;/span&gt;&lt;span class="p"&gt;[1]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
  &lt;span class="nv"&gt;PATCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;VERSION_PARTS&lt;/span&gt;&lt;span class="p"&gt;[2]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMMIT_MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"^BREAKING CHANGE:"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;MAJOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;MAJOR &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
    &lt;span class="nv"&gt;MINOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
    &lt;span class="nv"&gt;PATCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
  &lt;span class="k"&gt;elif &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMMIT_MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"^feat:"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;MINOR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;MINOR &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
    &lt;span class="nv"&gt;PATCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
  &lt;span class="k"&gt;elif &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMMIT_MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"^fix:"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;PATCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;PATCH &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0 &lt;span class="c"&gt;# No version bump needed&lt;/span&gt;
  &lt;span class="k"&gt;fi

  &lt;/span&gt;&lt;span class="nv"&gt;NEW_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$MAJOR&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$MINOR&lt;/span&gt;&lt;span class="s2"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;$PATCH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="c"&gt;# Update package.json&lt;/span&gt;
  node &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"const pkg = require('./package.json'); pkg.version = '&lt;/span&gt;&lt;span class="nv"&gt;$NEW_VERSION&lt;/span&gt;&lt;span class="s2"&gt;'; require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2));"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Check if commit message matches Conventional Commits&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMMIT_MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"^(feat|fix|BREAKING CHANGE):"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;bump_version
  git add package.json
  &lt;span class="c"&gt;# Append a marker to the commit message&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMMIT_MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMMIT_MSG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"[amend-package-json]"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMMIT_MSG_FILE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 2: The &lt;code&gt;post-commit&lt;/code&gt; Hook
&lt;/h4&gt;

&lt;p&gt;This hook checks for the marker, removes it, and amends the commit to include the updated &lt;code&gt;package.json&lt;/code&gt; with a clean message.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;.git/hooks/post-commit&lt;/code&gt; and add (make it executable too):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="c"&gt;# Get the latest commit message&lt;/span&gt;
&lt;span class="nv"&gt;COMMIT_MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git log &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="nt"&gt;--pretty&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;%B&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Check for the marker&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMMIT_MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\[&lt;/span&gt;&lt;span class="s2"&gt;amend-package-json&lt;/span&gt;&lt;span class="se"&gt;\]&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
  &lt;span class="c"&gt;# Remove the marker from the message&lt;/span&gt;
  &lt;span class="nv"&gt;CLEAN_MSG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$COMMIT_MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/\[amend-package-json\]//'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="c"&gt;# Amend the commit with the updated package.json and clean message&lt;/span&gt;
  git commit &lt;span class="nt"&gt;--amend&lt;/span&gt; &lt;span class="nt"&gt;--no-verify&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLEAN_MSG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  How It Works
&lt;/h3&gt;

&lt;p&gt;Here’s the flow in action:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You run &lt;code&gt;git commit -m "feat: add new feature"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;commit-msg&lt;/code&gt; hook:

&lt;ul&gt;
&lt;li&gt;Detects &lt;code&gt;feat:&lt;/code&gt;, bumps the version (e.g., &lt;code&gt;1.0.0&lt;/code&gt; to &lt;code&gt;1.1.0&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Updates and stages &lt;code&gt;package.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Adds &lt;code&gt;[amend-package-json]&lt;/code&gt; to the commit message.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The initial commit is created with the marker (e.g., &lt;code&gt;feat: add new feature [amend-package-json]&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;post-commit&lt;/code&gt; hook:

&lt;ul&gt;
&lt;li&gt;Spots the marker.&lt;/li&gt;
&lt;li&gt;Amends the commit, removing the marker and including the staged &lt;code&gt;package.json&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;The final commit contains your changes, the updated &lt;code&gt;package.json&lt;/code&gt;, and a clean message: &lt;code&gt;feat: add new feature&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Why a Single Git Hook Won’t Work—and the Issues I Encountered
&lt;/h3&gt;

&lt;p&gt;When I set out to automate version updates in &lt;code&gt;package.json&lt;/code&gt; 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 &lt;code&gt;commit-msg&lt;/code&gt; 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 &lt;code&gt;package.json&lt;/code&gt;, wouldn’t be included in &lt;em&gt;that&lt;/em&gt; commit; they’d just sit there for the next one. I experimented with tricks to force it in, but nothing worked. The &lt;code&gt;pre-commit&lt;/code&gt; 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: &lt;code&gt;commit-msg&lt;/code&gt; to update and stage the file, and &lt;code&gt;post-commit&lt;/code&gt; 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!&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing and Validation
&lt;/h3&gt;

&lt;p&gt;To ensure it works, I tested it thoroughly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;feat: add something&lt;/code&gt; → Minor version bump (e.g., &lt;code&gt;1.0.0&lt;/code&gt; to &lt;code&gt;1.1.0&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fix: tweak bug&lt;/code&gt; → Patch bump (e.g., &lt;code&gt;1.1.0&lt;/code&gt; to &lt;code&gt;1.1.1&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;BREAKING CHANGE: overhaul system&lt;/code&gt; → Major bump (e.g., &lt;code&gt;1.1.1&lt;/code&gt; to &lt;code&gt;2.0.0&lt;/code&gt;).&lt;/li&gt;
&lt;/ul&gt;

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




&lt;h3&gt;
  
  
  A Note on Edge Cases
&lt;/h3&gt;

&lt;p&gt;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.&lt;/p&gt;




</description>
      <category>git</category>
      <category>node</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
