Add Gitea webhook auto-deploy scripts and config template.
Co-authored-by: Cursor <cursoragent@cursor.com>
这个提交包含在:
3
.gitignore
vendored
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
普通文件
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
普通文件
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
普通文件
0
deploy/logs/.gitkeep
普通文件
18
public/gitea_webhook.php
普通文件
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
普通文件
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 ==="
|
||||
在新工单中引用
屏蔽一个用户