Spaces:
Sleeping
Sleeping
File size: 11,988 Bytes
e87efd1 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 |
import gradio as gr
import subprocess
import os
import tempfile
import shutil
from pathlib import Path
import logging
from urllib.parse import urlparse
import re
import time
# Set up logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def format_status_message(message, status="info"):
"""
Format a status message with HTML styling.
Args:
message (str): The message to format
status (str): One of "info", "success", "error"
Returns:
str: HTML formatted message
"""
colors = {
"info": "#2196F3", # Blue
"success": "#4CAF50", # Green
"error": "#F44336" # Red
}
color = colors.get(status, colors["info"])
timestamp = time.strftime("%H:%M:%S")
return f'<div style="color: {color}; margin: 5px 0; font-family: monospace;"><span style="color: #666;">[{timestamp}]</span> {message}</div>'
def extract_github_username(repo_url):
"""
Extract GitHub username from repository URL.
Args:
repo_url (str): GitHub repository URL
Returns:
str: GitHub username
"""
# Handle both HTTPS and SSH URLs
if repo_url.startswith('git@'):
# SSH format: [email protected]:username/repo.git
return repo_url.split(':')[1].split('/')[0]
else:
# HTTPS format: https://github.com/username/repo.git
path = urlparse(repo_url).path
return path.strip('/').split('/')[0]
def validate_image_name(image_name):
"""
Validate that the image name follows Docker tag naming conventions.
Args:
image_name (str): The image name to validate
Returns:
tuple: (bool, str) - (is_valid, error_message)
"""
if not image_name:
return False, "Image name cannot be empty"
# Docker tag naming rules:
# - Can contain lowercase letters, digits, and separators (., -, _)
# - Must start with a letter or digit
# - Must not end with a separator
if not re.match(r'^[a-z0-9][a-z0-9._-]*[a-z0-9]$', image_name):
return False, "Image name must contain only lowercase letters, numbers, and separators (., -, _), and must start and end with a letter or number"
return True, ""
def build_and_push_image(repo_url, registry_token, image_name, username=None, registry="GitHub Container Registry (GHCR)", dockerfile_subdir="."):
temp_dir = None
status_messages = []
original_cwd = os.getcwd() # Save the original working directory
try:
is_valid, error_msg = validate_image_name(image_name)
if not is_valid:
status_messages.append(format_status_message(f"Invalid image name: {error_msg}", "error"))
yield _format_log(status_messages)
return
gh_username = username if username else extract_github_username(repo_url)
if registry == "Docker Hub":
registry_url = "docker.io"
full_image_name = f"{gh_username}/{image_name}"
else:
registry_url = "ghcr.io"
full_image_name = f"ghcr.io/{gh_username}/{image_name}"
steps = [
(f"Starting build process for {full_image_name}...", "info"),
(f"Authenticating with {registry_url}...", "info")
]
for msg, st in steps:
status_messages.append(format_status_message(msg, st))
logger.info(f"β {msg}")
yield _format_log(status_messages)
login_cmd = ["docker", "login", registry_url, "-u", gh_username, "--password-stdin"]
result = subprocess.run(login_cmd, input=registry_token, capture_output=True, text=True)
if result.returncode != 0:
status_messages.append(format_status_message(f"Failed to authenticate with {registry_url}: {result.stderr}", "error"))
logger.error(f"Failed to authenticate with {registry_url}: {result.stderr}")
yield _format_log(status_messages)
return
status_messages.append(format_status_message(f"Successfully authenticated with {registry_url}", "success"))
logger.info(f"β Successfully authenticated with {registry_url}")
yield _format_log(status_messages)
temp_dir = tempfile.mkdtemp()
status_messages.append(format_status_message(f"Created temporary directory: {temp_dir}", "info"))
logger.info(f"β Created temporary directory: {temp_dir}")
yield _format_log(status_messages)
status_messages.append(format_status_message(f"Cloning repository: {repo_url}", "info"))
logger.info(f"β Cloning repository: {repo_url}")
yield _format_log(status_messages)
clone_cmd = ["git", "clone", repo_url, temp_dir]
result = subprocess.run(clone_cmd, capture_output=True, text=True)
if result.returncode != 0:
status_messages.append(format_status_message(f"Failed to clone repository: {result.stderr}", "error"))
logger.error(f"Failed to clone repository: {result.stderr}")
yield _format_log(status_messages)
return
status_messages.append(format_status_message("Repository cloned successfully", "success"))
logger.info("β Repository cloned successfully")
yield _format_log(status_messages)
status_messages.append(format_status_message(f"Changing to repository directory: {temp_dir}", "info"))
logger.info(f"β Changing to repository directory: {temp_dir}")
os.chdir(temp_dir)
yield _format_log(status_messages)
# Check for Dockerfile in the specified subdirectory
dockerfile_path = os.path.join(temp_dir, dockerfile_subdir, "Dockerfile")
if not os.path.exists(dockerfile_path):
status_messages.append(format_status_message(f"No Dockerfile found in the specified subdirectory: {dockerfile_subdir}", "error"))
logger.error(f"No Dockerfile found in the specified subdirectory: {dockerfile_subdir}")
yield _format_log(status_messages)
return
status_messages.append(format_status_message(f"Dockerfile found in {dockerfile_subdir}", "success"))
logger.info(f"β Dockerfile found in {dockerfile_subdir}")
yield _format_log(status_messages)
# Build the Docker image using the specified subdirectory as context
status_messages.append(format_status_message(f"Building Docker image: {full_image_name}", "info"))
logger.info(f"β Building Docker image: {full_image_name}")
yield _format_log(status_messages)
build_cmd = [
"docker", "build", "-t", full_image_name, "-f", dockerfile_path, os.path.join(temp_dir, dockerfile_subdir)
]
result = subprocess.run(build_cmd, capture_output=True, text=True)
if result.returncode != 0:
status_messages.append(format_status_message(f"Failed to build Docker image: {result.stderr}", "error"))
logger.error(f"Failed to build Docker image: {result.stderr}")
yield _format_log(status_messages)
return
status_messages.append(format_status_message("Docker image built successfully", "success"))
logger.info("β Docker image built successfully")
yield _format_log(status_messages)
status_messages.append(format_status_message(f"Pushing Docker image to {registry_url}...", "info"))
logger.info(f"β Pushing Docker image to {registry_url}...")
yield _format_log(status_messages)
push_cmd = ["docker", "push", full_image_name]
result = subprocess.run(push_cmd, capture_output=True, text=True)
if result.returncode != 0:
status_messages.append(format_status_message(f"Failed to push Docker image: {result.stderr}", "error"))
logger.error(f"Failed to push Docker image: {result.stderr}")
yield _format_log(status_messages)
return
status_messages.append(format_status_message(f"Successfully built and pushed image: {full_image_name}", "success"))
logger.info(f"β Successfully built and pushed image: {full_image_name}")
yield _format_log(status_messages)
status_messages.append(format_status_message("Build process completed successfully!", "success"))
yield _format_log(status_messages)
except Exception as e:
error_msg = f"Error: {str(e)}"
logger.error(error_msg)
status_messages.append(format_status_message(error_msg, "error"))
yield _format_log(status_messages)
finally:
try:
os.chdir(original_cwd)
except Exception:
pass
if temp_dir and os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
status_messages.append(format_status_message(f"Cleaned up temporary directory: {temp_dir}", "info"))
yield _format_log(status_messages)
def _format_log(status_messages):
return f'<div style="background-color: #1E1E1E; padding: 15px; border-radius: 5px; border: 1px solid #333; font-family: monospace;">{"".join(status_messages)}</div>'
about_md = """
# Your Personal MCP Remote Builder for GitHub and Docker Registries
This project is a Gradio-based web and MCP server that automates building Docker images from remote GitHub repositories and publishes them to either the GitHub Container Registry (GHCR) or Docker Hub.
## The Problem
Using GitHub for CI/CD for personal projects can be frustrating, as Github's default runners are often too small and fail at even moderately complex Docker image builds due to memory constraints. And at present larger runners are only available for enterprise customers.
## Why Gradio + HuggingFace Spaces
Gradio gives your MCP setup even more freedom β you can kick off builds from anywhere: straight from your IDE or a local LLM using the MCP protocol, programmatically via your codebase or GitHub workflow, or by using the Gradio web UI.
HuggingFace Spaces is perfect for this use case β clone the app there and you can easily pick whatever size or type of machine you want to run it on, and when youβre not using it, it automatically scales right down to zero.
"""
with gr.Blocks() as demo:
with gr.Tab("Builder"):
gr.Markdown("# Your Personal MCP Remote Builder for Github and Docker Registries")
with gr.Column():
repo_url = gr.Textbox(
label="GitHub Repository URL",
value="https://github.com/neonwatty/gradio-mcp-test-build-repo"
)
registry_token = gr.Textbox(
label="Registry Token",
type="password",
value=""
)
image_name = gr.Textbox(
label="Image Name",
value="gradio-mcp-test-build-repo"
)
username = gr.Textbox(label="GitHub Username (optional)")
registry = gr.Dropdown(
label="Registry",
choices=["GitHub Container Registry (GHCR)", "Docker Hub"],
value="GitHub Container Registry (GHCR)"
)
dockerfile_subdir = gr.Textbox(
label="Dockerfile Subdirectory (relative to repo root)",
value=".",
info="Path to the subdirectory containing the Dockerfile. Use '.' for the root."
)
build_button = gr.Button("Build and Push Image")
output = gr.HTML(
label="Build Progress",
value='<div style="background-color: #1E1E1E; padding: 15px; border-radius: 5px; border: 1px solid #333; font-family: monospace;"></div>'
)
build_button.click(
fn=build_and_push_image,
inputs=[repo_url, registry_token, image_name, username, registry, dockerfile_subdir],
outputs=output,
concurrency_limit=1
)
with gr.Tab("About"):
gr.Markdown(about_md)
demo.queue().launch(mcp_server=True)
|