$value) { if (substr($name, 0, 5) == 'HTTP_') { $header_name = str_replace( ' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($name, 5)))) ); $headers[$header_name] = $value; } } return $headers; } /** * 验证请求合法性 */ function validateRequest($headers) { // 检查请求方法 if ($_SERVER['REQUEST_METHOD'] !== 'POST') { header('HTTP/1.1 405 Method Not Allowed'); die('Only POST requests allowed'); } // Token验证 $token = $headers['X-Gitee-Token'] ?? ''; if ($token === SECRET_KEY) { return true; } // 签名验证 $timestamp = $headers['X-Gitee-Timestamp'] ?? ''; $signature = $headers['X-Gitee-Signature'] ?? ''; $expected = hash_hmac('sha256', $timestamp . "\n" . SECRET_KEY, SECRET_KEY); if ($signature !== $expected) { header('HTTP/1.1 403 Forbidden'); die('Secret validation failed'); } return true; } /** * 执行命令并记录日志 */ function executeCommand($cmd) { $output = shell_exec($cmd . ' 2>&1'); logMessage("执行命令: $cmd -> 输出: " . trim($output)); return $output; } /** * 检查提交中是否包含目标目录的变化 */ function hasTargetDirChanges($commits) { if (!CHECK_TARGET_CHANGES || empty(TARGET_DIR)) { logMessage("配置为检查所有变化或目标目录为空,继续部署"); return true; } $target_prefix = TARGET_DIR . '/'; foreach ($commits as $commit) { // 检查新增的文件 foreach ($commit['added'] ?? [] as $file) { if (strpos($file, $target_prefix) === 0) { logMessage("检测到目标目录新增文件: $file"); return true; } } // 检查修改的文件 foreach ($commit['modified'] ?? [] as $file) { if (strpos($file, $target_prefix) === 0) { logMessage("检测到目标目录修改文件: $file"); return true; } } // 检查删除的文件 foreach ($commit['removed'] ?? [] as $file) { if (strpos($file, $target_prefix) === 0) { logMessage("检测到目标目录删除文件: $file"); return true; } } } logMessage("未检测到目标目录 '" . TARGET_DIR . "' 的变化,跳过部署"); return false; } /** * 获取需要保护的文件列表(包括保护目录内的所有文件) */ function getProtectedFiles() { $protected_files = ['.', '..']; // 添加保护目录及其内容 foreach (PROTECTED_DIRS as $protected_dir) { $protected_files[] = $protected_dir; // 如果保护目录存在,递归获取目录内的所有文件和子目录 $full_path = WEBSITE_ROOT . '/' . $protected_dir; if (is_dir($full_path)) { $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($full_path, RecursiveDirectoryIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST ); foreach ($iterator as $item) { if ($item->isDir()) { $relative_path = str_replace(WEBSITE_ROOT . '/', '', $item->getPathname()); $protected_files[] = $relative_path; } else { $relative_path = str_replace(WEBSITE_ROOT . '/', '', $item->getPathname()); $protected_files[] = $relative_path; } } } } logMessage("保护文件列表: " . implode(', ', array_slice($protected_files, 0, 10)) . (count($protected_files) > 10 ? '...' : '')); return $protected_files; } /** * 部署主流程 */ function deploy() { $tempDir = WEBSITE_ROOT . '/.temp_deploy'; // 1. 清理并创建临时目录 if (is_dir($tempDir)) { executeCommand("rm -rf $tempDir"); } mkdir($tempDir, 0755, true); // 2. 配置Git并克隆仓库 executeCommand(GIT_PATH . " config --global http.sslVerify false"); executeCommand(GIT_PATH . " config --global url.https://gitee.com/.insteadOf git@gitee.com:"); $cloneCmd = "cd $tempDir && " . GIT_PATH . " clone -b " . BRANCH . " " . GIT_REMOTE . " ."; if (executeCommand($cloneCmd) === null || !is_dir("$tempDir/.git")) { throw new Exception('仓库克隆失败'); } // 3. 验证目标目录 if (TARGET_DIR && !is_dir("$tempDir/" . TARGET_DIR)) { throw new Exception('目标目录不存在: ' . TARGET_DIR); } // 4. 清理网站目录(保护重要文件和目录) $protected_files = getProtectedFiles(); $dir = opendir(WEBSITE_ROOT); while (($file = readdir($dir)) !== false) { if (!in_array($file, $protected_files)) { $path = WEBSITE_ROOT . '/' . $file; if (is_dir($path)) { executeCommand("rm -rf $path"); logMessage("删除目录: $file"); } else { executeCommand("rm -f $path"); logMessage("删除文件: $file"); } } else { logMessage("保护文件/目录: $file"); } } closedir($dir); // 5. 复制文件并设置权限 $source = TARGET_DIR ? "$tempDir/" . TARGET_DIR : $tempDir; // 使用rsync排除保护目录,确保不会覆盖重要文件 $exclude_args = ''; foreach (PROTECTED_DIRS as $dir) { $exclude_args .= " --exclude='$dir'"; } executeCommand("rsync -av $exclude_args $source/* " . WEBSITE_ROOT . "/ 2>&1"); executeCommand("chown -R www:www " . WEBSITE_ROOT); executeCommand("chmod 755 " . __FILE__); // 6. 清理临时目录 executeCommand("rm -rf $tempDir"); } // ==================== 主执行流程 ==================== logMessage("=== Webhook 请求开始 ==="); try { // 1. 获取并验证请求 $headers = getHeaders(); logMessage("请求头: " . json_encode($headers, JSON_UNESCAPED_UNICODE)); validateRequest($headers); // 2. 处理事件类型 $event = $headers['X-Gitee-Event'] ?? ''; logMessage("事件类型: $event"); // 3. Ping测试处理 if (($headers['X-Gitee-Ping'] ?? '') === 'true') { header('Content-Type: application/json'); echo json_encode([ 'status' => 'success', 'message' => 'Webhook is working', 'time' => date('Y-m-d H:i:s') ]); logMessage("Ping测试响应成功"); exit; } // 4. Push事件处理 if ($event === 'Push Hook') { // 验证目录存在性 if (!is_dir(WEBSITE_ROOT)) { throw new Exception('网站根目录不存在: ' . WEBSITE_ROOT); } // 获取请求体并解析 $payload = file_get_contents('php://input'); $data = json_decode($payload, true); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception('解析JSON数据失败: ' . json_last_error_msg()); } // 检查是否需要部署(根据目标目录变化) $commits = $data['commits'] ?? []; if (!hasTargetDirChanges($commits)) { header('Content-Type: application/json'); echo json_encode([ 'status' => 'skip', 'message' => '目标目录无变化,跳过部署', 'time' => date('Y-m-d H:i:s') ]); logMessage("跳过部署:目标目录无变化"); exit; } // 执行部署 deploy(); // 返回成功响应 $response = [ 'status' => 'success', 'message' => '自动部署完成', 'time' => date('Y-m-d H:i:s') ]; header('Content-Type: application/json'); echo json_encode($response, JSON_UNESCAPED_UNICODE); logMessage("部署完成: " . json_encode($response, JSON_UNESCAPED_UNICODE)); } else { throw new Exception('不支持的事件类型: ' . $event); } } catch (Exception $e) { // 错误处理 logMessage("错误: " . $e->getMessage()); header('HTTP/1.1 500 Internal Server Error'); header('Content-Type: application/json'); echo json_encode([ 'status' => 'error', 'message' => $e->getMessage() ]); } logMessage("=== Webhook 请求结束 ===\n"); ?>