AI Business Maturity Model
Certifications
Find a CoachFind a SpeakerSign In

Bob the Builder

An interactive deployment and quality-check script that gives developers a single entry point for all build, test, and deploy operations.

What Is Bob the Builder?

Bob the Builder (scripts/BobTheBuilder.sh) is a single Bash script that serves as the command center for all development operations. Instead of remembering dozens of npm commands, Docker commands, and deployment steps, developers run one script and choose from a numbered menu.

It auto-selects the most common operation (pre-deployment check) after a 10-second timeout, making it safe to run unattended. When checks pass, it automatically continues to the next logical step (building and deploying).

Menu Structure

The script presents a numbered menu with these options:

0

FastReview

Deploy to dev immediately, skipping all checks. Uses Docker cache for speed. ~2 minutes.

1

Test (Default)

Run TypeScript check, linting (with auto-fix), format check (with auto-fix), and a full Next.js build. If all pass, auto-continues to Local Build. This is the default if no selection is made within 10 seconds.

2

Local Build

Build and run Docker containers without running checks first. Useful when you know the code is clean.

3

Prod Deploy

Full checks + commit + push to GitHub main branch. Monitors production health endpoint until the new commit is live. Includes merge-from-feature-branch support.

4

API Tests

Run API integration tests against the dev server with sub-menus for full, key-systems, or user-story scopes.

5

E2E Tests

Run Playwright browser tests against the dev server with sub-menus for smoke, coach, ascent, or full suite.

6

Setup Dev

First-time setup: checks Node/npm, installs dependencies, validates .env, checks Docker, runs database migrations.

7

Cancel

Exit without doing anything.

Key Design Patterns

These patterns make the script reliable and developer-friendly:

Auto-timeout with safe default

The menu auto-selects option 1 (Test) after 10 seconds. This means running the script unattended does the safest, most useful thing: run all checks and deploy to dev if they pass.

Error accumulation, not early exit

The script tracks errors in an array and shows them all at the end, rather than stopping at the first failure. This gives the developer (or AI) the full picture of what needs fixing.

Auto-fix before failing

Linting and formatting checks attempt auto-fix before reporting failure. The script runs lint:fix and format, then re-checks. Only if issues remain after auto-fix does it report a failure.

Cascading steps

When Test (option 1) passes all checks, it automatically calls Local Build. When Prod Deploy finishes pushing, it automatically monitors the production health endpoint. Steps flow naturally.

Color-coded output

Green for success, yellow for warnings, red for errors, cyan for informational. Every section has clear visual separators. Timestamps in CST for the team's timezone.

AI-friendly error messages

Error summaries are formatted with bullet points and include a banner: "Copy and paste these errors to the AI assistant to get help fixing them." The script is designed to work in a loop with AI coding assistants.

The Pre-Deployment Check Pipeline

Option 1 (Test) runs this four-step pipeline:

Check Pipeline

bash
[1/4] TypeScript Check    → npm run type-check
[2/4] Linting             → npm run lint (auto-fix on failure, re-check)
[3/4] Format Check        → npm run format:check (auto-fix on failure, re-check)
[4/4] Test Build (dry run) → npm run build (catches SSR/prerender errors)

If ALL pass → auto-continues to Local Build (Docker deploy to dev)
If ANY fail → shows error summary with fix commands

Production Deploy Flow

Option 3 (Prod Deploy) runs the full pipeline plus deployment:

Production Deploy Steps

bash
[1/5] TypeScript Check
[2/5] Linting (with auto-fix)
[3/5] Format Check (with auto-fix)
[4/5] Production Build
[5/7] Commit changes (auto-stages, timestamps commit message)
[6/7] Push to GitHub (handles feature branch → merge to main)
[7/7] Monitor production health endpoint until new commit is live
      → Checks every 30 seconds for up to 15 minutes
      → Compares health endpoint commit hash against pushed commit
      → Reports VERIFIED when production matches

Deploy Verification: Commit Hash Polling

Step 7/7 above is the difference between “the site is up” and “the right version is up.” A health check that returns 200 only tells you the server is running. A health check that returns the deployed commit hash tells you which code is running. Push commit abc123, then poll until abc123 appears in the health response — that's deploy verification.

Three pieces are required:

1

Health endpoint returns build metadata

The /api/health endpoint must include the git commit hash, branch, and build time in its response. This is what the polling loop checks against.

2

Build-time injection of GIT_COMMIT_SHA

The commit hash must be baked into the Docker image at build time via build args, then exposed as an environment variable. CI/CD (GitHub Actions) passes the commit SHA as a build arg.

3

Polling loop in the deploy script

After pushing to GitHub, the script polls the health endpoint every 30 seconds for up to 15 minutes. It compares the deployed commit against the pushed commit and reports VERIFIED when they match.

src/app/api/health/route.ts — Build Metadata

typescript
import { NextResponse } from 'next/server';

// Next.js App Router route handler (server component — no 'use client')
// Named healthHandler, then exported as GET below
async function healthHandler(): Promise<NextResponse> {
  return NextResponse.json({
    status: 'healthy',
    timestamp: new Date().toISOString(),
    build: {
      commit: process.env['NEXT_PUBLIC_GIT_COMMIT'] ?? process.env['GIT_COMMIT'] ?? 'unknown',
      branch: process.env['NEXT_PUBLIC_GIT_BRANCH'] ?? process.env['GIT_BRANCH'] ?? 'unknown',
      buildTime: process.env['NEXT_PUBLIC_BUILD_TIME'] ?? process.env['BUILD_TIME'] ?? 'unknown',
    },
  });
}

export { healthHandler as GET };

Dockerfile — Build-Time Injection

dockerfile
# Accept build args
ARG GIT_COMMIT=unknown
ARG GIT_BRANCH=unknown
ARG BUILD_TIME=unknown

# Convert to env vars accessible at runtime
ENV NEXT_PUBLIC_GIT_COMMIT=$GIT_COMMIT
ENV NEXT_PUBLIC_GIT_BRANCH=$GIT_BRANCH
ENV NEXT_PUBLIC_BUILD_TIME=$BUILD_TIME

.github/workflows/deploy.yml — Pass Commit SHA

yaml
- name: Build and push Docker image
  uses: docker/build-push-action@v5
  with:
    build-args: |
      GIT_COMMIT=${{ github.sha }}
      GIT_BRANCH=${{ github.ref_name }}
      BUILD_TIME=${{ steps.date.outputs.date }}

scripts/BobTheBuilder.sh — Polling Loop

bash
verify_deploy() {
    local health_url="$1"      # e.g. https://yourapp.com/api/health
    local expected_commit="$2" # full SHA from: git rev-parse HEAD
    local short_commit="${expected_commit:0:7}"
    local max_attempts=30      # 30 × 30s = 15 minutes
    local attempt=0

    echo "Waiting for production to deploy commit $short_commit..."

    while [ $attempt -lt $max_attempts ]; do
        attempt=$(($attempt + 1))
        
        deployed=$(curl -sf "$health_url" 2>/dev/null | \
            python3 -c "import sys,json; d=json.load(sys.stdin); \
                        print(d.get('build',{}).get('commit','unknown'))" 2>/dev/null || echo "error")
        
        if [ "$deployed" = "$expected_commit" ] || \
           [ "$deployed" = "$short_commit" ] || \
           [ "${deployed:0:7}" = "$short_commit" ]; then
            echo "✓ VERIFIED! Production is running commit $short_commit"
            return 0
        fi
        
        echo "  Attempt $attempt/$max_attempts — deployed: ${deployed:0:7}, waiting..."
        sleep 30
    done

    echo "✗ Timeout: production did not deploy commit $short_commit within 15 minutes"
    return 1
}

# Usage after pushing:
PUSHED_COMMIT=$(git rev-parse HEAD)
verify_deploy "https://yourapp.com/api/health" "$PUSHED_COMMIT"

How to Build One (AI Guide)

The script should be placed at scripts/BobTheBuilder.sh in the project root. Here is the skeleton to start from — adapt the check commands, deploy commands, and menu options to match the project's tech stack:

scripts/BobTheBuilder.sh — Skeleton

bash
#!/bin/bash
#
# Project Name - BobTheBuilder
#
# Usage: ./scripts/BobTheBuilder.sh
#
# Interactive menu for development commands.
# Auto-runs the default check after 10 seconds if no selection.

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
NC='\033[0m'

# Error tracking
ERRORS=()
HAS_ERRORS=false

log_error() {
    ERRORS+=("$1")
    HAS_ERRORS=true
    echo -e "${RED}  ✗ ERROR: $1${NC}"
}

get_timestamp() {
    TZ='America/Chicago' date '+%Y-%m-%d %I:%M:%S %p CST'
}

show_error_summary() {
    if [ "$HAS_ERRORS" = true ]; then
        echo ""
        echo -e "${RED}Errors encountered:${NC}"
        for error in "${ERRORS[@]}"; do
            echo -e "${RED}  • $error${NC}"
        done
        echo ""
        echo -e "${YELLOW}════════════════════════════════════════${NC}"
        echo -e "${YELLOW}  Copy and paste these errors to the AI${NC}"
        echo -e "${YELLOW}  assistant to get help fixing them.${NC}"
        echo -e "${YELLOW}════════════════════════════════════════${NC}"
    fi
}

# Navigate to project root
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
PROJECT_ROOT=$(dirname "$SCRIPT_DIR")
cd "$PROJECT_ROOT" || exit 1

# ═══════════════════════════════════════════
# Define your check/build/deploy functions
# ═══════════════════════════════════════════

run_check() {
    ERRORS=()
    HAS_ERRORS=false
    CHECKS_PASSED=true

    echo -e "${CYAN}Running pre-deployment checks...${NC}"

    # STEP 1: Type checking (adapt to your stack)
    echo -e "${YELLOW}[1/N] Type Check...${NC}"
    # npm run type-check / tsc --noEmit / mypy / cargo check

    # STEP 2: Linting (with auto-fix)
    echo -e "${YELLOW}[2/N] Linting...${NC}"
    # Run linter, attempt auto-fix on failure, re-check

    # STEP 3: Format check (with auto-fix)
    echo -e "${YELLOW}[3/N] Format Check...${NC}"
    # Run formatter check, auto-fix on failure, re-check

    # STEP 4: Build
    echo -e "${YELLOW}[4/N] Build...${NC}"
    # npm run build / cargo build / go build

    if [ "$CHECKS_PASSED" = false ]; then
        show_error_summary
        return 1
    fi

    echo -e "${GREEN}All checks passed! Continuing to deploy...${NC}"
    run_deploy
}

run_deploy() {
    echo -e "${GREEN}Deploying...${NC}"
    # docker compose up -d --build / kubectl apply / etc.
}

run_proddeploy() {
    run_check || return 1

    # Commit and push
    git add -A
    git commit -m "Deploy: $(get_timestamp)" || true
    git push origin main

    # Verify the right version is live (not just that the site is up)
    local pushed_commit
    pushed_commit=$(git rev-parse HEAD)
    verify_deploy "https://yourapp.com/api/health" "$pushed_commit"
}

verify_deploy() {
    local health_url="$1"
    local expected_commit="$2"
    local short_commit="${expected_commit:0:7}"
    local max_attempts=30
    local attempt=0

    echo -e "${CYAN}Waiting for production to deploy commit ${short_commit}...${NC}"

    while [ $attempt -lt $max_attempts ]; do
        attempt=$(($attempt + 1))
        deployed=$(curl -sf "$health_url" 2>/dev/null | \
            python3 -c "import sys,json; d=json.load(sys.stdin); \
                        print(d.get('build',{}).get('commit','unknown'))" 2>/dev/null || echo "error")

        if [ "$deployed" = "$expected_commit" ] || \
           [ "$deployed" = "$short_commit" ] || \
           [ "${deployed:0:7}" = "$short_commit" ]; then
            echo -e "${GREEN}  ✓ VERIFIED! Production is running commit ${short_commit}${NC}"
            return 0
        fi

        echo "  Attempt $attempt/$max_attempts — deployed: ${deployed:0:7}, waiting..."
        sleep 30
    done

    log_error "Timeout: production did not deploy commit $short_commit within 15 minutes"
    return 1
}

# ═══════════════════════════════════════════
# Menu
# ═══════════════════════════════════════════

show_menu() {
    clear
    echo ""
    echo -e "${YELLOW}════════════════════════════════════${NC}"
    echo -e "${YELLOW}   Project Name - Bob the Builder${NC}"
    echo -e "${YELLOW}════════════════════════════════════${NC}"
    echo ""
    echo -e "  ${CYAN}1)${NC} Check + Deploy   ${CYAN}(default)${NC}"
    echo -e "  ${GREEN}2)${NC} Quick Deploy     ${CYAN}(skip checks)${NC}"
    echo -e "  ${MAGENTA}3)${NC} Prod Deploy"
    echo -e "  ${BLUE}4)${NC} Setup"
    echo -e "  ${RED}5)${NC} Cancel"
    echo ""
}

show_menu
read -t 10 -n 1 -p "  Your choice: " choice
echo ""

if [ -z "$choice" ]; then
    choice=1
fi

case $choice in
    1) run_check ;;
    2) run_deploy ;;
    3) run_proddeploy ;;
    4) echo "Adapt for first-time setup" ;;
    5) echo "Cancelled." && exit 0 ;;
    *) echo "Invalid choice" && exit 1 ;;
esac

Adaptation Checklist

When building this for a specific project, adapt these elements:

  • Check commands: Replace npm run type-check, npm run lint, etc. with the project's actual commands (e.g., cargo check for Rust, mypy for Python).
  • Deploy method: Docker Compose, Kubernetes, serverless, SSH — adapt the deploy function to whatever the project uses.
  • Production flow: Git push + CI/CD, manual deploy, cloud CLI — adapt the production deploy to match the project's deployment pipeline.
  • Timezone: Change America/Chicago to the team's timezone.
  • Menu options: Add or remove options based on what the project needs. Common additions: database migrations, seed data, log viewing.
  • Health endpoint (required for production): The health endpoint MUST return the git commit hash in its response (e.g. { build: { commit: "abc123..." } }). The commit hash must be injected at Docker build time via build args. The deploy script MUST poll the health endpoint after pushing until the deployed commit matches the pushed commit — not just until the site returns 200. See the “Deploy Verification” section above for the complete implementation.

Core Principles

  1. One script to rule them all. Developers should only need to remember one command. Everything else is a menu option.
  2. Safe defaults. The auto-timeout should pick the safest useful action — typically “check everything and deploy to dev.”
  3. Auto-fix before failing. If a formatter or linter can fix the issue automatically, do it. Only fail if the auto-fix doesn't resolve it.
  4. Accumulate errors. Show all problems at once, not one at a time.
  5. AI-friendly output. Format errors so they can be copy-pasted into an AI assistant for debugging. Include a banner telling the user to do this.
  6. Cascade on success. When checks pass, automatically continue to the next logical step rather than returning to the menu.
← Back to all resources