How to Auto-Increment a Build Number in Batch Script
An auto-incrementing build number provides a unique, ever-increasing identifier for every build of your software. Unlike manually managed version numbers, build numbers increment automatically each time the build script runs, ensuring that no two builds share the same identifier. This is essential for traceability, deployment tracking, and distinguishing between builds that share the same semantic version.
In this guide, we will explore multiple approaches to auto-incrementing build numbers in Batch Script, from simple file-based counters to timestamp-based strategies and CI/CD integration.
Method 1: File-Based Build Counter
The simplest approach stores the build number in a plain text file and increments it on each run.
@echo off
setlocal enabledelayedexpansion
set "build_file=build_number.txt"
:: Read current build number (default to 0 if file is missing or empty)
set "build=0"
if exist "%build_file%" (
set /p "build=" < "%build_file%"
)
:: Increment
set /a build+=1
:: Save new build number (redirect first to avoid trailing space)
>"%build_file%" echo !build!
echo =============================================
echo BUILD #!build!
echo %date% %time:~0,8%
echo =============================================
echo.
:: Use the build number in your build process
echo Build number !build! is now available.
echo Use %%build%% in subsequent commands.
pause
Key Points
- The file contains a single number (e.g.,
1547). - Each run reads, increments, and writes back.
- The file should be committed to source control if you want consistent numbering across machines.
- Alternatively, exclude it from source control if each developer's machine should have independent build counts.
Method 2: Build Number with Version Stamp
Combine a semantic version with an auto-incrementing build number:
@echo off
setlocal enabledelayedexpansion
set "version_file=VERSION"
set "build_file=BUILD_NUMBER"
:: Read version (default if missing)
set "version=1.0.0"
if exist "%version_file%" (
set /p "version=" < "%version_file%"
) else (
>"%version_file%" echo !version!
)
:: Read and increment build number
set "build=0"
if exist "%build_file%" (
set /p "build=" < "%build_file%"
)
set /a build+=1
>"%build_file%" echo !build!
:: Compose full version
set "full_version=!version!.!build!"
echo =============================================
echo VERSION: !full_version!
echo =============================================
echo.
:: Generate a build info file
for /f "tokens=2 delims==" %%T in ('wmic os get LocalDateTime /value') do set "dt=%%T"
set "build_date=%dt:~0,4%-%dt:~4,2%-%dt:~6,2%"
set "build_time=%dt:~8,2%:%dt:~10,2%:%dt:~12,2%"
(
echo version=!version!
echo build=!build!
echo full_version=!full_version!
echo date=!build_date!
echo time=!build_time!
echo machine=%COMPUTERNAME%
echo user=%USERNAME%
) > build_info.txt
echo Build info written to build_info.txt
type build_info.txt
pause
Method 3: Timestamp-Based Build Numbers
Use the date and time to generate a unique build identifier without maintaining a counter file:
@echo off
setlocal
:: Method A: YYYYMMDD.HHMM format
for /f "tokens=2 delims==" %%T in ('wmic os get LocalDateTime /value') do set "dt=%%T"
set "build=%dt:~0,8%.%dt:~8,4%"
echo Build: %build%
:: Output: Build: 20240115.1430
echo.
:: Method B: Days since a reference date (compact number)
powershell -NoProfile -Command ^
"$ref = [datetime]'2024-01-01';" ^
"$days = ((Get-Date) - $ref).Days;" ^
"$secs = [int]((Get-Date).TimeOfDay.TotalSeconds / 2);" ^
"Write-Host ('Build: {0}.{1}' -f $days, $secs)"
:: Output: Build: 14.26100 (14 days since Jan 1, unique within day)
Timestamp-based build numbers are always unique and require no persistent state file. However, they are not sequential in a traditional sense and can be harder to compare at a glance. Use them when simplicity and statelessness are priorities.
Method 4: Atomic Build Counter (Multi-Process Safe)
When multiple build processes might run concurrently, the simple read/increment/write pattern can cause race conditions. Use a lock directory for safety, since mkdir is an atomic operation on Windows:
@echo off
setlocal enabledelayedexpansion
set "build_file=build_number.txt"
set "lock_dir=build_number.lock"
set "max_wait=30"
:: Acquire lock using mkdir (atomic on Windows)
set /a attempts=0
:acquire_lock
mkdir "%lock_dir%" 2>nul && goto got_lock
set /a attempts+=1
if !attempts! geq %max_wait% (
echo [ERROR] Could not acquire build lock after %max_wait% seconds.
echo If no other build is running, delete the "%lock_dir%" folder manually.
exit /b 1
)
timeout /t 1 /nobreak >nul
goto acquire_lock
:got_lock
:: Read and increment (inside critical section)
set "build=0"
if exist "%build_file%" (
set /p "build=" < "%build_file%"
)
set /a build+=1
>"%build_file%" echo !build!
:: Release lock
rmdir "%lock_dir%"
echo Build #!build!
If the script is terminated (Ctrl+C, crash) between acquiring and releasing the lock, the build_number.lock directory remains and blocks subsequent runs. Delete it manually if no other build process is running.
Method 5: Build Number from Git
Derive the build number from the Git commit count, which naturally increments with every commit:
@echo off
setlocal enabledelayedexpansion
:: Total commit count as build number
set "build="
for /f %%C in ('git rev-list HEAD --count 2^>nul') do set "build=%%C"
if not defined build (
echo [WARNING] Not a git repository. Using timestamp.
for /f "tokens=2 delims==" %%T in ('wmic os get LocalDateTime /value') do set "dt=%%T"
set "build=!dt:~0,8!!dt:~8,4!"
)
:: Get short hash for identification
set "hash=N/A"
for /f %%H in ('git rev-parse --short HEAD 2^>nul') do set "hash=%%H"
:: Read version
set "version=0.0.0"
if exist VERSION (
set /p "version=" < VERSION
)
set "full_version=!version!.!build!"
echo =============================================
echo BUILD INFO
echo =============================================
echo Version: !version!
echo Build: !build!
echo Commit: !hash!
echo Full: !full_version!
echo =============================================
pause
Integrating Build Numbers into Source Code
Generating a Build Header (C/C++)
@echo off
setlocal enabledelayedexpansion
set "build_file=build_number.txt"
set "header=src\build_info.h"
:: Increment build
set "build=0"
if exist "%build_file%" (
set /p "build=" < "%build_file%"
)
set /a build+=1
>"%build_file%" echo !build!
:: Generate timestamp
for /f "tokens=2 delims==" %%T in ('wmic os get LocalDateTime /value') do set "dt=%%T"
set "build_date=%dt:~0,4%-%dt:~4,2%-%dt:~6,2%"
set "build_time=%dt:~8,2%:%dt:~10,2%:%dt:~12,2%"
:: Generate header
(
echo // Auto-generated. Do not edit manually.
echo #pragma once
echo #define BUILD_NUMBER !build!
echo #define BUILD_DATE "!build_date!"
echo #define BUILD_TIME "!build_time!"
echo #define BUILD_MACHINE "%COMPUTERNAME%"
) > "%header%"
echo [OK] Generated %header% with build #!build!
Updating package.json (Node.js)
@echo off
setlocal enabledelayedexpansion
:: Use %~dp0 to ensure files are relative to the script's own folder
set "build_file=%~dp0build_number.txt"
set "json_file=%~dp0package.json"
:: Verify package.json exists before starting
if not exist "%json_file%" (
echo [ERROR] "%json_file%" not found. Ensure this script is in the same folder as your project.
pause
exit /b 1
)
set "build=0"
if exist "%build_file%" (
set /p "build=" < "%build_file%"
)
set /a build+=1
echo !build!>"%build_file%"
:: Read current version from package.json - Collapse into single line to avoid ^ bugs
for /f "delims=" %%V in ('powershell -NoProfile -Command "(Get-Content '%json_file%' | ConvertFrom-Json).version"') do set "ver=%%V"
echo Version: %ver% (Build #!build!)
:: Update package.json with the new buildNumber property
powershell -NoProfile -Command "$pkg = Get-Content '%json_file%' | ConvertFrom-Json; $pkg | Add-Member -Force -Name 'buildNumber' -Value !build! -MemberType NoteProperty; $pkg | ConvertTo-Json -Depth 10 | Set-Content '%json_file%'"
echo [OK] package.json updated with build number !build!.
pause
Resetting the Build Number
When bumping a major or minor version, you might want to reset the build number:
@echo off
set "build_file=build_number.txt"
echo Resetting build number to 0...
>"%build_file%" echo 0
echo [OK] Build number reset.
pause
Common Mistakes
The Wrong Way: Using Volatile Environment Variables
:: WRONG - RANDOM changes every time, is not sequential
set "build=%RANDOM%"
Output Concern:
%RANDOM% produces a random number between 0 and 32767, not a sequential build number. Builds could have duplicate or non-sequential numbers, making them useless for ordering or tracking.
The Wrong Way: Not Persisting the Build Number
:: WRONG - Counter resets to 1 every time
set "build=0"
set /a build+=1
echo Build #%build%
:: Always outputs: Build #1
Without reading from and writing to a persistent file, the build number starts at 1 on every execution. The counter must be stored in a file or external system.
Best Practices
- Use a persistent file: Store the build counter in a file that survives script restarts.
- Handle concurrent access: Use lock directories or atomic operations when multiple processes might build simultaneously.
- Combine with semantic version: Use
MAJOR.MINOR.PATCH.BUILDfor complete version identification. - Commit the counter file selectively: Decide whether the counter should be shared (committed) or per-machine (gitignored).
- Consider Git commit counts: For Git-based projects, commit count provides a natural, append-only build number.
Conclusion
Auto-incrementing build numbers in Batch Script is most commonly achieved by reading a counter from a persistent file, incrementing it, and writing it back. This simple pattern can be enhanced with lock directories for concurrent safety, combined with semantic versioning for complete identification, and integrated into source code through generated header files or metadata updates. Whether using file-based counters, Git commit counts, or timestamp-based identifiers, the goal is the same: every build gets a unique, traceable number that connects the deployed artifact back to its source.