How to Implement a Simple Continuous Integration Loop in Batch Script
Continuous Integration (CI) is the practice of automatically building and testing code every time changes are detected. While most teams use dedicated CI servers like Jenkins, GitHub Actions, or Azure DevOps, a simple CI loop can be implemented directly in a Batch Script. This is useful for local development workflows, small teams without CI infrastructure, educational purposes, and as a lightweight build monitor for projects that do not justify a full CI platform.
In this guide, we will explore how to build a simple continuous integration loop in Batch Script that watches for changes, triggers builds, runs tests, and reports results.
Understanding the CI Loop
A basic CI loop follows this cycle:
+-----------+
| Watch |
| for |<-----------+
| changes | |
+-----+-----+ |
| |
v |
+-----+-----+ |
| Pull | |
| latest | |
+-----+-----+ |
| |
v |
+-----+-----+ |
| Build | |
+-----+-----+ |
| |
v |
+-----+-----+ |
| Test | |
+-----+-----+ |
| |
v |
+-----+-----+ |
| Report |------------+
+-----------+
Method 1: Git Polling CI Loop
Poll a Git repository for new commits and trigger builds:
@echo off
title CI Monitor
setlocal enabledelayedexpansion
set "repo_dir=C:\Projects\MyApp"
set "branch=main"
set "poll_interval=30"
:: Verify prerequisites
where git >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Git not found.
pause
exit /b 1
)
if not exist "%repo_dir%\.git\" (
echo [ERROR] Not a git repository: %repo_dir%
pause
exit /b 1
)
:: Generate locale-safe log filename
for /f "tokens=2 delims==" %%T in ('wmic os get LocalDateTime /value') do set "dt=%%T"
set "logfile=ci_log_%dt:~0,4%%dt:~4,2%%dt:~6,2%.txt"
echo =============================================
echo CONTINUOUS INTEGRATION LOOP
echo Repository: %repo_dir%
echo Branch: %branch%
echo Interval: %poll_interval% seconds
echo Log: %logfile%
echo Press Ctrl+C to stop
echo =============================================
echo.
:loop
:: Get current HEAD
pushd "%repo_dir%"
git fetch origin %branch% >nul 2>&1
if !errorlevel! neq 0 (
echo [%time:~0,8%] Fetch failed. Retrying in %poll_interval%s...
popd
timeout /t %poll_interval% /nobreak >nul
goto loop
)
set "remote_commit="
set "local_commit="
for /f "delims=" %%H in ('git rev-parse origin/%branch% 2^>nul') do set "remote_commit=%%H"
for /f "delims=" %%H in ('git rev-parse %branch% 2^>nul') do set "local_commit=%%H"
:: Check if there are new commits
if "!remote_commit!"=="!local_commit!" (
echo [%time:~0,8%] No changes. Waiting %poll_interval%s...
popd
timeout /t %poll_interval% /nobreak >nul
goto loop
)
echo.
echo [%time:~0,8%] NEW COMMITS DETECTED
echo Local: !local_commit:~0,8!
echo Remote: !remote_commit:~0,8!
echo.
:: Pull changes
echo [BUILD] Pulling latest...
git pull origin %branch% >nul 2>&1
:: Build
echo [BUILD] Building...
call build.bat >"%temp%\ci_build.log" 2>&1
set "build_result=!errorlevel!"
if !build_result!==0 (
echo [BUILD] SUCCESS
echo [%date% %time:~0,8%] BUILD SUCCESS - !remote_commit:~0,8! >> "%logfile%"
:: Run tests
echo [TEST] Running tests...
call test.bat >"%temp%\ci_test.log" 2>&1
set "test_result=!errorlevel!"
if !test_result!==0 (
echo [TEST] PASSED
echo [%date% %time:~0,8%] TESTS PASSED >> "%logfile%"
) else (
echo [TEST] FAILED
echo [%date% %time:~0,8%] TESTS FAILED >> "%logfile%"
)
) else (
echo [BUILD] FAILED
echo [%date% %time:~0,8%] BUILD FAILED - !remote_commit:~0,8! >> "%logfile%"
)
popd
echo.
timeout /t %poll_interval% /nobreak >nul
goto loop
Method 2: File-System Change Detection
Watch for file modifications instead of Git commits:
@echo off
title CI - File Watcher
setlocal enabledelayedexpansion
set "watch_dir=C:\Projects\MyApp\src"
set "project_dir=C:\Projects\MyApp"
set "poll_interval=10"
set "file_patterns=*.cs *.java *.js"
if not exist "%watch_dir%\" (
echo [ERROR] Watch directory not found: %watch_dir%
pause
exit /b 1
)
echo =============================================
echo FILE-BASED CI LOOP
echo Watching: %watch_dir%
echo Patterns: %file_patterns%
echo Interval: %poll_interval%s
echo Press Ctrl+C to stop
echo =============================================
echo.
:: Compute initial state
call :compute_hash
set "last_hash=!file_hash!"
echo [%time:~0,8%] Initial state captured.
:loop
timeout /t %poll_interval% /nobreak >nul
:: Compute current hash
call :compute_hash
if "!file_hash!" neq "!last_hash!" (
echo.
echo [%time:~0,8%] CHANGES DETECTED
:: Trigger build
call :run_build
set "last_hash=!file_hash!"
) else (
echo [%time:~0,8%] No changes.
)
goto loop
:compute_hash
:: Create a composite hash of all source file timestamps and sizes
set "file_hash="
for %%P in (%file_patterns%) do (
for /f "delims=" %%F in ('dir /b /s /a-d "%watch_dir%\%%P" 2^>nul') do (
set "file_hash=!file_hash!%%~tF%%~zF"
)
)
if not defined file_hash set "file_hash=EMPTY"
exit /b
:run_build
echo [1/2] Building...
pushd "%project_dir%"
dotnet build -c Release --verbosity quiet >nul 2>&1
if !errorlevel!==0 (
echo [OK] Build passed.
echo [2/2] Testing...
dotnet test -c Release --no-build --verbosity quiet >nul 2>&1
if !errorlevel!==0 (
echo [OK] Tests passed.
) else (
echo [FAIL] Tests failed.
)
) else (
echo [FAIL] Build failed.
)
popd
echo.
exit /b
Method 3: Full CI Pipeline with Reporting
@echo off
title CI Pipeline
setlocal enabledelayedexpansion
:: Configuration
set "repo=C:\Projects\MyApp"
set "branch=main"
set "poll_interval=60"
set "report_dir=C:\CI\Reports"
:: Verify prerequisites
where git >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Git not found.
exit /b 1
)
if not exist "%repo%\.git\" (
echo [ERROR] Not a git repository: %repo%
exit /b 1
)
if not exist "%report_dir%" mkdir "%report_dir%"
echo =============================================
echo CI PIPELINE SERVER
echo Repository: %repo%
echo Branch: %branch%
echo Reports: %report_dir%
echo =============================================
echo.
set "build_number=0"
if exist "%report_dir%\build_counter.txt" (
set /p "build_number=" < "%report_dir%\build_counter.txt"
)
:loop
pushd "%repo%"
:: Check for changes
git fetch origin %branch% >nul 2>&1
if !errorlevel! neq 0 (
echo [%time:~0,8%] Fetch failed. Retrying...
popd
timeout /t %poll_interval% /nobreak >nul
goto loop
)
set "remote="
set "local="
for /f "delims=" %%H in ('git rev-parse origin/%branch% 2^>nul') do set "remote=%%H"
for /f "delims=" %%H in ('git rev-parse %branch% 2^>nul') do set "local=%%H"
if "!remote!"=="!local!" (
echo [%time:~0,8%] Waiting... (last: Build #!build_number!^)
popd
timeout /t %poll_interval% /nobreak >nul
goto loop
)
:: New changes - start build
set /a build_number+=1
>"%report_dir%\build_counter.txt" echo !build_number!
set "commit_msg="
set "author="
for /f "delims=" %%M in ('git log origin/%branch% -1 --pretty^=format:"%%s" 2^>nul') do set "commit_msg=%%M"
for /f "delims=" %%A in ('git log origin/%branch% -1 --pretty^=format:"%%an" 2^>nul') do set "author=%%A"
for /f "tokens=2 delims==" %%T in ('wmic os get LocalDateTime /value') do set "dt=%%T"
set "build_time=%dt:~0,4%-%dt:~4,2%-%dt:~6,2% %dt:~8,2%:%dt:~10,2%:%dt:~12,2%"
set "report=%report_dir%\build_!build_number!.txt"
(
echo =============================================
echo BUILD #!build_number!
echo Date: !build_time!
echo Commit: !remote:~0,8!
echo Author: !author!
echo Message: !commit_msg!
echo =============================================
echo.
) > "!report!"
echo.
echo =============================================
echo BUILD #!build_number! STARTED
echo Commit: !remote:~0,8! by !author!
echo !commit_msg!
echo =============================================
:: Stage 1: Pull
echo [1/4] Pulling changes...
git pull origin %branch% >> "!report!" 2>&1
echo. >> "!report!"
:: Stage 2: Build
echo [2/4] Building...
echo --- BUILD --- >> "!report!"
dotnet build -c Release --verbosity minimal >> "!report!" 2>&1
set "build_status=!errorlevel!"
:: Stage 3: Test
set "test_status=-1"
if !build_status!==0 (
echo [3/4] Testing...
echo --- TESTS --- >> "!report!"
dotnet test -c Release --no-build --verbosity minimal >> "!report!" 2>&1
set "test_status=!errorlevel!"
) else (
echo [3/4] Skipped (build failed^)
)
:: Stage 4: Report
echo [4/4] Reporting...
if !build_status!==0 if !test_status!==0 (
set "status=PASSED"
echo Result: ALL PASSED
) else if !build_status! neq 0 (
set "status=BUILD_FAILED"
echo Result: BUILD FAILED
) else (
set "status=TESTS_FAILED"
echo Result: TESTS FAILED
)
echo. >> "!report!"
echo RESULT: !status! >> "!report!"
popd
timeout /t %poll_interval% /nobreak >nul
goto loop
Method 4: CI Dashboard
Display build history and current status:
@echo off
title CI Dashboard
setlocal enabledelayedexpansion
set "report_dir=C:\CI\Reports"
set "display_count=10"
set "refresh_interval=30"
if not exist "%report_dir%\" (
echo [ERROR] Report directory not found: %report_dir%
pause
exit /b 1
)
:refresh
cls
echo =============================================
echo CI BUILD DASHBOARD
echo %date% %time:~0,8%
echo =============================================
echo.
echo Recent builds (last %display_count%^):
echo -----------------------------------------------
set "count=0"
set "passed=0"
set "failed=0"
for /f "delims=" %%F in ('dir /b /o-d "%report_dir%\build_*.txt" 2^>nul') do (
set /a count+=1
if !count! leq %display_count% (
:: Extract status from report
set "status=UNKNOWN"
for /f "tokens=2" %%S in ('findstr /b "RESULT:" "%report_dir%\%%F" 2^>nul') do set "status=%%S"
:: Extract commit message
set "msg="
for /f "tokens=1,* delims=:" %%A in ('findstr /b " Message:" "%report_dir%\%%F" 2^>nul') do set "msg=%%B"
if "!status!"=="PASSED" (
echo [OK] %%~nF -!msg!
set /a passed+=1
) else (
echo [FAIL] %%~nF -!msg!
set /a failed+=1
)
)
)
if !count!==0 echo No builds yet.
echo.
echo -----------------------------------------------
set /a displayed=count
if !displayed! gtr %display_count% set "displayed=%display_count%"
echo Showing: !displayed! Total: !count! Passed: !passed! Failed: !failed!
echo.
echo Refreshing in %refresh_interval% seconds...
timeout /t %refresh_interval% /nobreak >nul
goto refresh
Method 5: Multi-Project CI
Monitor multiple repositories:
@echo off
title Multi-Project CI
setlocal enabledelayedexpansion
:: Define projects: path|branch|build_command
set "proj_count=3"
set "proj[0]=C:\Projects\Frontend|main|call npm run build"
set "proj[1]=C:\Projects\API|main|dotnet build -c Release --verbosity quiet"
set "proj[2]=C:\Projects\Worker|main|call mvn package -B -q"
set "poll_interval=60"
:: Verify git
where git >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Git not found.
exit /b 1
)
echo =============================================
echo MULTI-PROJECT CI MONITOR
echo Projects: %proj_count%
echo Interval: %poll_interval%s
echo Press Ctrl+C to stop
echo =============================================
echo.
set /a last=proj_count - 1
:loop
echo [%time:~0,8%] Checking all projects...
for /L %%I in (0,1,!last!) do (
for /f "tokens=1,2,3 delims=|" %%A in ("!proj[%%I]!") do (
set "proj_dir=%%A"
set "proj_branch=%%B"
set "proj_cmd=%%C"
if not exist "!proj_dir!\.git\" (
echo [%%I] !proj_dir! - not a git repo
) else (
pushd "!proj_dir!"
git fetch origin !proj_branch! >nul 2>&1
set "remote="
set "local="
for /f "delims=" %%H in ('git rev-parse origin/!proj_branch! 2^>nul') do set "remote=%%H"
for /f "delims=" %%H in ('git rev-parse !proj_branch! 2^>nul') do set "local=%%H"
if "!remote!" neq "!local!" (
echo.
echo [%%I] CHANGES in !proj_dir!
git pull origin !proj_branch! >nul 2>&1
!proj_cmd! >nul 2>&1
if !errorlevel!==0 (
echo [OK] Build passed.
) else (
echo [FAIL] Build failed.
)
) else (
echo [%%I] No changes in !proj_dir!
)
popd
)
)
)
echo.
timeout /t %poll_interval% /nobreak >nul
goto loop
Common Mistakes
The Wrong Way: No Delay Between Polls
:: WRONG - Hammers Git server and CPU with no delay
:loop
git fetch origin
git pull origin main
dotnet build
goto loop
:: 100% CPU usage, thousands of git requests per minute
Output Concern: Polling without delay wastes CPU, network bandwidth, and may trigger rate limits on Git hosting services. Always include a polling interval of at least 30–60 seconds.
The Wrong Way: Not Cleaning Between Builds
:: WRONG - Stale artifacts from previous builds contaminate new ones
:loop
git pull
dotnet build
goto loop
:: Passes locally but fails on clean CI server
Always clean build output before building to ensure each build is produced from the current source code only.
Best Practices
- Use reasonable poll intervals: 30–60 seconds balances responsiveness with resource usage.
- Clean before building: Ensure each build starts from a clean state.
- Log everything: Record build output, test results, and status for debugging.
- Fail fast: Stop the pipeline at the first failure stage.
- Track build history: Maintain reports for trend analysis and debugging.
Conclusion
Implementing a simple continuous integration loop in Batch Script provides a lightweight, dependency-free build monitor that watches for changes, triggers builds, runs tests, and reports results. While not a replacement for dedicated CI platforms like Jenkins or GitHub Actions, a Batch-based CI loop is valuable for local development workflows, small teams, and educational purposes. The core pattern of poll, pull, build, test, and report can be extended with dashboards, multi-project monitoring, and notification systems for a surprisingly capable CI solution.