deploy-mcp
Version:
Universal deployment tracker for AI assistants
1,184 lines (1,078 loc) • 31.5 kB
JavaScript
import {
MCPHandler,
VercelAdapter
} from "./chunk-BFOEFRRT.js";
// src/utils/github.ts
async function validateRepository(user, repo) {
try {
const response = await fetch(
`https://api.github.com/repos/${user}/${repo}`,
{
headers: {
"User-Agent": "deploy-mcp/1.0",
Accept: "application/vnd.github.v3+json"
}
}
);
if (!response.ok) {
return { exists: false, isPublic: false };
}
const repoData = await response.json();
const isPublic = !repoData.private;
return {
exists: true,
isPublic,
name: repoData.name,
fullName: repoData.full_name
};
} catch {
return { exists: false, isPublic: false };
}
}
function validateParams(user, repo, platform) {
const validName = /^[a-zA-Z0-9._-]+$/;
const validPlatforms = ["vercel", "netlify", "cloudflare-pages"];
return validName.test(user) && validName.test(repo) && validPlatforms.includes(platform);
}
// src/server/badge.ts
var STATUS_COLORS = {
success: "brightgreen",
failed: "red",
building: "yellow",
error: "lightgrey",
unknown: "lightgrey"
};
var PLATFORM_CONFIG = {
vercel: {
label: "Vercel",
logo: "vercel"
},
netlify: {
label: "Netlify",
logo: "netlify"
},
"cloudflare-pages": {
label: "Cloudflare Pages",
logo: "cloudflare"
}
};
function validateParams2(user, repo, platform) {
const validPlatforms = Object.keys(PLATFORM_CONFIG);
return validateParams(user, repo, platform) && user.length <= 50 && repo.length <= 100 && validPlatforms.includes(platform);
}
async function getDeploymentStatus(user, repo, platform, env) {
if (!env.BADGE_KV) {
return "unknown";
}
try {
const key = `${user}/${repo}/${platform}`;
const status = await env.BADGE_KV.get(key, "text");
if (!status) return "unknown";
const validStatuses = [
"success",
"failed",
"building",
"error",
"unknown"
];
return validStatuses.includes(status) ? status : "unknown";
} catch {
return "error";
}
}
async function generateBadgeData(user, repo, platform, env) {
if (!validateParams2(user, repo, platform)) {
throw new Error("Invalid parameters");
}
const repoInfo = await validateRepository(user, repo);
if (!repoInfo.exists || !repoInfo.isPublic) {
return generateErrorBadge(platform, "Repository not found or private");
}
const status = await getDeploymentStatus(user, repo, platform, env);
const config = PLATFORM_CONFIG[platform];
const messages = {
success: "deployed",
failed: "failed",
building: "deploying",
error: "error",
unknown: "unknown"
};
return {
schemaVersion: 1,
label: config.label,
message: messages[status],
color: STATUS_COLORS[status]
};
}
function generateErrorBadge(platform, message) {
const config = PLATFORM_CONFIG[platform];
return {
schemaVersion: 1,
label: config ? config.label : platform,
message,
color: "lightgrey"
};
}
async function updateDeploymentStatus(user, repo, platform, status, env) {
if (!env.BADGE_KV) {
throw new Error("KV storage not configured");
}
const key = `${user}/${repo}/${platform}`;
await env.BADGE_KV.put(key, status, {
expirationTtl: 86400 * 30
// Expire after 30 days
});
}
// src/landing-page.ts
var landingPageHTML = `
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>deploy-mcp - Universal Deployment Tracker</title>
<meta name="description" content="Track deployments across all platforms directly in your AI conversation. No context switching. No dashboard hunting.">
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='6' fill='%23000'/><path d='M16 8v8M12 12l4 4 4-4M10 20h12' stroke='%23ff6b6b' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/></svg>">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;900&display=swap" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--bg: #0a0a0a;
--bg-card: #111111;
--border: #1a1a1a;
--text: #ffffff;
--text-dim: #a0a0a0;
--text-muted: #606060;
--accent: #ff6b6b;
--accent-dim: #ff6b6b20;
--success: #22c55e;
--code-bg: #0d0d0d;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
min-height: 100vh;
}
.container {
max-width: 1100px;
margin: 0 auto;
padding: 0 24px;
}
/* Header */
header {
padding: 24px 0;
border-bottom: 1px solid var(--border);
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 20px;
font-weight: 700;
color: var(--text);
text-decoration: none;
}
.nav-links {
display: flex;
gap: 32px;
align-items: center;
}
.nav-links a {
color: var(--text-dim);
text-decoration: none;
font-size: 14px;
transition: color 0.2s;
}
.nav-links a:hover {
color: var(--text);
}
/* Hero */
.hero {
padding: 120px 0 80px;
text-align: center;
}
.hero h1 {
font-size: clamp(48px, 8vw, 72px);
font-weight: 900;
letter-spacing: -0.04em;
line-height: 1;
margin-bottom: 24px;
}
.hero .subtitle {
font-size: 20px;
color: var(--text-dim);
margin-bottom: 48px;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}
.quick-start {
display: inline-flex;
align-items: center;
gap: 16px;
padding: 20px 32px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
margin-bottom: 16px;
}
.quick-start code {
font-family: 'SF Mono', Monaco, monospace;
font-size: 16px;
color: var(--text);
}
.copy-btn {
padding: 8px 16px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-dim);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.copy-btn:hover {
color: var(--text);
border-color: var(--text-dim);
}
.copy-btn.copied {
background: var(--success);
border-color: var(--success);
color: white;
}
.hero-note {
font-size: 14px;
color: var(--text-muted);
}
/* Platforms Section */
.section {
padding: 80px 0;
border-top: 1px solid var(--border);
}
.section-header {
text-align: center;
margin-bottom: 56px;
}
.section-title {
font-size: 32px;
font-weight: 700;
margin-bottom: 16px;
}
.section-subtitle {
font-size: 16px;
color: var(--text-dim);
}
.platforms {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 24px;
margin-bottom: 32px;
}
.platform {
padding: 24px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
transition: all 0.2s;
}
.platform.active {
border-color: var(--accent);
background: var(--accent-dim);
}
.platform-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.platform-name {
font-size: 18px;
font-weight: 600;
}
.platform-status {
padding: 2px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 500;
background: var(--success);
color: white;
}
.platform-status.soon {
background: var(--text-muted);
}
.platform-desc {
font-size: 14px;
color: var(--text-dim);
line-height: 1.5;
}
.multi-platform-note {
padding: 24px;
background: var(--bg-card);
border: 1px solid var(--accent);
border-radius: 8px;
text-align: center;
}
.multi-platform-note h3 {
font-size: 16px;
margin-bottom: 12px;
color: var(--accent);
}
.multi-platform-note p {
font-size: 14px;
color: var(--text-dim);
margin-bottom: 16px;
}
.config-example {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 16px;
font-family: 'SF Mono', Monaco, monospace;
font-size: 12px;
text-align: left;
overflow-x: auto;
}
/* Features */
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 32px;
}
.feature {
padding: 32px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
}
.feature h3 {
font-size: 20px;
margin-bottom: 12px;
}
.feature p {
font-size: 14px;
color: var(--text-dim);
line-height: 1.6;
}
/* Configuration */
.config-grid {
display: grid;
grid-template-columns: 1fr 2fr;
gap: 48px;
align-items: start;
}
.config-steps {
display: flex;
flex-direction: column;
gap: 24px;
}
.step {
display: flex;
gap: 16px;
}
.step-number {
width: 32px;
height: 32px;
border-radius: 50%;
border: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 600;
flex-shrink: 0;
}
.step-content h4 {
font-size: 16px;
margin-bottom: 4px;
}
.step-content p {
font-size: 14px;
color: var(--text-dim);
}
.config-code {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 8px;
padding: 24px;
position: relative;
}
.config-code pre {
font-family: 'SF Mono', Monaco, monospace;
font-size: 13px;
line-height: 1.6;
overflow-x: auto;
}
.config-code .copy-btn {
position: absolute;
top: 16px;
right: 16px;
}
/* Tools */
.tools-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
}
.tool {
padding: 20px;
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
}
.tool-name {
font-family: 'SF Mono', Monaco, monospace;
font-size: 13px;
color: var(--accent);
margin-bottom: 8px;
}
.tool-desc {
font-size: 14px;
color: var(--text-dim);
}
/* Badges */
.badge-demo {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 8px;
padding: 32px;
text-align: center;
}
.badge-examples {
display: flex;
justify-content: center;
gap: 24px;
margin: 32px 0;
flex-wrap: wrap;
}
.badge-code {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 16px;
font-family: 'SF Mono', Monaco, monospace;
font-size: 12px;
margin-top: 24px;
}
/* Footer */
footer {
padding: 48px 0;
border-top: 1px solid var(--border);
margin-top: 120px;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.footer-links {
display: flex;
gap: 32px;
}
.footer-links a {
color: var(--text-dim);
text-decoration: none;
font-size: 14px;
transition: color 0.2s;
}
.footer-links a:hover {
color: var(--text);
}
.footer-copy {
font-size: 14px;
color: var(--text-muted);
}
/* Responsive */
@media (max-width: 768px) {
.hero {
padding: 80px 0 60px;
}
.hero h1 {
font-size: 40px;
}
.nav-links {
gap: 16px;
}
.config-grid {
grid-template-columns: 1fr;
gap: 32px;
}
.platforms {
grid-template-columns: 1fr;
}
.features {
grid-template-columns: 1fr;
}
.footer-content {
flex-direction: column;
gap: 24px;
}
.badge-examples {
flex-direction: column;
align-items: center;
}
}
</style>
</head>
<body>
<div class="container">
<!-- Header -->
<header>
<nav>
<a href="/" class="logo">deploy-mcp</a>
<div class="nav-links">
<a href="https://github.com/alexpota/deploy-mcp" target="_blank">GitHub</a>
<a href="https://www.npmjs.com/package/deploy-mcp" target="_blank">npm</a>
<a href="https://github.com/alexpota/deploy-mcp#readme" target="_blank">Docs</a>
</div>
</nav>
</header>
<!-- Hero -->
<section class="hero">
<h1>Universal Deployment Tracker</h1>
<p class="subtitle">
Track deployments across all platforms directly in your AI conversation.<br>
No context switching. No dashboard hunting.
</p>
<div class="quick-start">
<code>npx deploy-mcp</code>
<button class="copy-btn" onclick="copyToClipboard('npx deploy-mcp', event)">Copy</button>
</div>
<p class="hero-note">Works with Claude, Cursor, VS Code, and any MCP-compatible AI assistant</p>
</section>
<!-- Platforms -->
<section class="section">
<div class="section-header">
<h2 class="section-title">Supported Platforms</h2>
<p class="section-subtitle">Monitor all your deployments from a single interface</p>
</div>
<div class="platforms">
<div class="platform active">
<div class="platform-header">
<span class="platform-name">Vercel</span>
<span class="platform-status">Ready</span>
</div>
<p class="platform-desc">
Full deployment tracking with real-time status, logs, and build comparisons.
</p>
</div>
<div class="platform active">
<div class="platform-header">
<span class="platform-name">Netlify</span>
<span class="platform-status">Ready</span>
</div>
<p class="platform-desc">
Complete Netlify support with 15 deployment states and log streaming.
</p>
</div>
<div class="platform active">
<div class="platform-header">
<span class="platform-name">Cloudflare Pages</span>
<span class="platform-status">Ready</span>
</div>
<p class="platform-desc">
Fast edge deployments with free bandwidth and global CDN.
</p>
</div>
<div class="platform">
<div class="platform-header">
<span class="platform-name">GitHub Pages</span>
<span class="platform-status soon">Soon</span>
</div>
<p class="platform-desc">
Direct GitHub integration with Actions-based deployment tracking.
</p>
</div>
</div>
<div class="multi-platform-note">
<h3>\u2713 Use Multiple Platforms Simultaneously</h3>
<p>Configure all your platforms at once. The AI automatically detects which platform to query based on your project.</p>
<div class="config-code" style="margin-top: 16px; background: var(--code-bg); border: 1px solid var(--border); position: relative;">
<button class="copy-btn" onclick="copyMultiPlatform(event)">Copy</button>
<pre style="margin: 0;">{
"env": {
"VERCEL_TOKEN": "your-vercel-token",
"NETLIFY_TOKEN": "your-netlify-token",
"CLOUDFLARE_TOKEN": "accountId:globalApiKey"
}
}</pre>
</div>
</div>
</section>
<!-- Features -->
<section class="section">
<div class="section-header">
<h2 class="section-title">Key Features</h2>
<p class="section-subtitle">Everything you need for complete deployment visibility</p>
</div>
<div class="features">
<div class="feature">
<h3>Real-time Monitoring</h3>
<p>
Watch deployments as they happen with streaming updates.
See state changes, build progress, and errors instantly.
Never refresh a dashboard again.
</p>
</div>
<div class="feature">
<h3>Intelligent Analysis</h3>
<p>
Compare deployments to identify performance regressions.
Get smart error detection with suggested fixes.
Track build times and deployment metrics over time.
</p>
</div>
<div class="feature">
<h3>Zero Context Switching</h3>
<p>
Stay in your AI conversation while checking deployments.
No browser tabs, no dashboard hunting, no interruptions.
Everything happens in your development flow.
</p>
</div>
</div>
</section>
<!-- Configuration -->
<section class="section">
<div class="section-header">
<h2 class="section-title">Simple Configuration</h2>
<p class="section-subtitle">Get started in under 60 seconds</p>
</div>
<div class="config-grid">
<div class="config-steps">
<div class="step">
<div class="step-number">1</div>
<div class="step-content">
<h4>Run the server</h4>
<p>Start with npx - no installation needed</p>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
<h4>Add your tokens</h4>
<p>Get API tokens from your platform dashboards</p>
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
<h4>Configure your AI</h4>
<p>Add the configuration to your AI assistant</p>
</div>
</div>
</div>
<div class="config-code">
<button class="copy-btn" onclick="copyConfig(event)">Copy</button>
<pre>{
"mcpServers": {
"deploy-mcp": {
"command": "npx",
"args": ["-y", "deploy-mcp"],
"env": {
"VERCEL_TOKEN": "your-vercel-token",
"NETLIFY_TOKEN": "your-netlify-token",
"CLOUDFLARE_TOKEN": "accountId:globalApiKey"
}
}
}
}</pre>
</div>
</div>
</section>
<!-- Tools -->
<section class="section">
<div class="section-header">
<h2 class="section-title">Available Tools</h2>
<p class="section-subtitle">Powerful commands at your fingertips</p>
</div>
<div class="tools-grid">
<div class="tool">
<div class="tool-name">check_deployment_status</div>
<div class="tool-desc">Get latest deployment status or view deployment history</div>
</div>
<div class="tool">
<div class="tool-name">watch_deployment</div>
<div class="tool-desc">Stream real-time deployment progress with live updates</div>
</div>
<div class="tool">
<div class="tool-name">compare_deployments</div>
<div class="tool-desc">Compare deployments to track changes and performance</div>
</div>
<div class="tool">
<div class="tool-name">get_deployment_logs</div>
<div class="tool-desc">Fetch and analyze deployment logs with error detection</div>
</div>
<div class="tool">
<div class="tool-name">list_projects</div>
<div class="tool-desc">Discover all your projects and sites across platforms</div>
</div>
</div>
</section>
<!-- Badges -->
<section class="section">
<div class="section-header">
<h2 class="section-title">Deployment Badges</h2>
<p class="section-subtitle">Show live deployment status in your README</p>
</div>
<div class="badge-demo">
<div class="badge-examples">
<img src="https://img.shields.io/badge/vercel-success-22c55e" alt="Vercel Status">
<img src="https://img.shields.io/badge/netlify-building-FCD34D" alt="Netlify Status">
<img src="https://img.shields.io/badge/cloudflare%20pages-ready-22c55e" alt="Cloudflare Pages Status">
</div>
<div class="badge-code">
<code></code>
</div>
<p style="margin-top: 24px; font-size: 14px; color: var(--text-dim);">
Configure webhooks in your platform for real-time updates
</p>
</div>
</section>
</div>
<!-- Footer -->
<footer>
<div class="container">
<div class="footer-content">
<div class="footer-links">
<a href="https://github.com/alexpota/deploy-mcp" target="_blank">GitHub</a>
<a href="https://www.npmjs.com/package/deploy-mcp" target="_blank">npm</a>
<a href="https://github.com/alexpota/deploy-mcp/issues" target="_blank">Issues</a>
<a href="https://github.com/alexpota/deploy-mcp#readme" target="_blank">Documentation</a>
</div>
<div class="footer-copy">
\xA9 2025 deploy-mcp \xB7 Apache 2.0 License
</div>
</div>
</div>
</footer>
<script>
function copyToClipboard(text, event) {
const btn = event.target;
navigator.clipboard.writeText(text).then(() => {
const originalText = btn.textContent;
btn.classList.add('copied');
btn.textContent = 'Copied!';
setTimeout(() => {
btn.classList.remove('copied');
btn.textContent = originalText;
}, 2000);
});
}
function copyConfig(event) {
const config = JSON.stringify({
mcpServers: {
'deploy-mcp': {
command: 'npx',
args: ['-y', 'deploy-mcp'],
env: {
VERCEL_TOKEN: 'your-vercel-token',
NETLIFY_TOKEN: 'your-netlify-token',
CLOUDFLARE_TOKEN: 'accountId:globalApiKey'
}
}
}
}, null, 2);
copyToClipboard(config, event);
}
function copyMultiPlatform(event) {
const config = JSON.stringify({
env: {
VERCEL_TOKEN: 'your-vercel-token',
NETLIFY_TOKEN: 'your-netlify-token',
CLOUDFLARE_TOKEN: 'accountId:globalApiKey'
}
}, null, 2);
copyToClipboard(config, event);
}
</script>
</body>
</html>`;
// src/server/webhook.ts
async function validateWebhookSignature(request, platform, env) {
if (platform === "vercel") {
const signature = request.headers.get("x-vercel-signature");
if (!signature) {
return false;
}
const secret = env.VERCEL_WEBHOOK_SECRET;
if (!secret) {
return true;
}
try {
const body = await request.clone().text();
const encoder = new TextEncoder();
const keyData = encoder.encode(secret);
const bodyData = encoder.encode(body);
const cryptoKey = await crypto.subtle.importKey(
"raw",
keyData,
{ name: "HMAC", hash: "SHA-256" },
false,
["sign"]
);
const signatureBuffer = await crypto.subtle.sign(
"HMAC",
cryptoKey,
bodyData
);
const computedSignature = Array.from(new Uint8Array(signatureBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
const expectedSignature = `sha256=${computedSignature}`;
return signature === expectedSignature;
} catch (error) {
console.error("Webhook signature validation error:", error);
return false;
}
}
if (platform === "cloudflare-pages") {
return true;
}
return true;
}
function mapDeploymentStatus(platform, payload) {
switch (platform) {
case "vercel": {
const vercelPayload = payload;
switch (vercelPayload.deployment?.state) {
case "READY":
return "success";
case "BUILDING":
return "building";
case "ERROR":
return "failed";
case "CANCELED":
return "failed";
default:
return "unknown";
}
}
case "netlify": {
const netlifyPayload = payload;
switch (netlifyPayload.state) {
case "ready":
return "success";
case "building":
return "building";
case "error":
return "failed";
default:
return "unknown";
}
}
case "cloudflare-pages": {
const cloudflarePagesPayload = payload;
switch (cloudflarePagesPayload.status) {
case "success":
return "success";
case "failed":
case "canceled":
return "failed";
case "active":
return "building";
case "skipped":
return "unknown";
default:
return "unknown";
}
}
default:
return "unknown";
}
}
async function handleWebhook(request, user, repo, platform, env) {
if (!validateParams(user, repo, platform)) {
return {
success: false,
message: "Invalid parameters",
error: "User, repo, or platform format is invalid"
};
}
const repoValidation = await validateRepository(user, repo);
if (!repoValidation.exists) {
return {
success: false,
message: "Repository not found",
error: `Repository ${user}/${repo} does not exist or is not accessible`
};
}
if (!repoValidation.isPublic) {
return {
success: false,
message: "Private repository",
error: "Badge updates are only supported for public repositories"
};
}
const isValidSignature = await validateWebhookSignature(
request,
platform,
env
);
if (!isValidSignature) {
return {
success: false,
message: "Invalid webhook",
error: "Webhook validation failed"
};
}
let payload;
try {
payload = await request.json();
} catch {
return {
success: false,
message: "Invalid JSON payload",
error: "Could not parse webhook payload"
};
}
const deploymentStatus = mapDeploymentStatus(platform, payload);
try {
await updateDeploymentStatus(user, repo, platform, deploymentStatus, env);
return {
success: true,
message: `Deployment status updated to: ${deploymentStatus}`
};
} catch (error) {
console.error("Failed to update KV storage:", error);
return {
success: false,
message: "Failed to update deployment status",
error: error instanceof Error ? error.message : "Unknown KV error"
};
}
}
// src/worker.ts
function getAdapters(_env) {
const adapters = /* @__PURE__ */ new Map();
const vercelAdapter = new VercelAdapter();
adapters.set("vercel", vercelAdapter);
return adapters;
}
var worker_default = {
async fetch(request, env) {
const url = new URL(request.url);
if (request.method === "POST" && url.pathname === "/mcp") {
try {
const handler = new MCPHandler(getAdapters(env));
const body = await request.json();
const response = await handler.handleRequest(body);
return new Response(JSON.stringify(response), {
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
}
});
} catch (error) {
return new Response(
JSON.stringify({
error: error instanceof Error ? error.message : "Unknown error"
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
}
}
);
}
}
if (request.method === "GET" && url.pathname.startsWith("/badge/")) {
try {
const pathParts = url.pathname.split("/");
if (pathParts.length !== 5) {
const errorBadge = generateErrorBadge("deploy", "Invalid URL format");
return new Response(JSON.stringify(errorBadge), {
status: 400,
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=60",
"Access-Control-Allow-Origin": "*"
}
});
}
const [, , user, repo, platform] = pathParts;
const badgeData = await generateBadgeData(user, repo, platform, env);
return new Response(JSON.stringify(badgeData), {
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=300",
// Cache for 5 minutes
"Access-Control-Allow-Origin": "*"
}
});
} catch (error) {
const errorBadge = generateErrorBadge(
"deploy",
error instanceof Error ? error.message : "Unknown error"
);
return new Response(JSON.stringify(errorBadge), {
status: 500,
headers: {
"Content-Type": "application/json",
"Cache-Control": "public, max-age=60",
"Access-Control-Allow-Origin": "*"
}
});
}
}
if (request.method === "POST" && url.pathname.startsWith("/webhook/")) {
try {
const pathParts = url.pathname.split("/");
if (pathParts.length !== 5) {
return new Response("Invalid webhook URL format", { status: 400 });
}
const [, , user, repo, platform] = pathParts;
const result = await handleWebhook(request, user, repo, platform, env);
return new Response(JSON.stringify(result), {
status: result.success ? 200 : 400,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
}
});
} catch (error) {
console.error("Webhook error:", error);
return new Response(
JSON.stringify({
success: false,
error: error instanceof Error ? error.message : "Unknown error"
}),
{
status: 500,
headers: {
"Content-Type": "application/json",
"Access-Control-Allow-Origin": "*"
}
}
);
}
}
if (request.method === "OPTIONS") {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type"
}
});
}
return new Response(landingPageHTML, {
headers: {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "public, max-age=3600"
// Cache for 1 hour
}
});
}
};
export {
worker_default as default
};