neonwatty commited on
Commit
e87efd1
Β·
verified Β·
1 Parent(s): 98944bd

Upload 2 files

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