Skip to content

Instantly share code, notes, and snippets.

@jth0
Created October 7, 2025 05:03
Show Gist options
  • Save jth0/bcebb77b4817c472008f83e31d3e7f85 to your computer and use it in GitHub Desktop.
Save jth0/bcebb77b4817c472008f83e31d3e7f85 to your computer and use it in GitHub Desktop.

Revisions

  1. jth0 created this gist Oct 7, 2025.
    762 changes: 762 additions & 0 deletions docker.sh
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,762 @@
    #!/usr/bin/env bash

    # Script for building & pushing a docker image to Google Cloud (GCP) Artifact Registry (GAR)
    # Not a ton of "special sauce" here, and you may be able to tell by the emoji that it's been
    # AI-enhanced. Major happy paths tested, not all edge cases have been fully vetted though.
    # So far only found one hallucination and one set of parameters where code was added but the
    # help output and runtime didn't actually reference the var/param or run the code...

    set -euo pipefail

    # Script configuration and defaults
    SCRIPT_NAME="$(basename "$0")"
    SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

    # Default values - these are all overridable with CLI parameters
    DEFAULT_REGION="us-central1" # Let's go, No Coast!
    DEFAULT_PROJECT="MyProjectNameHere"
    DEFAULT_REPO="MyhRepoNameHere"
    DEFAULT_IMAGE="MyImageNameHere"
    DEFAULT_TAG="test" # A novel idea...
    DEFAULT_R_VERSION="4.4.3" # Manually put here, but should get parsed/updated based on base image selected and/or latest found
    DEFAULT_PYTHON_VERSION="3.12.11" # ditto

    # Initialize variables with defaults
    REGION="${DEFAULT_REGION}"
    PROJECT="${DEFAULT_PROJECT}"
    REPO="${DEFAULT_REPO}"
    IMAGE="${DEFAULT_IMAGE}"
    TAG="${DEFAULT_TAG}"
    R_VERSION="${DEFAULT_R_VERSION}"
    PYTHON_VERSION="${DEFAULT_PYTHON_VERSION}"

    # Variables for Dockerfile parsing
    DOCKERFILE_BASE_IMAGE=""
    DOCKERFILE_R_VERSION=""
    DOCKERFILE_PYTHON_VERSION=""
    DOCKERFILE_UPDATED=false

    # Configuration flags
    SKIP_CONFIRMATION=false
    SKIP_PUSH=false
    NO_CACHE=true
    PULL=true
    PLATFORM="linux/amd64"
    AUTO_UPDATE=false

    # Track if versions were explicitly specified
    R_VERSION_SPECIFIED=false
    PYTHON_VERSION_SPECIFIED=false

    # Colors for output
    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' # No Color

    # Logging functions
    log_info() {
    echo -e "${BLUE}ℹ️ $1${NC}"
    }

    log_success() {
    echo -e "${GREEN}$1${NC}"
    }

    log_warning() {
    echo -e "${YELLOW}⚠️ $1${NC}"
    }

    log_error() {
    echo -e "${RED}$1${NC}" >&2
    }

    log_header() {
    echo -e "${CYAN}🐳 $1${NC}"
    }

    log_update() {
    echo -e "${MAGENTA}🔄 $1${NC}"
    }

    # Help function
    show_help() {
    cat << EOF
    ${SCRIPT_NAME} - Build and deploy Docker images to Google Artifact Registry
    USAGE:
    ${SCRIPT_NAME} [OPTIONS]
    OPTIONS:
    -t, --tag TAG Docker image tag (default: ${DEFAULT_TAG})
    -r, --region REGION GCP region (default: ${DEFAULT_REGION})
    -p, --project PROJECT GCP project ID (default: ${DEFAULT_PROJECT})
    -R, --repo REPO Artifact Registry repository (default: ${DEFAULT_REPO})
    -i, --image IMAGE Image name (default: ${DEFAULT_IMAGE})
    --r-version VERSION R version (default: ${DEFAULT_R_VERSION})
    --python-version VERSION Python version (default: ${DEFAULT_PYTHON_VERSION})
    --platform PLATFORM Target platform (default: ${PLATFORM})
    --cache Use Docker cache (default: no-cache)
    --no-pull Don't pull base images
    --skip-push Build only, don't push to registry
    --auto-update Automatically check for and prompt to update base image
    --yes Skip confirmation prompt
    -h, --help Show this help message
    EXAMPLES:
    ${SCRIPT_NAME} --tag v1.2.3
    ${SCRIPT_NAME} --tag latest --yes
    ${SCRIPT_NAME} --tag dev --r-version 4.4.1 --python-version 3.11.10
    ${SCRIPT_NAME} --skip-push --tag local-test --auto-update
    EOF
    }

    # Parse command line arguments
    parse_args() {
    while [[ $# -gt 0 ]]; do
    case $1 in
    -t|--tag)
    TAG="$2"
    shift 2
    ;;
    -r|--region)
    REGION="$2"
    shift 2
    ;;
    -p|--project)
    PROJECT="$2"
    shift 2
    ;;
    -R|--repo)
    REPO="$2"
    shift 2
    ;;
    -i|--image)
    IMAGE="$2"
    shift 2
    ;;
    --r-version)
    R_VERSION="$2"
    R_VERSION_SPECIFIED=true
    shift 2
    ;;
    --python-version)
    PYTHON_VERSION="$2"
    PYTHON_VERSION_SPECIFIED=true
    shift 2
    ;;
    --platform)
    PLATFORM="$2"
    shift 2
    ;;
    --cache)
    NO_CACHE=false
    shift
    ;;
    --no-pull)
    PULL=false
    shift
    ;;
    --skip-push)
    SKIP_PUSH=true
    shift
    ;;
    --auto-update)
    AUTO_UPDATE=true
    shift
    ;;
    --yes)
    SKIP_CONFIRMATION=true
    shift
    ;;
    -h|--help)
    show_help
    exit 0
    ;;
    *)
    log_error "Unknown option: $1"
    echo "Use --help for usage information."
    exit 1
    ;;
    esac
    done
    }

    # Parse Dockerfile to extract base image and versions
    parse_dockerfile() {
    local dockerfile="${SCRIPT_DIR}/Dockerfile"

    if [[ ! -f "$dockerfile" ]]; then
    log_error "Dockerfile not found in ${SCRIPT_DIR}"
    exit 1
    fi

    log_info "Parsing Dockerfile for base image and versions..."

    # Extract base image from FROM instruction (get the active one, not commented)
    DOCKERFILE_BASE_IMAGE=$(grep -E "^FROM " "$dockerfile" | head -1 | awk '{print $2}' | tr -d '\r')

    if [[ -z "$DOCKERFILE_BASE_IMAGE" ]]; then
    log_error "Could not find FROM instruction in Dockerfile"
    exit 1
    fi

    # Parse versions from the base image tag
    # Format: rstudio/workbench-session:ubuntu2204-r4.4.3_4.3.3-py3.12.11_3.11.13
    local image_tag="${DOCKERFILE_BASE_IMAGE##*:}"

    # Extract R versions (primary and secondary)
    # Pattern: r4.4.3_4.3.3 -> we want the first one (4.4.3)
    if [[ "$image_tag" =~ r([0-9]+\.[0-9]+\.[0-9]+)_ ]]; then
    DOCKERFILE_R_VERSION="${BASH_REMATCH[1]}"
    else
    log_warning "Could not parse R version from base image tag: $image_tag"
    DOCKERFILE_R_VERSION="${DEFAULT_R_VERSION}"
    fi

    # Extract Python versions (primary and secondary)
    # Pattern: py3.12.11_3.11.13 -> we want the first one (3.12.11)
    if [[ "$image_tag" =~ py([0-9]+\.[0-9]+\.[0-9]+)_ ]]; then
    DOCKERFILE_PYTHON_VERSION="${BASH_REMATCH[1]}"
    else
    log_warning "Could not parse Python version from base image tag: $image_tag"
    DOCKERFILE_PYTHON_VERSION="${DEFAULT_PYTHON_VERSION}"
    fi

    log_success "Dockerfile parsed successfully"
    echo " • Base Image: ${DOCKERFILE_BASE_IMAGE}"
    echo " • R Version (from tag): ${DOCKERFILE_R_VERSION}"
    echo " • Python Version (from tag): ${DOCKERFILE_PYTHON_VERSION}"
    echo
    }

    # Update Dockerfile with new base image
    update_dockerfile_base_image() {
    local new_base_image="$1"
    local dockerfile="${SCRIPT_DIR}/Dockerfile"

    log_update "Updating Dockerfile with new base image: ${new_base_image}"

    # Create backup
    cp "$dockerfile" "${dockerfile}.backup.$(date +%Y%m%d_%H%M%S)"

    # Update the FROM line
    sed -i.tmp "s|^FROM .*|FROM ${new_base_image}|" "$dockerfile"
    rm -f "${dockerfile}.tmp"

    DOCKERFILE_BASE_IMAGE="$new_base_image"
    DOCKERFILE_UPDATED=true

    log_success "Dockerfile updated successfully"
    }

    # Check version specifications and warn if unnecessary
    check_version_warnings() {
    local warnings_shown=false

    if [[ "$R_VERSION_SPECIFIED" = true ]]; then
    if [[ "$R_VERSION" = "$DOCKERFILE_R_VERSION" ]]; then
    log_warning "R version ${R_VERSION} matches the base image version"
    log_info " The --r-version parameter is not needed - the base image already has R ${DOCKERFILE_R_VERSION}"
    warnings_shown=true
    else
    log_info "Custom R version specified: ${R_VERSION} (will override base image's ${DOCKERFILE_R_VERSION})"
    fi
    fi

    if [[ "$PYTHON_VERSION_SPECIFIED" = true ]]; then
    if [[ "$PYTHON_VERSION" = "$DOCKERFILE_PYTHON_VERSION" ]]; then
    log_warning "Python version ${PYTHON_VERSION} matches the base image version"
    log_info " The --python-version parameter is not needed - the base image already has Python ${DOCKERFILE_PYTHON_VERSION}"
    warnings_shown=true
    else
    log_info "Custom Python version specified: ${PYTHON_VERSION} (will override base image's ${DOCKERFILE_PYTHON_VERSION})"
    fi
    fi

    if [[ "$warnings_shown" = true ]]; then
    echo
    log_info "💡 Tip: Remove unnecessary version parameters for faster builds that use base image versions"
    echo
    fi
    }

    # Validate prerequisites
    validate_prerequisites() {
    log_info "Validating prerequisites..."

    # Check if Docker is available
    if ! command -v docker &> /dev/null; then
    log_error "Docker is not installed or not in PATH"
    exit 1
    fi

    # Check if gcloud is available
    if ! command -v gcloud &> /dev/null; then
    log_error "gcloud CLI is not installed or not in PATH"
    exit 1
    fi

    # Check if Dockerfile exists
    if [[ ! -f "${SCRIPT_DIR}/Dockerfile" ]]; then
    log_error "Dockerfile not found in ${SCRIPT_DIR}"
    exit 1
    fi

    # Check Docker daemon
    if ! docker info &> /dev/null; then
    log_error "Docker daemon is not running"
    exit 1
    fi

    # Check for optional tools
    if ! command -v jq &> /dev/null; then
    log_warning "jq not found - base image update checks will be limited"
    fi

    if ! command -v curl &> /dev/null; then
    log_warning "curl not found - base image update checks will be limited"
    fi

    log_success "Prerequisites validated"
    }

    # Build full image name
    build_image_name() {
    FULL_IMAGE="${REGION}-docker.pkg.dev/${PROJECT}/${REPO}/${IMAGE}:${TAG}"
    }

    # Show build summary
    show_summary() {
    log_header "Docker Build Summary"
    echo
    echo "📋 Build Configuration:"
    echo " • Image Name: ${FULL_IMAGE}"
    echo " • Base Image: ${DOCKERFILE_BASE_IMAGE}$([ "$DOCKERFILE_UPDATED" = true ] && echo " (updated)" || echo "")"
    echo " • Platform: ${PLATFORM}"
    echo " • R Version: ${R_VERSION}$([ "$R_VERSION_SPECIFIED" = false ] && echo " (default)" || echo "")"
    echo " • Python Version: ${PYTHON_VERSION}$([ "$PYTHON_VERSION_SPECIFIED" = false ] && echo " (default)" || echo "")"
    echo " • Build Context: ${SCRIPT_DIR}"
    echo " • Use Cache: $([ "$NO_CACHE" = true ] && echo "No" || echo "Yes")"
    echo " • Pull Base: $([ "$PULL" = true ] && echo "Yes" || echo "No")"
    echo
    echo "🚀 Actions to be performed:"
    echo " 1. Configure Docker authentication for Artifact Registry"
    echo " 2. Build Docker image with specified parameters"
    if [[ "$SKIP_PUSH" = false ]]; then
    echo " 3. Push image to Google Artifact Registry"
    else
    echo " 3. Skip pushing to registry (--skip-push specified)"
    fi
    echo
    }

    # Confirm before proceeding
    confirm_action() {
    if [[ "$SKIP_CONFIRMATION" = true ]]; then
    return 0
    fi

    echo -n "Do you want to proceed with the build? [y/N]: "
    read -r response
    case "$response" in
    [yY][eE][sS]|[yY])
    return 0
    ;;
    *)
    log_info "Operation cancelled by user"
    exit 0
    ;;
    esac
    }

    # Configure Docker authentication to GCP Artifact Registry (GAR)
    # If this fails, most likely just need to run `gcloud auth login` or `glcoud auth application-default login` -- not sure which
    configure_docker_auth() {
    log_info "Configuring Docker for Artifact Registry..."
    if ! gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet; then
    log_error "Failed to configure Docker authentication"
    exit 1
    fi
    log_success "Docker authentication configured"
    }

    # Build Docker image
    build_image() {
    log_info "Building image ${FULL_IMAGE}..."

    # Build docker command
    local docker_cmd=(
    "docker" "build"
    "--platform=${PLATFORM}"
    "--provenance=false" # disable attestations / extra images in deployment
    "--build-arg" "R_VERSION=${R_VERSION}" # the Dockerfile refers to this parameter when installing/updating R pkgs
    "--build-arg" "PYTHON_VERSION=${PYTHON_VERSION}" # the Dockerfile doesn't touch this but I left it anyway
    "-t" "${FULL_IMAGE}"
    )

    if [[ "$NO_CACHE" = true ]]; then
    docker_cmd+=("--no-cache")
    fi

    if [[ "$PULL" = true ]]; then
    docker_cmd+=("--pull")
    fi

    docker_cmd+=("${SCRIPT_DIR}")

    # Execute build command
    if ! "${docker_cmd[@]}"; then
    log_error "Docker build failed"
    exit 1
    fi

    log_success "Image built successfully"
    }

    # Push image to registry
    push_image() {
    if [[ "$SKIP_PUSH" = true ]]; then
    log_info "Skipping image push (--skip-push specified)"
    return 0
    fi

    log_info "Pushing image to Artifact Registry..."
    if ! docker push "${FULL_IMAGE}"; then
    log_error "Failed to push image to registry"
    exit 1
    fi
    log_success "Image pushed successfully"
    }

    # Enhanced base image update checking with version parsing
    check_base_image_updates() {
    if [[ -z "$DOCKERFILE_BASE_IMAGE" ]]; then
    log_warning "Could not determine base image from Dockerfile"
    return 0
    fi

    log_info "Current base image: ${DOCKERFILE_BASE_IMAGE}"
    log_info " • Current R version: ${DOCKERFILE_R_VERSION}"
    log_info " • Current Python version: ${DOCKERFILE_PYTHON_VERSION}"
    echo

    # Ask user if they want to check for newer versions
    if [[ "$AUTO_UPDATE" = false && "$SKIP_CONFIRMATION" = false ]]; then
    echo -n "Would you like to check for newer versions of the base image? [y/N]: "
    read -r response
    case "$response" in
    [yY][eE][sS]|[yY])
    check_available_versions
    ;;
    *)
    log_info "Skipping version check - continuing with current base image"
    return 0
    ;;
    esac
    elif [[ "$AUTO_UPDATE" = true ]]; then
    log_info "Auto-update enabled - checking for newer versions..."
    check_available_versions
    fi
    }

    # Function to check and list available versions
    check_available_versions() {
    local image_name tag_part
    if [[ "$DOCKERFILE_BASE_IMAGE" == *":"* ]]; then
    image_name="${DOCKERFILE_BASE_IMAGE%:*}"
    tag_part="${DOCKERFILE_BASE_IMAGE##*:}"
    else
    image_name="$DOCKERFILE_BASE_IMAGE"
    tag_part="latest"
    fi

    log_info "Checking available versions for ${image_name}..."

    # Method 1: Try using Docker Hub API for rstudio images
    if [[ "$image_name" == "rstudio/workbench-session" ]]; then
    check_rstudio_versions "$image_name" "$tag_part"
    else
    # Method 2: Try using skopeo for other registries
    check_versions_with_skopeo "$image_name" "$tag_part"
    fi
    }

    # Check rstudio/workbench-session versions using Docker Hub API
    check_rstudio_versions() {
    local image_name="$1"
    local current_tag="$2"

    log_info "Querying Docker Hub for ${image_name} tags..."

    # Use Docker Hub API to get tags
    local api_url="https://hub.docker.com/v2/repositories/${image_name}/tags/?page_size=100"
    local available_tags

    if command -v curl &> /dev/null && command -v jq &> /dev/null; then
    available_tags=$(curl -s "$api_url" | jq -r '.results[].name' 2>/dev/null)

    if [[ $? -eq 0 && -n "$available_tags" ]]; then
    # Filter for ubuntu2204 tags and extract version info
    local ubuntu_tags=$(echo "$available_tags" | grep "ubuntu2204-r" | sort -V)

    if [[ -n "$ubuntu_tags" ]]; then
    log_success "Found available versions:"
    echo

    local current_found=false
    local newer_versions=()
    local current_r_version current_python_version

    # Parse current versions for comparison
    if [[ "$current_tag" =~ r([0-9]+\.[0-9]+\.[0-9]+)_ ]]; then
    current_r_version="${BASH_REMATCH[1]}"
    fi
    if [[ "$current_tag" =~ py([0-9]+\.[0-9]+\.[0-9]+)_ ]]; then
    current_python_version="${BASH_REMATCH[1]}"
    fi

    echo "Available tags (showing last 10):"
    echo "$ubuntu_tags" | tail -10 | while read -r tag; do
    local tag_r_version tag_python_version

    # Parse versions from tag
    if [[ "$tag" =~ r([0-9]+\.[0-9]+\.[0-9]+)_ ]]; then
    tag_r_version="${BASH_REMATCH[1]}"
    fi
    if [[ "$tag" =~ py([0-9]+\.[0-9]+\.[0-9]+)_ ]]; then
    tag_python_version="${BASH_REMATCH[1]}"
    fi

    # Mark current version and potential newer versions
    if [[ "$tag" == "$current_tag" ]]; then
    echo " ${tag} (CURRENT) - R:${tag_r_version}, Python:${tag_python_version}"
    current_found=true
    else
    echo " ${tag} - R:${tag_r_version}, Python:${tag_python_version}"

    # Simple version comparison (newer if R or Python version is higher)
    if [[ -n "$tag_r_version" && -n "$current_r_version" ]] &&
    version_greater_than "$tag_r_version" "$current_r_version"; then
    newer_versions+=("$tag")
    elif [[ -n "$tag_python_version" && -n "$current_python_version" ]] &&
    version_greater_than "$tag_python_version" "$current_python_version"; then
    newer_versions+=("$tag")
    fi
    fi
    done

    echo

    # Show newer versions if found
    if [[ ${#newer_versions[@]} -gt 0 ]]; then
    log_update "Potentially newer versions detected:"
    for version in "${newer_versions[@]}"; do
    echo "$version"
    done
    echo
    prompt_for_update "$image_name"
    else
    log_success "Your current version appears to be up to date!"
    fi

    else
    log_warning "No ubuntu2204 tags found"
    fallback_version_check "$image_name"
    fi
    else
    log_warning "Failed to query Docker Hub API"
    fallback_version_check "$image_name"
    fi
    else
    log_warning "curl or jq not available for API queries"
    fallback_version_check "$image_name"
    fi
    }

    # Check versions using skopeo (for non-Docker Hub registries)
    check_versions_with_skopeo() {
    local image_name="$1"
    local current_tag="$2"

    if command -v skopeo &> /dev/null && command -v jq &> /dev/null; then
    log_info "Using skopeo to check available tags..."

    local available_tags
    available_tags=$(skopeo list-tags "docker://${image_name}" 2>/dev/null | jq -r '.Tags[]' 2>/dev/null)

    if [[ $? -eq 0 && -n "$available_tags" ]]; then
    echo "Available tags:"
    echo "$available_tags" | sort -V | tail -10
    echo
    prompt_for_update "$image_name"
    else
    log_warning "Failed to query registry with skopeo"
    fallback_version_check "$image_name"
    fi
    else
    log_warning "skopeo or jq not available"
    fallback_version_check "$image_name"
    fi
    }

    # Fallback when automatic checking fails
    fallback_version_check() {
    local image_name="$1"

    log_info "💡 To manually check for newer versions:"
    if [[ "$image_name" == "rstudio/workbench-session" ]]; then
    echo " Visit: https://hub.docker.com/r/rstudio/workbench-session/tags"
    else
    echo " Run: docker search ${image_name}"
    echo " Or visit the registry where this image is hosted"
    fi
    echo

    echo -n "Have you checked and found a newer version you'd like to use? [y/N]: "
    read -r response
    case "$response" in
    [yY][eE][sS]|[yY])
    prompt_for_manual_update "$image_name"
    ;;
    *)
    log_info "Continuing with current base image"
    ;;
    esac
    }

    # Prompt user to select or manually enter a new version
    prompt_for_update() {
    local image_name="$1"

    echo -n "Would you like to update to a newer version? [y/N]: "
    read -r response
    case "$response" in
    [yY][eE][sS]|[yY])
    prompt_for_manual_update "$image_name"
    ;;
    *)
    log_info "Continuing with current base image"
    ;;
    esac
    }

    # Manual update prompt
    prompt_for_manual_update() {
    local image_name="$1"

    echo -n "Enter the new tag (e.g., ubuntu2204-r4.4.3_4.3.3-py3.12.11_3.11.13): "
    read -r new_tag

    if [[ -n "$new_tag" ]]; then
    local new_image="${image_name}:${new_tag}"
    log_info "New base image will be: ${new_image}"
    echo -n "Proceed with this update? [y/N]: "
    read -r confirm
    case "$confirm" in
    [yY][eE][sS]|[yY])
    update_dockerfile_base_image "$new_image"
    # Re-parse to get new versions
    parse_dockerfile
    ;;
    *)
    log_info "Update cancelled"
    ;;
    esac
    else
    log_info "No tag provided - continuing with current image"
    fi
    }

    # Simple version comparison function
    version_greater_than() {
    local version1="$1"
    local version2="$2"

    # Convert versions to comparable format
    local v1_major v1_minor v1_patch
    local v2_major v2_minor v2_patch

    IFS='.' read -r v1_major v1_minor v1_patch <<< "$version1"
    IFS='.' read -r v2_major v2_minor v2_patch <<< "$version2"

    # Compare major version
    if [[ "$v1_major" -gt "$v2_major" ]]; then
    return 0
    elif [[ "$v1_major" -lt "$v2_major" ]]; then
    return 1
    fi

    # Compare minor version
    if [[ "$v1_minor" -gt "$v2_minor" ]]; then
    return 0
    elif [[ "$v1_minor" -lt "$v2_minor" ]]; then
    return 1
    fi

    # Compare patch version
    if [[ "$v1_patch" -gt "$v2_patch" ]]; then
    return 0
    else
    return 1
    fi
    }

    # Main execution
    main() {
    log_header "Docker Build & Deploy Script"
    echo

    # Parse command line arguments
    parse_args "$@"

    # Build full image name
    build_image_name

    # Parse Dockerfile for base image and versions
    parse_dockerfile

    # Check for base image updates
    check_base_image_updates

    # Check for version warnings
    check_version_warnings

    # Validate prerequisites
    validate_prerequisites

    # Show summary and confirm
    show_summary
    confirm_action

    echo
    log_header "Starting build process..."

    # Execute build steps
    configure_docker_auth
    build_image
    push_image

    echo
    log_success "Build process completed successfully!"
    log_success "Image ready: ${FULL_IMAGE}"

    if [[ "$DOCKERFILE_UPDATED" = true ]]; then
    log_info "📝 Dockerfile was updated during this build"
    log_info " Backup saved with timestamp"
    fi

    if [[ "$SKIP_PUSH" = false ]]; then
    echo
    log_info "You can now use this image in your deployments:"
    echo " docker pull ${FULL_IMAGE}"
    echo " docker run ${FULL_IMAGE}"
    fi
    }

    # Run main function with all arguments
    main "$@"