Add Gitea webhook auto-deploy scripts and config template.
Co-authored-by: Cursor <cursoragent@cursor.com>
这个提交包含在:
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;
|
||||
}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户