From 0ee8f55abaaadf5a85511a33463fee6596baed75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8F=AF=E4=B9=90=E5=8A=A0=E5=86=B0?= <11869599+addie66@user.noreply.gitee.com> Date: Wed, 23 Sep 2026 17:46:31 +0800 Subject: [PATCH] Add Gitea webhook auto-deploy scripts and config template. Co-authored-by: Cursor --- .gitignore | 3 + compress_images.sh | 2 +- deploy/WebhookDeploy.php | 153 ++++++++++++++++++++++++++++++++++++++ deploy/config.example.php | 28 +++++++ deploy/logs/.gitkeep | 0 public/gitea_webhook.php | 18 +++++ scripts/gitea-deploy.sh | 50 +++++++++++++ 7 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 deploy/WebhookDeploy.php create mode 100644 deploy/config.example.php create mode 100644 deploy/logs/.gitkeep create mode 100644 public/gitea_webhook.php create mode 100644 scripts/gitea-deploy.sh diff --git a/.gitignore b/.gitignore index b209131..de7d84c 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ composer.lock .vscode node_modules .user.ini +/deploy/config.php +/deploy/logs/* +!deploy/logs/.gitkeep diff --git a/compress_images.sh b/compress_images.sh index 03f38e9..6e225f5 100644 --- a/compress_images.sh +++ b/compress_images.sh @@ -1,6 +1,6 @@ #!/bin/bash -UPLOADS_DIR="/www/wwwroot/chufang.gynhc.com/chufang.gynhc.com/public/uploads" +UPLOADS_DIR="/www/wwwroot/chufang.gynhc.com/public/uploads" LOG_FILE="/tmp/image_compress.log" TOTAL_SIZE_BEFORE=0 TOTAL_SIZE_AFTER=0 diff --git a/deploy/WebhookDeploy.php b/deploy/WebhookDeploy.php new file mode 100644 index 0000000..19f048d --- /dev/null +++ b/deploy/WebhookDeploy.php @@ -0,0 +1,153 @@ +config = $config; + $this->logDir = dirname(__DIR__) . '/deploy/logs'; + if (!is_dir($this->logDir)) { + @mkdir($this->logDir, 0755, true); + } + } + + public function handle(): void + { + if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + $this->respond(405, 'Method Not Allowed'); + } + + $this->checkIp(); + + $payload = file_get_contents('php://input'); + if ($payload === false || $payload === '') { + $this->respond(400, 'Empty payload'); + } + + $this->verifySignature($payload); + + $data = json_decode($payload, true); + if (!is_array($data)) { + $this->respond(400, 'Invalid JSON'); + } + + $event = $_SERVER['HTTP_X_GITEA_EVENT'] ?? $_SERVER['HTTP_X_GOGS_EVENT'] ?? ''; + if ($event === 'ping') { + $this->respond(200, 'pong'); + } + + if ($event !== 'push') { + $this->respond(200, 'ignored: ' . $event); + } + + $ref = $data['ref'] ?? ''; + $branch = $this->config['branch'] ?? 'main'; + if ($ref !== 'refs/heads/' . $branch) { + $this->respond(200, 'ignored branch: ' . $ref); + } + + $this->log('Push received for ' . $ref . ', starting deploy'); + $output = $this->runDeploy(); + $this->respond(200, $output); + } + + private function checkIp(): void + { + $allowed = $this->config['allowed_ips'] ?? []; + if ($allowed === [] || $allowed === null) { + return; + } + $ip = $_SERVER['REMOTE_ADDR'] ?? ''; + if (!in_array($ip, $allowed, true)) { + $this->log('Rejected IP: ' . $ip); + $this->respond(403, 'Forbidden'); + } + } + + private function verifySignature($payload) + { + $secret = $this->config['webhook_secret'] ?? ''; + if ($secret === '' || $secret === '请改为随机长字符串') { + $this->respond(500, 'Webhook secret not configured'); + } + + $signature = $_SERVER['HTTP_X_GITEA_SIGNATURE'] + ?? $_SERVER['HTTP_X_GOGS_SIGNATURE'] + ?? ''; + if ($signature === '') { + $this->respond(403, 'Missing signature'); + } + + $expected = hash_hmac('sha256', $payload, $secret); + if (!hash_equals($expected, $signature)) { + $this->log('Invalid signature'); + $this->respond(403, 'Invalid signature'); + } + } + + private function runDeploy() + { + $deployPath = rtrim($this->config['deploy_path'] ?? '', '/'); + if ($deployPath === '' || !is_dir($deployPath)) { + $this->respond(500, 'deploy_path invalid'); + } + + $script = $this->config['deploy_script'] ?? 'scripts/gitea-deploy.sh'; + if ($script[0] !== '/') { + $script = $deployPath . '/' . ltrim($script, '/'); + } + if (!is_file($script)) { + $this->respond(500, 'deploy script missing: ' . $script); + } + + $lock = $this->logDir . '/deploy.lock'; + $lockFp = fopen($lock, 'c'); + if ($lockFp === false || !flock($lockFp, LOCK_EX | LOCK_NB)) { + $this->respond(409, 'Deploy already running'); + } + + $env = 'DEPLOY_PATH=' . escapeshellarg($deployPath); + if (!empty($this->config['run_composer'])) { + $env .= ' RUN_COMPOSER=1'; + } + $branch = preg_replace('/[^a-zA-Z0-9_\\/-]/', '', $this->config['branch'] ?? 'main'); + $env .= ' DEPLOY_BRANCH=' . escapeshellarg($branch); + + $cmd = $env . ' bash ' . escapeshellarg($script) . ' 2>&1'; + $this->log('Exec: ' . $cmd); + + $output = []; + $code = 0; + exec($cmd, $output, $code); + $text = implode("\n", $output); + $this->log("Exit $code\n$text"); + + flock($lockFp, LOCK_UN); + fclose($lockFp); + + if ($code !== 0) { + $this->respond(500, "Deploy failed ($code):\n$text"); + } + + return "Deploy OK\n$text"; + } + + private function log($message) + { + $line = date('Y-m-d H:i:s') . ' ' . $message . "\n"; + @file_put_contents($this->logDir . '/webhook.log', $line, FILE_APPEND | LOCK_EX); + } + + private function respond($code, $body) + { + http_response_code($code); + header('Content-Type: text/plain; charset=utf-8'); + echo $body; + exit; + } +} diff --git a/deploy/config.example.php b/deploy/config.example.php new file mode 100644 index 0000000..eb26a6c --- /dev/null +++ b/deploy/config.example.php @@ -0,0 +1,28 @@ + '请改为随机长字符串', + + // 站点根目录(含 .git 的项目目录,非 public) + 'deploy_path' => '/www/wwwroot/chufang.gynhc.com', + + // 仅在该分支 push 时部署 + 'branch' => 'main', + + // 部署 shell 脚本(相对 deploy_path 或绝对路径) + 'deploy_script' => 'scripts/gitea-deploy.sh', + + // 可选:仅允许这些 IP 调用(Gitea 服务器出口 IP),留空则不校验 + 'allowed_ips' => [], + + // 是否在拉代码后执行 composer install(vendor 未进仓库时建议 true) + 'run_composer' => true, +]; diff --git a/deploy/logs/.gitkeep b/deploy/logs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/public/gitea_webhook.php b/public/gitea_webhook.php new file mode 100644 index 0000000..c957ccb --- /dev/null +++ b/public/gitea_webhook.php @@ -0,0 +1,18 @@ +handle(); diff --git a/scripts/gitea-deploy.sh b/scripts/gitea-deploy.sh new file mode 100644 index 0000000..2b8d6b9 --- /dev/null +++ b/scripts/gitea-deploy.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Gitea Webhook 触发的自动部署(在服务器上由 gitea_webhook.php 调用) +set -euo pipefail + +DEPLOY_PATH="${DEPLOY_PATH:-/www/wwwroot/chufang.gynhc.com}" +DEPLOY_BRANCH="${DEPLOY_BRANCH:-main}" +RUN_COMPOSER="${RUN_COMPOSER:-0}" +LOG_DIR="${DEPLOY_PATH}/deploy/logs" +mkdir -p "$LOG_DIR" +LOG_FILE="${LOG_DIR}/deploy-$(date +%Y%m%d).log" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE" +} + +log "=== Deploy start ===" +log "DEPLOY_PATH=$DEPLOY_PATH branch=$DEPLOY_BRANCH" + +cd "$DEPLOY_PATH" + +if [[ ! -d .git ]]; then + log "ERROR: not a git repository" + exit 1 +fi + +git config --global --add safe.directory "$DEPLOY_PATH" 2>/dev/null || true + +git fetch origin "$DEPLOY_BRANCH" +git reset --hard "origin/${DEPLOY_BRANCH}" +log "Git updated to origin/${DEPLOY_BRANCH} @ $(git rev-parse --short HEAD)" + +if [[ "$RUN_COMPOSER" == "1" ]] && [[ -f composer.json ]]; then + if command -v composer >/dev/null 2>&1; then + log "Running composer install..." + composer install --no-dev --optimize-autoloader --no-interaction 2>&1 | tee -a "$LOG_FILE" + else + log "WARN: composer not found, skip" + fi +fi + +if [[ -d runtime/cache ]]; then + find runtime/cache -type f -delete 2>/dev/null || true + log "Cleared runtime/cache" +fi +if [[ -d runtime/temp ]]; then + find runtime/temp -type f -delete 2>/dev/null || true + log "Cleared runtime/temp" +fi + +log "=== Deploy done ==="