Skip to content

How to Implement a Simple Continuous Integration Loop in Batch Script

Learn how to create a lightweight CI system in Batch Script for local development, small teams, or educational use with Git polling, file monitoring, and automated builds/tests.

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

  1. Use reasonable poll intervals: 30–60 seconds balances responsiveness with resource usage.
  2. Clean before building: Ensure each build starts from a clean state.
  3. Log everything: Record build output, test results, and status for debugging.
  4. Fail fast: Stop the pipeline at the first failure stage.
  5. 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.

Navigation