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 次删除

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;
}
}