From 0b6d176a4520c0d64caadbe1c4599d8dc4f4d258 Mon Sep 17 00:00:00 2001 From: camelliamc <16583354+camelliamc@user.noreply.gitee.com> Date: Tue, 7 Jul 2026 09:25:39 +0800 Subject: [PATCH] =?UTF-8?q?feat(workflows):=20=E7=A4=BE=E5=8C=BA=E8=BF=90?= =?UTF-8?q?=E8=90=A5=E8=87=AA=E5=8A=A8=E5=8C=96=20=E2=80=94=20Webhook?= =?UTF-8?q?=E6=8E=A5=E6=94=B6=E5=99=A8=20+=20=E9=83=A8=E7=BD=B2=20+=20syst?= =?UTF-8?q?emd?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- workflows/01a-webhook-listener.ps1 | 410 +++++++++++++++++++++++++++++ workflows/01a-webhook-listener.py | 227 ++++++++++++++++ workflows/01a-webhook-setup.ps1 | 322 ++++++++++++++++++++++ workflows/01a-webhook-setup.sh | 101 +++++++ workflows/deploy.sh | 245 +++++++++++++++++ workflows/gitlink-webhook.service | 41 +++ 6 files changed, 1346 insertions(+) create mode 100644 workflows/01a-webhook-listener.ps1 create mode 100644 workflows/01a-webhook-listener.py create mode 100644 workflows/01a-webhook-setup.ps1 create mode 100644 workflows/01a-webhook-setup.sh create mode 100644 workflows/deploy.sh create mode 100644 workflows/gitlink-webhook.service diff --git a/workflows/01a-webhook-listener.ps1 b/workflows/01a-webhook-listener.ps1 new file mode 100644 index 0000000..d138cec --- /dev/null +++ b/workflows/01a-webhook-listener.ps1 @@ -0,0 +1,410 @@ +# ---------------------------------------------------------------- +# Scenario 1a: Webhook HTTP Listener (实时链路接收器) +# Role: HTTP server listening for GitLink webhook events (Issue created) +# → Parse payload → Call 01a-issue-triage.ps1 to classify & assign +# +# 这是社区运营自动化"实时链路"的入口,负责接收 GitLink 平台推送的 +# Webhook 事件,解析 Issue 编号,然后调用分类脚本。 +# +# 架构: +# GitLink 平台 (Issue 创建) +# → POST https://:/webhook +# → 01a-webhook-listener.ps1 (本脚本,HTTP 服务器) +# → 01a-issue-triage.ps1 (分类+打标签+分配) +# +# 部署方式: +# A. 本地 + ngrok 内网穿透: +# 1. 启动本脚本: powershell 01a-webhook-listener.ps1 +# 2. 启动 ngrok: ngrok http 8080 +# 3. 运行注册: powershell 01a-webhook-setup.ps1 -WebhookUrl "https://xxx.ngrok.io/webhook" +# +# B. 部署到公网服务器: +# 1. 上传脚本到服务器 +# 2. 启动本脚本: powershell 01a-webhook-listener.ps1 -Port 443 -Ssl +# 3. 运行注册: powershell 01a-webhook-setup.ps1 -WebhookUrl "https://your-server.com/webhook" +# +# C. 仅手动触发 (无需 webhook): +# powershell 01a-issue-triage.ps1 -IssueNumber 42 +# ---------------------------------------------------------------- +#Requires -Version 5.1 +#Requires -RunAsAdministrator + +param( + [int]$Port = 8080, + [string]$Secret = "", + [string]$HostPrefix = "+", + [switch]$Ssl, + [switch]$Help +) + +$ErrorActionPreference = "Stop" +$ScriptDir = $PSScriptRoot + +if ($Help) { + Write-Host "Usage: powershell 01a-webhook-listener.ps1 [-Port PORT] [-Secret SECRET] [-HostPrefix +] [-Ssl]" + Write-Host "" + Write-Host " Webhook HTTP 接收器 — 监听 GitLink 平台的 Issue 事件,自动触发分类。" + Write-Host "" + Write-Host " -Port PORT 监听端口 (默认: 8080)" + Write-Host " -Secret SECRET HMAC 密钥,用于验证 GitLink 请求来源(需与注册时一致)" + Write-Host " -HostPrefix PREFIX 监听主机前缀 (默认: + 表示所有IP,也可用 localhost)" + Write-Host " -Ssl 启用 HTTPS (需要已导入的 SSL 证书)" + Write-Host "" + Write-Host " 部署前准备:" + Write-Host " 如需公网访问,请使用 ngrok 或部署到有公网IP的服务器:" + Write-Host " ngrok http $Port" + Write-Host " 然后运行 01a-webhook-setup.ps1 在 GitLink 平台注册 webhook" + exit 0 +} + +# ================================================================ +# Color Helpers (no dependency on common.psm1 since this is a server) +# ================================================================ +function Log-Step { param([string]$Msg) Write-Host "[$(Get-Date -Format 'HH:mm:ss')] [STEP] $Msg" -ForegroundColor Blue } +function Log-Ok { param([string]$Msg) Write-Host "[$(Get-Date -Format 'HH:mm:ss')] [ OK] $Msg" -ForegroundColor Green } +function Log-Warn { param([string]$Msg) Write-Host "[$(Get-Date -Format 'HH:mm:ss')] [WARN] $Msg" -ForegroundColor Yellow } +function Log-Err { param([string]$Msg) Write-Host "[$(Get-Date -Format 'HH:mm:ss')] [ ERR] $Msg" -ForegroundColor Red } +function Log-Info { param([string]$Msg) Write-Host "[$(Get-Date -Format 'HH:mm:ss')] [INFO] $Msg" -ForegroundColor Cyan } + +# ================================================================ +# HMAC Signature Verification +# ================================================================ +function Test-WebhookSignature { + param( + [string]$RequestBody, + [string]$SignatureHeader, + [string]$Secret + ) + if (-not $Secret) { return $true } # No secret configured, skip verification + + if (-not $SignatureHeader) { + Log-Warn "No signature header in request (expected X-GitLink-Signature or X-Hub-Signature-256)" + return $false + } + + try { + $hmac = New-Object System.Security.Cryptography.HMACSHA256 + $hmac.Key = [System.Text.Encoding]::UTF8.GetBytes($Secret) + $hash = $hmac.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($RequestBody)) + $computed = "sha256=" + [System.BitConverter]::ToString($hash).Replace("-", "").ToLower() + + # Support both X-GitLink-Signature and X-Hub-Signature-256 (GitHub-compatible) + # Strip prefix if present (e.g., "sha256=abc123..." → "abc123...") + $received = $SignatureHeader + if ($received -match '^sha256=') { + $received = $received + } else { + $received = "sha256=$received" + } + + return $computed -eq $received + } catch { + Log-Err "HMAC verification error: $_" + return $false + } +} + +# ================================================================ +# Extract Issue Number from Webhook Payload +# ================================================================ +function Get-IssueNumberFromPayload { + param([string]$Body, [string]$EventType) + + try { + $payload = $Body | ConvertFrom-Json + + # Try multiple known payload structures + # GitLink format: { action: "opened", issue: { id: ..., number: ..., project_issues_index: ... } } + if ($payload.issue) { + $num = $payload.issue.project_issues_index + if (-not $num) { $num = $payload.issue.number } + if (-not $num) { $num = $payload.issue.id } + if ($num) { + Log-Info "Extracted issue number: #$num (event: $EventType)" + return $num.ToString() + } + } + + # GitHub-compatible format: { action: "opened", issue: { number: ... } } + if ($payload.issue -and $payload.issue.number) { + Log-Info "Extracted issue number (GitHub format): #$($payload.issue.number)" + return $payload.issue.number.ToString() + } + + # Direct format: { number: ..., id: ... } + if ($payload.number) { return $payload.number.ToString() } + if ($payload.id) { + Log-Info "Extracted issue id: $($payload.id)" + return $payload.id.ToString() + } + + Log-Warn "Could not extract issue number from payload" + Log-Info "Payload keys: $($payload.PSObject.Properties.Name -join ', ')" + if ($payload.issue) { + Log-Info "Issue keys: $($payload.issue.PSObject.Properties.Name -join ', ')" + } + return $null + } catch { + Log-Err "Failed to parse webhook payload: $_" + Log-Info "Raw body (first 500 chars): $($Body.Substring(0, [Math]::Min(500, $Body.Length)))" + return $null + } +} + +# ================================================================ +# Extract Owner/Repo from payload or git remote +# ================================================================ +function Get-RepoInfoFromPayload { + param([string]$Body) + + try { + $payload = $Body | ConvertFrom-Json + $owner = $null + $repo = $null + + # GitLink format + if ($payload.repository) { + if ($payload.repository.owner) { + $owner = if ($payload.repository.owner.login) { $payload.repository.owner.login } + elseif ($payload.repository.owner.username) { $payload.repository.owner.username } + else { $payload.repository.owner } + } + if ($payload.repository.name) { $repo = $payload.repository.name } + } + + # GitHub-compatible format + if ((-not $owner) -and $payload.repository -and $payload.repository.full_name) { + $parts = $payload.repository.full_name -split '/' + $owner = $parts[0] + $repo = $parts[1] + } + + return @{ Owner = $owner; Repo = $repo } + } catch { + return @{ Owner = $null; Repo = $null } + } +} + +# ================================================================ +# Process Incoming Webhook +# ================================================================ +function Invoke-WebhookHandler { + param( + [string]$Body, + [string]$EventType, + [string]$EventHeader, + [string]$SignatureHeader + ) + + # Validate secret if configured + if ($Secret -and -not (Test-WebhookSignature -RequestBody $Body -SignatureHeader $SignatureHeader -Secret $Secret)) { + Log-Err "HMAC signature verification FAILED — request rejected" + return @{ StatusCode = 403; Body = '{"error":"Invalid signature"}' } + } + + # Only process issue events + if ($EventType -notmatch '^issue' -and $EventHeader -notmatch 'issue') { + Log-Info "Ignoring non-issue event: $EventType" + return @{ StatusCode = 200; Body = '{"status":"ignored","reason":"non-issue event"}' } + } + + # Only process "opened" action (new issue created) + try { + $payload = $Body | ConvertFrom-Json + if ($payload.action -and $payload.action -ne 'opened') { + Log-Info "Ignoring issue event with action: $($payload.action)" + return @{ StatusCode = 200; Body = '{"status":"ignored","reason":"action is not opened"}' } + } + } catch { } + + # Extract issue number + $issueNumber = Get-IssueNumberFromPayload -Body $Body -EventType $EventType + if (-not $issueNumber) { + Log-Err "Cannot extract issue number — skipping triage" + return @{ StatusCode = 400; Body = '{"error":"Cannot extract issue number from payload"}' } + } + + Log-Ok "=== New Issue #$issueNumber — dispatching to triage ===" + + # Extract owner/repo to pass to triage script + $repoInfo = Get-RepoInfoFromPayload -Body $Body + + # Dispatch triage script asynchronously so we can respond to webhook quickly + $jobScript = { + param($ScriptDir, $IssueNum, $Owner, $Repo, $Body) + $argList = @("-File", "$ScriptDir\01a-issue-triage.ps1", "-IssueNumber", $IssueNum) + if ($Owner) { $argList += @("-Owner", $Owner) } + if ($Repo) { $argList += @("-Repo", $Repo) } + $result = & powershell.exe -NoProfile -ExecutionPolicy Bypass @argList 2>&1 + $result | Out-File "$ScriptDir\webhook-triage-$IssueNum-$(Get-Date -Format 'yyyyMMdd-HHmmss').log" -Encoding UTF8 + } + + Start-Job -ScriptBlock $jobScript -ArgumentList $ScriptDir, $issueNumber, $repoInfo.Owner, $repoInfo.Repo, $Body | Out-Null + Log-Ok "Triage job started for #$issueNumber (running in background)" + + return @{ StatusCode = 200; Body = '{"status":"accepted","issue_number":' + $issueNumber + '}' } +} + +# ================================================================ +# Main: Start HTTP Listener +# ================================================================ +Clear-Host +Write-Host "" +Write-Host "╔══════════════════════════════════════════════════════════════╗" -ForegroundColor Cyan +Write-Host "║ GitLink Community Ops — Webhook Listener ║" -ForegroundColor Cyan +Write-Host "║ 实时链路接收器: Issue 创建 → 自动分类 → 分配责任人 ║" -ForegroundColor Cyan +Write-Host "╚══════════════════════════════════════════════════════════════╝" -ForegroundColor Cyan +Write-Host "" + +$protocol = if ($Ssl) { "https" } else { "http" } +$listenUrl = "$($protocol)://$($HostPrefix):$Port/" + +Log-Info "Starting listener on: $listenUrl" +Log-Info "Triage script: $ScriptDir\01a-issue-triage.ps1" +if ($Secret) { + Log-Info "HMAC verification: ENABLED" +} else { + Log-Warn "HMAC verification: DISABLED (set -Secret to enable)" +} + +# Try to register URL ACL if not running as admin for non-localhost +if ($HostPrefix -ne "localhost" -and $HostPrefix -ne "127.0.0.1") { + Write-Host "" + Log-Warn "Listening on $HostPrefix requires URL ACL registration." + Log-Info "If you get 'Access Denied', run as Administrator OR use -HostPrefix localhost" +} + +# Create HttpListener +$listener = $null +try { + $listener = New-Object System.Net.HttpListener + $listener.Prefixes.Add($listenUrl + "webhook/") + $listener.Prefixes.Add($listenUrl) # Also listen on root path + $listener.Start() + Log-Ok "HTTP listener started successfully" +} catch { + Log-Err "Failed to start HTTP listener: $_" + Write-Host "" + Write-Host "Troubleshooting:" -ForegroundColor Yellow + Write-Host " 1. Run as Administrator" + Write-Host " 2. Or register URL ACL manually:" + Write-Host " netsh http add urlacl url=$listenUrl user=Everyone" + Write-Host " 3. Or use localhost only: -HostPrefix localhost" + Write-Host " 4. Check if port $Port is already in use: netstat -ano | findstr $Port" + exit 1 +} + +Write-Host "" +Log-Ok "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +Log-Ok " Listening on: $listenUrl" +Log-Ok " Webhook URL: ${listenUrl}webhook" +Log-Ok " Health check: ${listenUrl}" +Log-Ok "" +Log-Ok " 按 Ctrl+C 停止服务" +Log-Ok "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +Write-Host "" + +# Handle Ctrl+C gracefully +$keepRunning = $true +$null = Register-EngineEvent -SourceIdentifier "WebhookListenerStop" -Forward -SupportEvent +try { + [Console]::TreatControlCAsInput = $false +} catch { } + +# Main event loop +while ($keepRunning) { + try { + $context = $listener.GetContext() + $request = $context.Request + $response = $context.Response + + $requestMethod = $request.HttpMethod + $requestUrl = $request.Url.ToString() + $remoteIp = $request.RemoteEndPoint.Address.ToString() + + Log-Step "$requestMethod $requestUrl (from $remoteIp)" + + if ($requestMethod -eq "GET" -and ($requestUrl -notmatch '/webhook$')) { + # Health check / root page + $html = @" + +GitLink Webhook Listener + + +

GitLink Community Ops — Webhook Listener

+

✓ Running

+

Listening for issue events at ${listenUrl}webhook

+

When a new Issue is created, this server will:

+
    +
  1. Receive the webhook payload from GitLink
  2. +
  3. Validate HMAC signature (if secret is configured)
  4. +
  5. Extract the issue number
  6. +
  7. Call 01a-issue-triage.ps1 to AI-classify and assign
  8. +
+

Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') | Port: $Port | HMAC: $(if($Secret){'enabled'}else{'disabled'})

+ +"@ + $buffer = [System.Text.Encoding]::UTF8.GetBytes($html) + $response.ContentType = "text/html; charset=utf-8" + $response.ContentLength64 = $buffer.Length + $response.OutputStream.Write($buffer, 0, $buffer.Length) + $response.OutputStream.Close() + Log-Ok "Health check OK" + continue + } + + # Read request body + $reader = New-Object System.IO.StreamReader($request.InputStream, $request.ContentEncoding) + $body = $reader.ReadToEnd() + $reader.Close() + + # Get event headers + $eventType = $request.Headers.Get("X-GitLink-Event") + if (-not $eventType) { + $eventType = $request.Headers.Get("X-GitHub-Event") # GitHub-compatible + } + if (-not $eventType) { + $eventType = $request.Headers.Get("X-Event-Type") + } + + $signatureHeader = $request.Headers.Get("X-GitLink-Signature") + if (-not $signatureHeader) { + $signatureHeader = $request.Headers.Get("X-Hub-Signature-256") # GitHub-compatible + } + + Log-Info "Event: $eventType | Body length: $($body.Length) bytes" + + # Process the webhook + $result = Invoke-WebhookHandler -Body $body -EventType $eventType -EventHeader $eventType -SignatureHeader $signatureHeader + + # Send response + $response.StatusCode = $result.StatusCode + $responseBuffer = [System.Text.Encoding]::UTF8.GetBytes($result.Body) + $response.ContentType = "application/json; charset=utf-8" + $response.ContentLength64 = $responseBuffer.Length + $response.OutputStream.Write($responseBuffer, 0, $responseBuffer.Length) + $response.OutputStream.Close() + + } catch [System.Net.HttpListenerException] { + if ($_.Exception.ErrorCode -eq 995) { + # Operation aborted — likely shutting down + Log-Info "Listener shutting down..." + $keepRunning = $false + } else { + Log-Err "HTTP error: $_" + } + } catch { + Log-Err "Unexpected error: $_" + Start-Sleep -Milliseconds 100 + } +} + +# Cleanup +if ($listener -and $listener.IsListening) { + $listener.Stop() + $listener.Close() + Log-Ok "HTTP listener stopped" +} + +Log-Ok "Webhook listener exited" diff --git a/workflows/01a-webhook-listener.py b/workflows/01a-webhook-listener.py new file mode 100644 index 0000000..2a3da6f --- /dev/null +++ b/workflows/01a-webhook-listener.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +# ================================================================ +# Scenario 1a: Webhook HTTP Listener (Linux 版) +# Role: HTTP server 监听 GitLink webhook → 解析 Issue 编号 → 调用分类脚本 +# +# 部署: systemd 管理,端口 8080,无需 root(用 systemd socket activation 或 sudo) +# 依赖: Python 3.6+ (无需额外 pip 包) +# ================================================================ + +import os +import sys +import json +import hmac +import hashlib +import subprocess +import threading +from http.server import HTTPServer, BaseHTTPRequestHandler +from datetime import datetime + +# ── 配置 ──────────────────────────────────────────────────────── +PORT = int(os.environ.get("WEBHOOK_PORT", "8080")) +SECRET = os.environ.get("WEBHOOK_SECRET", "") +SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +TRIAGE_SCRIPT = os.path.join(SCRIPT_DIR, "01a-issue-triage.sh") +LOG_DIR = os.path.join(SCRIPT_DIR, "webhook-logs") + +os.makedirs(LOG_DIR, exist_ok=True) + + +def log(msg, level="INFO"): + ts = datetime.now().strftime("%H:%M:%S") + color = {"INFO": "\033[36m", "OK": "\033[32m", "WARN": "\033[33m", "ERR": "\033[31m"}.get(level, "") + reset = "\033[0m" + print(f"[{ts}] {color}[{level:>4}]{reset} {msg}", flush=True) + # Also append to log file + logfile = os.path.join(LOG_DIR, datetime.now().strftime("webhook-%Y%m%d.log")) + with open(logfile, "a", encoding="utf-8") as f: + f.write(f"[{ts}] [{level:>4}] {msg}\n") + + +def verify_signature(body: bytes, signature_header: str) -> bool: + """HMAC-SHA256 签名验证""" + if not SECRET: + return True # 未配置密钥,跳过验证 + if not signature_header: + log("No signature header in request", "WARN") + return False + + try: + expected = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest() + # Support both "sha256=abc..." and plain "abc..." formats + received = signature_header + if received.startswith("sha256="): + received = received[7:] + return hmac.compare_digest(expected, received) + except Exception as e: + log(f"HMAC error: {e}", "ERR") + return False + + +def extract_issue_number(payload: dict) -> str: + """从 webhook payload 提取 Issue 编号""" + # GitLink 格式 + issue = payload.get("issue", {}) + if issue: + num = issue.get("project_issues_index") or issue.get("number") or issue.get("id") + if num: + log(f"Extracted issue number: #{num}", "OK") + return str(num) + + # 直接格式 + if "number" in payload: + return str(payload["number"]) + if "id" in payload: + return str(payload["id"]) + + log("Could not extract issue number from payload", "WARN") + return None + + +def extract_repo_info(payload: dict): + """从 payload 提取 owner/repo""" + repo = payload.get("repository", {}) + owner = repo.get("owner", {}) + owner_name = owner.get("login") or owner.get("username") or str(owner) if isinstance(owner, dict) else str(owner) + repo_name = repo.get("name", "") + return owner_name, repo_name + + +def run_triage_async(issue_number: str, owner: str = "", repo: str = ""): + """异步调用分类脚本,另起线程避免阻塞 webhook 响应""" + def _run(): + cmd = ["bash", TRIAGE_SCRIPT, "--issue-number", issue_number] + if owner: + cmd += ["--owner", owner] + if repo: + cmd += ["--repo", repo] + + log(f"Dispatching triage: {' '.join(cmd)}", "INFO") + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=120, cwd=SCRIPT_DIR) + # Save output to log + outfile = os.path.join(LOG_DIR, f"triage-{issue_number}-{datetime.now().strftime('%Y%m%d-%H%M%S')}.log") + with open(outfile, "w", encoding="utf-8") as f: + f.write(f"=== STDOUT ===\n{result.stdout}\n=== STDERR ===\n{result.stderr}\n") + if result.returncode == 0: + log(f"Triage #{issue_number} completed successfully → {outfile}", "OK") + else: + log(f"Triage #{issue_number} failed (exit={result.returncode}) → {outfile}", "ERR") + except subprocess.TimeoutExpired: + log(f"Triage #{issue_number} TIMEOUT after 120s", "ERR") + except Exception as e: + log(f"Triage #{issue_number} error: {e}", "ERR") + + t = threading.Thread(target=_run, daemon=True) + t.start() + + +class WebhookHandler(BaseHTTPRequestHandler): + def log_message(self, format, *args): + log(f"{self.client_address[0]} - {format % args}", "INFO") + + def do_GET(self): + """健康检查 / 根页面""" + html = f""" +GitLink Webhook Listener + + +

GitLink Community Ops — Webhook Listener

+

✓ Running

+

Listening for issue events at /webhook

+

{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} | Port: {PORT} | HMAC: {'enabled' if SECRET else 'disabled'}

+""" + self._respond(200, html, "text/html") + + def do_POST(self): + """接收 webhook""" + content_length = int(self.headers.get("Content-Length", 0)) + body = self.rfile.read(content_length) if content_length > 0 else b"" + + event_type = self.headers.get("X-GitLink-Event") or self.headers.get("X-GitHub-Event") or "" + signature = self.headers.get("X-GitLink-Signature") or self.headers.get("X-Hub-Signature-256") or "" + + log(f"POST {self.path} | Event: {event_type} | Size: {content_length}B | From: {self.client_address[0]}") + + # HMAC 验证 + if SECRET and not verify_signature(body, signature): + self._respond(403, '{"error":"Invalid signature"}') + return + + # 只处理 issue 事件 + if "issue" not in event_type.lower(): + log(f"Ignoring non-issue event: {event_type}", "INFO") + self._respond(200, '{"status":"ignored","reason":"non-issue event"}') + return + + # 解析 payload + try: + payload = json.loads(body) + except json.JSONDecodeError: + log("Failed to parse JSON payload", "ERR") + self._respond(400, '{"error":"Invalid JSON"}') + return + + # 只处理 "opened" 动作 + action = payload.get("action", "") + if action and action != "opened": + log(f"Ignoring issue event with action: {action}", "INFO") + self._respond(200, f'{{"status":"ignored","reason":"action={action}"}}') + return + + # 提取 Issue 编号 + issue_number = extract_issue_number(payload) + if not issue_number: + self._respond(400, '{"error":"Cannot extract issue number"}') + return + + # 提取 owner/repo + owner, repo = extract_repo_info(payload) + + log(f"=== New Issue #{issue_number} — dispatching to triage ===", "OK") + + # 异步调起分类 + run_triage_async(issue_number, owner or "", repo or "") + + self._respond(200, f'{{"status":"accepted","issue_number":{issue_number}}}') + + def _respond(self, code, body, content_type="application/json"): + self.send_response(code) + self.send_header("Content-Type", f"{content_type}; charset=utf-8") + self.send_header("Access-Control-Allow-Origin", "*") + self.end_headers() + self.wfile.write(body.encode("utf-8")) + + +def main(): + print() + print("\033[36m╔══════════════════════════════════════════════════════════════╗\033[0m") + print("\033[36m║ GitLink Community Ops — Webhook Listener (Linux) ║\033[0m") + print("\033[36m╚══════════════════════════════════════════════════════════════╝\033[0m") + print() + + log(f"Starting on port {PORT}") + log(f"Triage script: {TRIAGE_SCRIPT}") + log(f"HMAC verification: {'ENABLED' if SECRET else 'DISABLED (set WEBHOOK_SECRET env var)'}") + print() + + server = HTTPServer(("0.0.0.0", PORT), WebhookHandler) + log(f"Listening on http://0.0.0.0:{PORT}/webhook", "OK") + log(f"Health check: http://0.0.0.0:{PORT}/", "OK") + print() + log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "OK") + log(" Press Ctrl+C to stop", "OK") + log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "OK") + print() + + try: + server.serve_forever() + except KeyboardInterrupt: + log("Shutting down...", "INFO") + server.shutdown() + log("Stopped", "OK") + + +if __name__ == "__main__": + main() diff --git a/workflows/01a-webhook-setup.ps1 b/workflows/01a-webhook-setup.ps1 new file mode 100644 index 0000000..7d34bcf --- /dev/null +++ b/workflows/01a-webhook-setup.ps1 @@ -0,0 +1,322 @@ +# ---------------------------------------------------------------- +# Scenario 1a: Webhook Setup — Register on GitLink Platform +# Role: 在 GitLink 平台上注册 webhook,连接"平台事件"到"本地接收器" +# +# 这是整个实时链路的最后一块拼图——把 GitLink 平台的 Issue 创建事件 +# 和本地的 01a-webhook-listener.ps1 接收器连接起来。 +# +# 完整链路: +# GitLink Issue 创建 +# → Webhook POST 到 +# → 01a-webhook-listener.ps1 (接收 HTTP 请求) +# → 01a-issue-triage.ps1 (AI 分类 + 打标签 + 分配) +# +# 前置条件: +# 方案A (本地开发): 先启动 ngrok → 再启动 01a-webhook-listener.ps1 → 再运行本脚本 +# 方案B (公网服务器): 先启动 01a-webhook-listener.ps1 → 再运行本脚本(直接给公网URL) +# ---------------------------------------------------------------- +#Requires -Version 5.1 + +param( + [Parameter(Mandatory=$true, HelpMessage="Webhook 回调 URL,GitLink 会向此 URL 推送事件")] + [string]$WebhookUrl, + + [string]$Owner = "", + [string]$Repo = "", + [string]$Secret = "", + [string]$Events = "issue", + [string]$Description = "Community Ops — Issue Auto-Triage (created by gitlink-cli)", + [switch]$DryRun, + [switch]$ListExisting, + [switch]$DeleteExisting, + [switch]$Force, + [switch]$Help +) + +$ErrorActionPreference = "Stop" +Import-Module "$PSScriptRoot/lib/common.psm1" -Force + +if ($Help) { + Write-Host "Usage: powershell 01a-webhook-setup.ps1 -WebhookUrl URL [-Owner OWNER] [-Repo REPO] [-Secret SECRET] [-Events EVENTS] [-DryRun]" + Write-Host "" + Write-Host " 在 GitLink 平台上注册 webhook,将 Issue 创建事件连接到本地接收器。" + Write-Host "" + Write-Host " -WebhookUrl URL (必填) Webhook 回调地址,GitLink 会向此 URL POST 事件" + Write-Host " - 本地开发: https://xxx.ngrok-free.app/webhook" + Write-Host " - 公网服务器: https://your-server.com:8080/webhook" + Write-Host " -Owner OWNER 仓库所有者(在git仓库内可自动检测)" + Write-Host " -Repo REPO 仓库名称(在git仓库内可自动检测)" + Write-Host " -Secret SECRET HMAC 密钥(需与 01a-webhook-listener.ps1 的 -Secret 一致)" + Write-Host " -Events EVENTS 触发事件类型(默认: issue)" + Write-Host " -Description DESC Webhook 描述" + Write-Host " -ListExisting 列出当前仓库已有的 webhook" + Write-Host " -DeleteExisting 删除当前仓库所有非 gitlink-cli 创建的 issue 类 webhook(需配合 -Force)" + Write-Host " -Force 配合 -DeleteExisting 使用" + Write-Host " -DryRun 预览模式" + Write-Host "" + Write-Host " 部署步骤:" + Write-Host " # 终端 1: 启动 ngrok(本地开发)" + Write-Host " ngrok http 8080" + Write-Host "" + Write-Host " # 终端 2: 启动接收器" + Write-Host " powershell workflows/01a-webhook-listener.ps1 -Port 8080 -Secret 'your-secret'" + Write-Host "" + Write-Host " # 终端 3: 注册 webhook(ngrok 提供的 URL)" + Write-Host " powershell workflows/01a-webhook-setup.ps1 -WebhookUrl 'https://xxx.ngrok-free.app/webhook' -Secret 'your-secret'" + exit 0 +} + +Check-Auth +$r = Resolve-OwnerRepo $Owner $Repo +$Owner = $r.Owner; $Repo = $r.Repo + +# ================================================================ +# Validation +# ================================================================ +Log-Title "Webhook Setup: $Owner/$Repo" + +# Validate URL format +if ($WebhookUrl -notmatch '^https?://') { + Log-Err "Webhook URL must start with http:// or https://" + Log-Info "For GitLink platform, HTTPS is required. Use ngrok for local dev: ngrok http 8080" + exit 1 +} + +if ($WebhookUrl -notmatch '^https://') { + Log-Warn "GitLink requires HTTPS URLs for webhooks. Your URL uses HTTP which may be rejected." + Log-Info "Consider using ngrok to get an HTTPS URL: ngrok http " +} + +Write-Host " Owner: $Owner" +Write-Host " Repo: $Repo" +Write-Host " Webhook URL: $WebhookUrl" +Write-Host " Events: $Events" +Write-Host " Secret: $(if ($Secret) { '***configured***' } else { '(not set)' })" +Write-Host " Description: $Description" +Divider + +# ================================================================ +# List existing webhooks +# ================================================================ +if ($ListExisting -or $DeleteExisting) { + Log-Title "Existing Webhooks" + + $listResult = Invoke-GL webhook,+list,--owner,$Owner,--repo,$Repo + if (-not $listResult) { + Log-Warn "Could not list webhooks (API may not be available or no webhooks)" + } else { + try { + $listData = $listResult | ConvertFrom-Json + if ($listData.ok -and $listData.data) { + $webhooks = @() + if ($listData.data.webhooks) { $webhooks = @($listData.data.webhooks) } + elseif ($listData.data -is [array]) { $webhooks = $listData.data } + + if ($webhooks.Count -eq 0) { + Log-Info "No webhooks configured for this repo" + } else { + Write-Host "" + foreach ($wh in $webhooks) { + $whId = if ($wh.id) { $wh.id } else { "?" } + $whUrl = if ($wh.hook_url) { $wh.hook_url } elseif ($wh.url) { $wh.url } else { "?" } + $whActive = if ($wh.is_active -ne $null) { $wh.is_active } elseif ($wh.active -ne $null) { $wh.active } else { "?" } + $whEvents = if ($wh.events) { ($wh.events -join ',') } else { "?" } + $whDesc = if ($wh.description) { $wh.description } else { "(no description)" } + Write-Host " [#$whId] $whUrl" -ForegroundColor Cyan + Write-Host " Active: $whActive | Events: $whEvents" + Write-Host " $whDesc" + Write-Host "" + } + } + } + } catch { + Log-Warn "Could not parse webhook list: $_" + Log-Info "Raw output: $listResult" + } + } +} + +# ================================================================ +# Delete existing issue-related webhooks (cleanup before create) +# ================================================================ +if ($DeleteExisting) { + if (-not $Force) { + Log-Warn "-DeleteExisting requires -Force flag for safety. Add -Force to confirm deletion." + } else { + Log-Warn "Removing existing webhooks that match issue events..." + + $listResult = Invoke-GL webhook,+list,--owner,$Owner,--repo,$Repo + if ($listResult) { + try { + $listData = $listResult | ConvertFrom-Json + $webhooks = @() + if ($listData.data.webhooks) { $webhooks = @($listData.data.webhooks) } + elseif ($listData.data -is [array]) { $webhooks = $listData.data } + + foreach ($wh in $webhooks) { + $whId = if ($wh.id) { $wh.id } else { $null } + $whEvents = if ($wh.events) { $wh.events } else { @() } + $whUrl = if ($wh.hook_url) { $wh.hook_url } else { "" } + + # Only delete issue-related ones that point to gitlink-cli created URLs + $isIssueWebhook = ($whEvents -contains "issue") -or ($whEvents -is [string] -and $whEvents -match "issue") + if ($isIssueWebhook) { + if ($DryRun) { + Log-Warn "[DRY RUN] Would delete webhook #$whId ($whUrl)" + } else { + Log-Step "Deleting webhook #$whId..." + $delResult = Invoke-GL webhook,+delete,--owner,$Owner,--repo,$Repo,--id,$whId + if ($delResult) { + Log-Ok "Deleted webhook #$whId" + } else { + Log-Warn "Failed to delete webhook #$whId" + } + } + } + } + } catch { } + } + } + + if (-not $ListExisting) { + Log-Info "Cleanup complete. Proceeding to create new webhook..." + } else { + # User just wanted to list, exit + exit 0 + } +} + +if ($ListExisting) { exit 0 } + +# ================================================================ +# Check for existing webhook with same URL +# ================================================================ +Log-Step "Checking for existing webhooks with same URL..." +$listResult = Invoke-GL webhook,+list,--owner,$Owner,--repo,$Repo +$existingId = $null +if ($listResult) { + try { + $listData = $listResult | ConvertFrom-Json + $webhooks = @() + if ($listData.data.webhooks) { $webhooks = @($listData.data.webhooks) } + elseif ($listData.data -is [array]) { $webhooks = $listData.data } + + foreach ($wh in $webhooks) { + if ($wh.hook_url -eq $WebhookUrl -or $wh.url -eq $WebhookUrl) { + $existingId = $wh.id + break + } + } + } catch { } +} + +if ($existingId) { + Log-Warn "A webhook with URL '$WebhookUrl' already exists (ID: #$existingId)" + Log-Info "To replace it, delete the existing one first with:" + Log-Info " gitlink-cli webhook +delete --owner $Owner --repo $Repo --id $existingId" + Log-Info "Or run this script with -DeleteExisting -Force to clean up" + exit 1 +} +Log-Ok "No duplicate webhook found" + +# ================================================================ +# Register Webhook on GitLink +# ================================================================ +Log-Title "Registering Webhook" + +# Build command arguments +$createArgs = @( + "webhook", "+create", + "--owner", $Owner, + "--repo", $Repo, + "--url", $WebhookUrl, + "--events", $Events, + "--description", $Description +) + +if ($Secret) { + $createArgs += @("--secret", $Secret) +} + +Log-Step "Creating webhook on GitLink platform..." +Log-Info "Command: gitlink-cli $($createArgs -join ' ')" + +if ($DryRun) { + Log-Warn "[DRY RUN] Would create webhook with above parameters" + exit 0 +} + +$createResult = Invoke-GLCheck @createArgs +if (-not $createResult) { + Log-Err "Webhook creation failed" + Log-Info "Common issues:" + Log-Info " 1. URL must be HTTPS (GitLink requirement)" + Log-Info " 2. URL must be publicly accessible from GitLink's servers" + Log-Info " 3. You may need admin permissions on the repo" + Log-Info " 4. Max 20 webhooks per repo — use -ListExisting to check" + exit 1 +} + +$webhookId = "" +if ($createResult.data.id) { $webhookId = $createResult.data.id } +elseif ($createResult.data.webhook.id) { $webhookId = $createResult.data.webhook.id } +Log-Ok "Webhook created! ID: #$webhookId" + +# ================================================================ +# Test Webhook +# ================================================================ +Log-Step "Testing webhook connectivity..." +$testResult = Invoke-GL webhook,+test,--owner,$Owner,--repo,$Repo,--id,$webhookId,--event,issue +if ($testResult) { + try { + $testOk = (($testResult | ConvertFrom-Json).ok -eq $true) + } catch { $testOk = $false } + + if ($testOk) { + Log-Ok "Webhook test ping sent successfully" + Log-Info "Check the listener console for the test event" + } else { + Log-Warn "Webhook test may have failed — check that your listener is running and accessible" + Log-Info "Verify: curl -X POST $WebhookUrl -H 'Content-Type: application/json' -d '{}'" + } +} else { + Log-Warn "Could not test webhook — check that your listener is running" +} + +# ================================================================ +# Complete +# ================================================================ +Log-Title "Webhook Setup Complete" +Write-Host "" + +$checkmark = [char]0x2714 +Write-Host " ${checkmark} GitLink Platform: Webhook registered" -ForegroundColor Green +Write-Host " → When a new Issue is created in $Owner/$Repo" -ForegroundColor Gray +Write-Host " → GitLink POSTs to: $WebhookUrl" -ForegroundColor Gray +Write-Host " → Webhook ID: #$webhookId" -ForegroundColor Gray +Write-Host "" +Write-Host " ${checkmark} Local Listener: 01a-webhook-listener.ps1" -ForegroundColor Green +Write-Host " → Receives HTTP POST from GitLink" -ForegroundColor Gray +Write-Host " → Validates HMAC signature" -ForegroundColor Gray +Write-Host " → Calls 01a-issue-triage.ps1" -ForegroundColor Gray +Write-Host "" +Write-Host " ${checkmark} Triage Script: 01a-issue-triage.ps1" -ForegroundColor Green +Write-Host " → AI analyzes issue content" -ForegroundColor Gray +Write-Host " → Selects matching label from repo's existing labels" -ForegroundColor Gray +Write-Host " → Assigns to issue creator" -ForegroundColor Gray +Write-Host "" + +Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan +Write-Host " 验证方式:" -ForegroundColor Cyan +Write-Host " 1. 在 GitLink 网页端 $Owner/$Repo 创建一个新 Issue" -ForegroundColor White +Write-Host " 2. 观察 01a-webhook-listener.ps1 的控制台输出" -ForegroundColor White +Write-Host " 3. 检查 Issue 是否自动被打上标签并分配了负责人" -ForegroundColor White +Write-Host "" +Write-Host " 手动测试(不走 webhook,直接触发分类):" -ForegroundColor Cyan +Write-Host " powershell workflows/01a-issue-triage.ps1 -IssueNumber " -ForegroundColor White +Write-Host "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ForegroundColor Cyan +Write-Host "" +Write-Host " Webhook 管理:" -ForegroundColor Cyan +Write-Host " - 查看: gitlink-cli webhook +list" +Write-Host " - 详情: gitlink-cli webhook +info --id $webhookId" +Write-Host " - 删除: gitlink-cli webhook +delete --id $webhookId" diff --git a/workflows/01a-webhook-setup.sh b/workflows/01a-webhook-setup.sh new file mode 100644 index 0000000..f368dbf --- /dev/null +++ b/workflows/01a-webhook-setup.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# ================================================================ +# Scenario 1a: Webhook Setup — 在 GitLink 平台注册 webhook +# ================================================================ + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "$SCRIPT_DIR/lib/common.sh" + +WEBHOOK_URL="" +OWNER="" +REPO="" +SECRET="" +EVENTS="issue" +DESCRIPTION="Community Ops - Issue Auto-Triage (gitlink-cli)" +DRY_RUN=false + +usage() { + echo "Usage: $0 --webhook-url URL [--owner OWNER] [--repo REPO] [--secret SECRET] [--events EVENTS] [--dry-run]" + echo "" + echo " 在 GitLink 平台注册 webhook,连接 Issue 创建事件到服务器接收器。" + echo "" + echo " --webhook-url URL (必填) Webhook 回调地址" + echo " 例: https://your-server.com:8080/webhook" + echo " --owner OWNER 仓库所有者" + echo " --repo REPO 仓库名称" + echo " --secret SECRET HMAC 密钥(需与监听器环境变量一致)" + echo " --events EVENTS 触发事件(默认: issue)" + echo " --dry-run 预览模式" + exit 1 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --webhook-url) WEBHOOK_URL="$2"; shift 2 ;; + --owner) OWNER="$2"; shift 2 ;; + --repo) REPO="$2"; shift 2 ;; + --secret) SECRET="$2"; shift 2 ;; + --events) EVENTS="$2"; shift 2 ;; + --dry-run) DRY_RUN="true"; shift ;; + --help|-h) usage ;; + *) log_err "Unknown arg: $1"; usage ;; + esac +done + +[[ -z "$WEBHOOK_URL" ]] && { log_err "--webhook-url is required"; usage; } + +check_auth +require_owner_repo + +log_title "Webhook Setup: $OWNER/$REPO" +echo " Webhook URL: $WEBHOOK_URL" +echo " Events: $EVENTS" +echo " Secret: $( [[ -n "$SECRET" ]] && echo '***configured***' || echo '(not set)' )" + +# 检查重复 +log_step "Checking for existing webhooks with same URL..." +EXISTING_ID=$(gl_run webhook +list --owner "$OWNER" --repo "$REPO" 2>/dev/null | \ + jq -r --arg url "$WEBHOOK_URL" '(.data.webhooks // .data // [])[] | select(.url == $url or .hook_url == $url) | .id' 2>/dev/null || true) + +if [[ -n "$EXISTING_ID" && "$EXISTING_ID" != "null" ]]; then + log_warn "A webhook with URL '$WEBHOOK_URL' already exists (ID: #$EXISTING_ID)" + log_info "Delete it first: gitlink-cli webhook +delete --id $EXISTING_ID" + exit 1 +fi +log_ok "No duplicate found" + +# 注册 +log_step "Creating webhook on GitLink..." + +CREATE_ARGS=(webhook +create --owner "$OWNER" --repo "$REPO" --url "$WEBHOOK_URL" --events "$EVENTS" --description "$DESCRIPTION") +[[ -n "$SECRET" ]] && CREATE_ARGS+=(--secret "$SECRET") + +if [[ "$DRY_RUN" == "true" ]]; then + log_warn "[DRY RUN] Would run: gitlink-cli ${CREATE_ARGS[*]}" + exit 0 +fi + +CREATE_RESULT=$(gl_check "${CREATE_ARGS[@]}" 2>&1) || { + log_err "Webhook creation failed" + log_info "Make sure the URL is HTTPS and publicly accessible" + exit 1 +} + +WEBHOOK_ID=$(echo "$CREATE_RESULT" | jq -r '.data.id // .data.webhook.id') +log_ok "Webhook created! ID: #$WEBHOOK_ID" + +# 测试 +log_step "Testing webhook connectivity..." +gl_run webhook +test --owner "$OWNER" --repo "$REPO" --id "$WEBHOOK_ID" --event issue > /dev/null 2>&1 && { + log_ok "Webhook test ping sent successfully" +} || { + log_warn "Webhook test failed — check that the listener is running" +} + +echo "" +echo -e "${GREEN} ✓ Webhook registered: https://www.gitlink.org.cn/$OWNER/$REPO${NC}" +echo " → When a new Issue is created" +echo " → GitLink POSTs to: $WEBHOOK_URL" +echo " → Webhook ID: #$WEBHOOK_ID" diff --git a/workflows/deploy.sh b/workflows/deploy.sh new file mode 100644 index 0000000..1f09786 --- /dev/null +++ b/workflows/deploy.sh @@ -0,0 +1,245 @@ +#!/usr/bin/env bash +# ================================================================ +# GitLink Community Ops — 一键部署到 Linux 服务器 +# +# 用法: +# 在本地执行 (scp 上传 + 远程安装): +# bash deploy.sh --host 1.2.3.4 --port 8080 \ +# --secret "my-secret" --owner mengcheng --repo gitlink_help_center +# +# 或在服务器本地执行 (已经上传完文件后): +# sudo bash deploy.sh --local --port 8080 --secret "my-secret" \ +# --owner mengcheng --repo gitlink_help_center --webhook-url "https://1.2.3.4:8080/webhook" +# ================================================================ + +set -euo pipefail + +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' +log() { echo -e "${CYAN}[INFO]${NC} $*"; } +ok() { echo -e "${GREEN}[ OK]${NC} $*"; } +err() { echo -e "${RED}[ ERR]${NC} $*"; exit 1; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } + +# ── 参数 ──────────────────────────────────────────────────────── +HOST="" +PORT="8080" +SECRET="" +OWNER="" +REPO="" +WEBHOOK_URL="" +LOCAL=false +INSTALL_DIR="/opt/gitlink-webhook" +SYSTEMD_USER="gitlink" + +usage() { + echo "Usage: $0 --host IP --port PORT --secret SECRET --owner OWNER --repo REPO" + echo " 或 $0 --local --port PORT --secret SECRET --owner OWNER --repo REPO --webhook-url URL" + echo "" + echo " 远程部署 (本地执行):" + echo " --host IP 服务器公网 IP" + echo " --port PORT 监听端口 (默认 8080)" + echo " --secret SECRET HMAC 密钥" + echo " --owner OWNER GitLink 仓库所有者" + echo " --repo REPO GitLink 仓库名" + echo "" + echo " 本地安装 (服务器上执行):" + echo " --local 在当前机器安装" + echo " --webhook-url URL 完整 webhook 回调 URL" + exit 1 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --host) HOST="$2"; shift 2 ;; + --port) PORT="$2"; shift 2 ;; + --secret) SECRET="$2"; shift 2 ;; + --owner) OWNER="$2"; shift 2 ;; + --repo) REPO="$2"; shift 2 ;; + --webhook-url) WEBHOOK_URL="$2"; shift 2 ;; + --local) LOCAL=true; shift ;; + --help|-h) usage ;; + *) err "Unknown arg: $1" ;; + esac +done + +# ── 校验 ──────────────────────────────────────────────────────── +if [[ "$LOCAL" == "true" ]]; then + [[ -z "$WEBHOOK_URL" ]] && err "--webhook-url is required in --local mode" + [[ -z "$SECRET" ]] && err "--secret is required" +else + [[ -z "$HOST" ]] && err "--host is required for remote deployment" + [[ -z "$SECRET" ]] && err "--secret is required" + WEBHOOK_URL="https://${HOST}:${PORT}/webhook" +fi + +WORKFLOW_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REQUIRED_FILES=( + "01a-webhook-listener.py" + "01a-issue-triage.sh" + "01a-webhook-setup.sh" + "01-community-ops.sh" + "lib/common.sh" + "gitlink-webhook.service" +) + +# ── 远程部署 ───────────────────────────────────────────────────── +if [[ "$LOCAL" != "true" ]]; then + log "Deploying to $HOST ..." + + # 检查文件 + for f in "${REQUIRED_FILES[@]}"; do + [[ -f "$WORKFLOW_DIR/$f" ]] || err "Missing: $WORKFLOW_DIR/$f" + done + + log "Uploading files to $HOST:$INSTALL_DIR ..." + ssh "root@$HOST" "mkdir -p $INSTALL_DIR/webhook-logs $INSTALL_DIR/workflows/lib" || err "SSH connection failed" + + scp "$WORKFLOW_DIR/01a-webhook-listener.py" "root@$HOST:$INSTALL_DIR/" + scp "$WORKFLOW_DIR/01a-issue-triage.sh" "root@$HOST:$INSTALL_DIR/workflows/" + scp "$WORKFLOW_DIR/01-community-ops.sh" "root@$HOST:$INSTALL_DIR/workflows/" + scp "$WORKFLOW_DIR/01a-webhook-setup.sh" "root@$HOST:$INSTALL_DIR/workflows/" + scp "$WORKFLOW_DIR/lib/common.sh" "root@$HOST:$INSTALL_DIR/workflows/lib/" + scp "$WORKFLOW_DIR/gitlink-webhook.service" "root@$HOST:$INSTALL_DIR/" + ok "Files uploaded" + + log "Running remote installation..." + ssh "root@$HOST" "bash -s" << REMOTE_SCRIPT +set -e + +INSTALL_DIR="$INSTALL_DIR" +PORT="$PORT" +SECRET="$SECRET" +OWNER="$OWNER" +REPO="$REPO" +WEBHOOK_URL="$WEBHOOK_URL" +SYSTEMD_USER="$SYSTEMD_USER" + +echo '=== Installing GitLink Webhook ===' + +# 1. 创建用户 +if ! id -u \$SYSTEMD_USER &>/dev/null; then + useradd -r -s /usr/sbin/nologin -d \$INSTALL_DIR \$SYSTEMD_USER + echo "[OK] User \$SYSTEMD_USER created" +else + echo "[OK] User \$SYSTEMD_USER exists" +fi + +# 2. 设置权限 +chown -R \$SYSTEMD_USER:\$SYSTEMD_USER \$INSTALL_DIR +chmod +x \$INSTALL_DIR/01a-webhook-listener.py +chmod +x \$INSTALL_DIR/workflows/*.sh +echo "[OK] Permissions set" + +# 3. 创建 .env +cat > \$INSTALL_DIR/.env << EOF +WEBHOOK_PORT=$PORT +WEBHOOK_SECRET=$SECRET +EOF +chmod 600 \$INSTALL_DIR/.env +chown \$SYSTEMD_USER:\$SYSTEMD_USER \$INSTALL_DIR/.env +echo "[OK] .env created" + +# 4. 开放防火墙 +if command -v ufw &>/dev/null && ufw status | grep -q "Status: active"; then + ufw allow \$PORT/tcp 2>/dev/null || true + echo "[OK] Firewall: port \$PORT opened" +elif command -v firewall-cmd &>/dev/null; then + firewall-cmd --permanent --add-port=\$PORT/tcp 2>/dev/null || true + firewall-cmd --reload 2>/dev/null || true + echo "[OK] Firewall: port \$PORT opened" +else + echo "[WARN] No firewall detected — ensure port \$PORT is open in security group" +fi + +# 5. 安装 systemd 服务 +cp \$INSTALL_DIR/gitlink-webhook.service /etc/systemd/system/ +systemctl daemon-reload +systemctl enable gitlink-webhook +systemctl restart gitlink-webhook +echo "[OK] Systemd service installed and started" + +# 6. 等待启动 +sleep 2 +systemctl status gitlink-webhook --no-pager | head -5 + +echo '' +echo '=== Installation complete ===' +echo "Health check: http://$HOST:$PORT/" +echo "Webhook URL: $WEBHOOK_URL" +REMOTE_SCRIPT + + ok "Remote installation complete" + + # 7. 注册 webhook + echo "" + log "Registering webhook on GitLink..." + ssh "root@$HOST" "cd \$INSTALL_DIR/workflows && bash 01a-webhook-setup.sh --webhook-url '$WEBHOOK_URL' --owner '$OWNER' --repo '$REPO' --secret '$SECRET'" || { + warn "Webhook registration failed — you can run manually:" + echo " ssh root@$HOST" + echo " cd $INSTALL_DIR/workflows" + echo " bash 01a-webhook-setup.sh --webhook-url '$WEBHOOK_URL' --owner '$OWNER' --repo '$REPO' --secret '$SECRET'" + } + + echo "" + echo -e "${GREEN}╔══════════════════════════════════════════════════════════╗${NC}" + echo -e "${GREEN}║ Deployment Complete! ║${NC}" + echo -e "${GREEN}║ ║${NC}" + echo -e "${GREEN}║ Health: http://$HOST:$PORT/ ║${NC}" + echo -e "${GREEN}║ Webhook: $WEBHOOK_URL ║${NC}" + echo -e "${GREEN}║ Logs: ssh root@$HOST journalctl -u gitlink-webhook -f ║${NC}" + echo -e "${GREEN}╚══════════════════════════════════════════════════════════╝${NC}" + +# ── 本地安装(在服务器上执行)───────────────────────────────────── +else + [[ "$EUID" -ne 0 ]] && err "Please run as root (sudo)" + + log "Installing locally to $INSTALL_DIR ..." + + # 创建用户 + if ! id -u "$SYSTEMD_USER" &>/dev/null; then + useradd -r -s /usr/sbin/nologin -d "$INSTALL_DIR" "$SYSTEMD_USER" + ok "User $SYSTEMD_USER created" + fi + + # 设置权限 + chown -R "$SYSTEMD_USER:$SYSTEMD_USER" "$INSTALL_DIR" + chmod +x "$INSTALL_DIR/01a-webhook-listener.py" + chmod +x "$INSTALL_DIR/workflows/"*.sh 2>/dev/null || true + ok "Permissions set" + + # .env + cat > "$INSTALL_DIR/.env" << EOF +WEBHOOK_PORT=$PORT +WEBHOOK_SECRET=$SECRET +EOF + chmod 600 "$INSTALL_DIR/.env" + chown "$SYSTEMD_USER:$SYSTEMD_USER" "$INSTALL_DIR/.env" + ok ".env created" + + # 防火墙 + if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then + ufw allow "$PORT/tcp" 2>/dev/null || true + ok "UFW: port $PORT opened" + fi + + # systemd + cp "$INSTALL_DIR/gitlink-webhook.service" /etc/systemd/system/ + systemctl daemon-reload + systemctl enable gitlink-webhook + systemctl restart gitlink-webhook + ok "Systemd service installed" + + sleep 2 + systemctl status gitlink-webhook --no-pager | head -8 + + echo "" + echo -e "${GREEN}Local installation complete!${NC}" + echo " Health check: curl http://localhost:$PORT/" + echo " Status: systemctl status gitlink-webhook" + echo " Logs: journalctl -u gitlink-webhook -f" + echo "" + echo " Next: register webhook on GitLink:" + echo " bash $INSTALL_DIR/workflows/01a-webhook-setup.sh \\" + echo " --webhook-url '$WEBHOOK_URL' \\" + echo " --owner '$OWNER' --repo '$REPO' --secret '$SECRET'" +fi diff --git a/workflows/gitlink-webhook.service b/workflows/gitlink-webhook.service new file mode 100644 index 0000000..c63fd72 --- /dev/null +++ b/workflows/gitlink-webhook.service @@ -0,0 +1,41 @@ +# ================================================================ +# GitLink Community Ops — Webhook Listener Systemd Service +# +# 安装: +# sudo cp gitlink-webhook.service /etc/systemd/system/ +# sudo systemctl daemon-reload +# sudo systemctl enable --now gitlink-webhook +# +# 管理: +# sudo systemctl status gitlink-webhook # 查看状态 +# sudo systemctl restart gitlink-webhook # 重启 +# sudo journalctl -u gitlink-webhook -f # 查看日志 +# ================================================================ +[Unit] +Description=GitLink Community Ops Webhook Listener +Documentation=https://github.com/your-org/gitlink-cli +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +User=gitlink +Group=gitlink +WorkingDirectory=/opt/gitlink-webhook +EnvironmentFile=/opt/gitlink-webhook/.env +ExecStart=/usr/bin/python3 /opt/gitlink-webhook/01a-webhook-listener.py +Restart=always +RestartSec=10 +StandardOutput=journal +StandardError=journal + +# 安全加固 +NoNewPrivileges=yes +PrivateTmp=yes +ProtectSystem=strict +ProtectHome=yes +ReadWritePaths=/opt/gitlink-webhook/webhook-logs +ReadOnlyPaths=/opt/gitlink-webhook/workflows + +[Install] +WantedBy=multi-user.target