Add Gitea webhook auto-deploy scripts and config template.

Co-authored-by: Cursor <cursoragent@cursor.com>
这个提交包含在:
可乐加冰
2026-09-23 17:46:31 +08:00
父节点 f7d3d7d1bd
当前提交 0ee8f55aba
共有 7 个文件被更改,包括 253 次插入 和 1 次删除

3
.gitignore vendored
查看文件

@@ -16,3 +16,6 @@ composer.lock
.vscode
node_modules
.user.ini
/deploy/config.php
/deploy/logs/*
!deploy/logs/.gitkeep

查看文件

@@ -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

153
deploy/WebhookDeploy.php 普通文件
查看文件

@@ -0,0 +1,153 @@
<?php
class WebhookDeploy
{
/** @var array */
private $config;
/** @var string */
private $logDir;
public function __construct($config)
{
$this->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;
}
}

28
deploy/config.example.php 普通文件
查看文件

@@ -0,0 +1,28 @@
<?php
/**
* 复制为 config.php 并填写实际值(config.php 已加入 .gitignore,勿提交密钥)
*
* Gitea 仓库 → 设置 → Web 钩子 → 添加 Web 钩子:
* - 请求 URL: https://chufang.gynhc.com/gitea_webhook.php
* - 密钥: 与下方 webhook_secret 相同
* - 触发事件: 推送
*/
return [
// Gitea Web 钩子「密钥」
'webhook_secret' => '请改为随机长字符串',
// 站点根目录(含 .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,
];

0
deploy/logs/.gitkeep 普通文件
查看文件

18
public/gitea_webhook.php 普通文件
查看文件

@@ -0,0 +1,18 @@
<?php
/**
* Gitea Web 钩子入口(仅 POST + 签名校验)
* 配置:项目根目录 deploy/config.php(勿提交仓库)
*/
$configFile = dirname(__DIR__) . '/deploy/config.php';
if (!is_file($configFile)) {
http_response_code(500);
header('Content-Type: text/plain; charset=utf-8');
echo 'Missing deploy/config.php — copy deploy/config.example.php';
exit;
}
$config = require $configFile;
require dirname(__DIR__) . '/deploy/WebhookDeploy.php';
(new WebhookDeploy($config))->handle();

50
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 ==="