feat: 添加自动化部署脚本、Webhook处理器与TDD测试套件

这个提交包含在:
wikiboss
2026-10-08 16:27:00 +08:00
父节点 90f3b4e21f
当前提交 9b3538c77a
共有 5 个文件被更改,包括 417 次插入 和 0 次删除

42
AGENTS.md 普通文件
查看文件

@@ -0,0 +1,42 @@
\### 站点信息
118.184.172.94
root
密码Bjsslxz1
目录
/www/wwwroot/www.gynhc.com
https://www.gynhc.com/a.php
远程代码仓
https://gitcode.gynhc.com/wikiboss/nahaichuan.git
浅色专业风
背景:#FFFFFF / #F8FAFC
主色:#2563EB 科技蓝
辅助:#06B6D4 青、#7C3AED 紫
文字:#0F172A / #475569
现代中式高端软件公司官网。主菜单:服务、产品、案例、关于我们、联系我们。独立页面:服务、产品、案例。风格:严谨商务、高级感、现代中式、浅色专业风。背景 #FFFFFF 与 #F8FAFC,主色 #2563EB,辅助 #06B6D4 与 #7C3AED,文字 #0F172A 与 #475569。标题宋体,正文黑体或 Inter。12 列网格,1280px,大留白,细线,水墨抽象,窗棂,印章点缀,圆角 4 至 6px。服务:软件开发、APP与小程序定制、智能体开发。产品:商城、收银、进销存、SaaS版本。首页含首屏、服务产品案例入口、流程、评价、CTA。服务页含分类、能力、流程、交付、FAQ。产品页含矩阵、功能、版本、定价、演示。案例页含筛选、挑战方案结果、数据。关于我们含品牌、团队、资质。联系我们含表单、地图、电话。响应式,禁止 emoji、卡通、霓虹、赛博朋克、国潮堆砌。

查看文件

@@ -0,0 +1,94 @@
<?php
namespace app\common\library;
/**
* Git Webhook 核心处理器
*/
class WebhookHandler
{
private $secret;
private $targetBranch;
private $deployExecutor;
public function __construct(string $secret, string $targetBranch = 'master', $deployExecutor = null)
{
$this->secret = $secret;
$this->targetBranch = $targetBranch;
$this->deployExecutor = $deployExecutor;
}
public function handle(array $server, array $get, string $rawBody): array
{
// 1. 验证权限 (支持 Header 传 Token、URL Token 以及 HMAC-SHA256 签名)
if (!$this->verifyAuth($server, $get, $rawBody)) {
return ['code' => 403, 'data' => ['status' => 'error', 'message' => 'Invalid token']];
}
// 2. 验证 HTTP 方式
$method = strtoupper($server['REQUEST_METHOD'] ?? 'GET');
if ($method !== 'POST') {
return ['code' => 405, 'data' => ['status' => 'error', 'message' => 'Method not allowed']];
}
// 3. 响应 Ping / Test 事件
$event = strtolower($server['HTTP_X_GITEA_EVENT'] ?? $server['HTTP_X_GIT_EVENT'] ?? $server['HTTP_X_GITHUB_EVENT'] ?? '');
if ($event === 'ping') {
return ['code' => 200, 'data' => ['status' => 'success', 'message' => 'pong']];
}
// 4. 分支过滤
$payload = json_decode($rawBody, true) ?: [];
$ref = $payload['ref'] ?? '';
$branch = str_replace('refs/heads/', '', $ref);
if ($branch !== $this->targetBranch) {
return [
'code' => 200,
'data' => [
'status' => 'skipped',
'message' => "Branch {$branch} ignored, target is {$this->targetBranch}"
]
];
}
// 5. 触发部署
$output = '';
if (is_callable($this->deployExecutor)) {
$output = call_user_func($this->deployExecutor, $payload);
}
return [
'code' => 200,
'data' => [
'status' => 'success',
'message' => 'Deployment triggered',
'branch' => $branch,
'output' => $output
]
];
}
private function verifyAuth(array $server, array $get, string $rawBody): bool
{
// A. 明文 Token 校验
$token = $server['HTTP_X_GITEA_TOKEN'] ?? $server['HTTP_X_GIT_TOKEN'] ?? $server['HTTP_X_GITLAB_TOKEN'] ?? ($get['token'] ?? '');
if ($token !== '' && hash_equals($this->secret, $token)) {
return true;
}
// B. HMAC-SHA256 签名校验 (Gitea / GitHub)
$signature = $server['HTTP_X_GITEA_SIGNATURE'] ?? $server['HTTP_X_HUB_SIGNATURE_256'] ?? '';
if ($signature !== '') {
if (strpos($signature, 'sha256=') === 0) {
$signature = substr($signature, 7);
}
$expected = hash_hmac('sha256', $rawBody, $this->secret);
if (hash_equals($expected, $signature)) {
return true;
}
}
return false;
}
}

57
deploy.sh 普通文件
查看文件

@@ -0,0 +1,57 @@
#!/bin/bash
# 纳海川官网自动部署脚本
set -e
DEPLOY_DIR="/www/wwwroot/www.gynhc.com"
LOG_FILE="${DEPLOY_DIR}/runtime/deploy.log"
TARGET_BRANCH="${1:-master}"
DATE=$(date '+%Y-%m-%d %H:%M:%S')
echo "[${DATE}] >>> 开始部署分支: ${TARGET_BRANCH}..." >> "${LOG_FILE}"
cd "${DEPLOY_DIR}"
# 验证是否为 git 仓库
if [ ! -d ".git" ]; then
echo "[${DATE}] 错误: ${DEPLOY_DIR} 尚未初始化为 Git 仓库" >> "${LOG_FILE}"
echo "Git repo not initialized"
exit 1
fi
# 备份关键环境配置
if [ -f "application/database.php" ]; then
cp -p application/database.php application/database.php.deploy_bak
fi
# 忽略文件权限变化
git config core.fileMode false
# 拉取最新代码并硬重置
echo "[${DATE}] 执行 git fetch origin ${TARGET_BRANCH}..." >> "${LOG_FILE}"
git fetch origin "${TARGET_BRANCH}" >> "${LOG_FILE}" 2>&1
git reset --hard "origin/${TARGET_BRANCH}" >> "${LOG_FILE}" 2>&1
# 还原配置文件保护
if [ -f "application/database.php.deploy_bak" ]; then
cp -p application/database.php.deploy_bak application/database.php
rm -f application/database.php.deploy_bak
fi
# 保持目录可写
chmod -R 777 runtime 2>/dev/null || true
chmod -R 775 public/uploads 2>/dev/null || true
# 平滑重启 PHP-FPM 刷新 OPcache
if [ -x "/etc/init.d/php-fpm-73" ]; then
if command -v sudo >/dev/null 2>&1; then
sudo -n /etc/init.d/php-fpm-73 reload >> "${LOG_FILE}" 2>&1 || true
else
/etc/init.d/php-fpm-73 reload >> "${LOG_FILE}" 2>&1 || true
fi
fi
COMMIT_INFO=$(git log -1 --pretty=format:"%h - %s (%an, %cd)")
echo "[${DATE}] 当前最新版本: ${COMMIT_INFO}" >> "${LOG_FILE}"
echo "[${DATE}] >>> 部署成功" >> "${LOG_FILE}"
echo "Deploy success: ${COMMIT_INFO}"

49
public/webhook.php 普通文件
查看文件

@@ -0,0 +1,49 @@
<?php
/**
* GitCode Webhook 自动部署入口
*/
// 响应头设置
header('Content-Type: application/json; charset=utf-8');
// Webhook 密钥 Token
$secret = 'nahaichuan_deploy_webhook_2026';
// 项目根目录与部署脚本
$rootDir = dirname(__DIR__);
$deployScript = $rootDir . '/deploy.sh';
$logFile = $rootDir . '/runtime/webhook.log';
// 加载核心处理类
require_once $rootDir . '/application/common/library/WebhookHandler.php';
$deployExecutor = function (array $payload) use ($deployScript, $rootDir) {
$branch = str_replace('refs/heads/', '', $payload['ref'] ?? 'master');
// 用 bash 执行部署脚本并捕获输出
$cmd = "bash " . escapeshellarg($deployScript) . " " . escapeshellarg($branch) . " 2>&1";
$output = shell_exec($cmd);
return trim($output ?? '');
};
$handler = new \app\common\library\WebhookHandler($secret, 'master', $deployExecutor);
$rawBody = file_get_contents('php://input') ?: '';
$result = $handler->handle($_SERVER, $_GET, $rawBody);
// 记录日志
$date = date('Y-m-d H:i:s');
$ip = $_SERVER['REMOTE_ADDR'] ?? 'UNKNOWN';
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$status = $result['data']['status'] ?? 'unknown';
$msg = $result['data']['message'] ?? '';
$out = $result['data']['output'] ?? '';
$logEntry = "[{$date}] [{$ip}] {$method} => Status: {$status}, Msg: {$msg}";
if (!empty($out)) {
$logEntry .= "\nDeploy Output:\n" . $out;
}
$logEntry .= "\n" . str_repeat('-', 50) . "\n";
@file_put_contents($logFile, $logEntry, FILE_APPEND | LOCK_EX);
http_response_code($result['code']);
echo json_encode($result['data'], JSON_UNESCAPED_UNICODE);

175
tests/WebhookTest.php 普通文件
查看文件

@@ -0,0 +1,175 @@
<?php
/**
* Webhook 单元测试 (TDD 模式)
*/
require_once __DIR__ . '/../application/common/library/WebhookHandler.php';
class WebhookTest
{
private $passed = 0;
private $failed = 0;
public function run()
{
echo "=== 开始执行 Webhook 单元测试 (TDD) ===\n";
$this->testInvalidTokenRejection();
$this->testUrlTokenAllowed();
$this->testHeaderTokenAllowed();
$this->testHmacSha256SignatureAllowed();
$this->testHubSignature256Allowed();
$this->testNonPostMethodRejection();
$this->testPingEventSuccess();
$this->testIgnoredBranchSkipped();
$this->testTargetBranchTriggersDeploy();
echo "\n测试结果: {$this->passed} 通过, {$this->failed} 失败\n";
if ($this->failed > 0) {
exit(1);
}
}
private function assert($condition, $message)
{
if ($condition) {
echo " [PASS] {$message}\n";
$this->passed++;
} else {
echo " [FAIL] {$message}\n";
$this->failed++;
}
}
/**
* 测试非法 Token 被拦截
*/
public function testInvalidTokenRejection()
{
$handler = new \app\common\library\WebhookHandler('test_secret_123');
$server = ['REQUEST_METHOD' => 'POST', 'HTTP_X_GITEA_TOKEN' => 'wrong_token'];
$res = $handler->handle($server, [], '{}');
$this->assert($res['code'] === 403, "非法 Token 应返回 HTTP 403");
$this->assert($res['data']['status'] === 'error', "非法 Token 应为 error 状态");
}
/**
* 测试通过 URL 传 Token 允许通过
*/
public function testUrlTokenAllowed()
{
$handler = new \app\common\library\WebhookHandler('test_secret_123');
$server = ['REQUEST_METHOD' => 'POST'];
$get = ['token' => 'test_secret_123'];
$res = $handler->handle($server, $get, json_encode(['ref' => 'refs/heads/master']));
$this->assert($res['code'] === 200, "合法 URL Token 应返回 HTTP 200");
}
/**
* 测试通过 Header 传 Token 允许通过
*/
public function testHeaderTokenAllowed()
{
$handler = new \app\common\library\WebhookHandler('test_secret_123');
$server = ['REQUEST_METHOD' => 'POST', 'HTTP_X_GITEA_TOKEN' => 'test_secret_123'];
$res = $handler->handle($server, [], json_encode(['ref' => 'refs/heads/master']));
$this->assert($res['code'] === 200, "合法 Header Token 应返回 HTTP 200");
}
/**
* 测试通过 Gitea HMAC-SHA256 签名校验
*/
public function testHmacSha256SignatureAllowed()
{
$handler = new \app\common\library\WebhookHandler('test_secret_123');
$body = json_encode(['ref' => 'refs/heads/master']);
$sig = hash_hmac('sha256', $body, 'test_secret_123');
$server = ['REQUEST_METHOD' => 'POST', 'HTTP_X_GITEA_SIGNATURE' => $sig];
$res = $handler->handle($server, [], $body);
$this->assert($res['code'] === 200, "合法 HMAC-SHA256 签名应返回 HTTP 200");
}
/**
* 测试通过 GitHub/Gitea sha256= 前缀签名校验
*/
public function testHubSignature256Allowed()
{
$handler = new \app\common\library\WebhookHandler('test_secret_123');
$body = json_encode(['ref' => 'refs/heads/master']);
$sig = 'sha256=' . hash_hmac('sha256', $body, 'test_secret_123');
$server = ['REQUEST_METHOD' => 'POST', 'HTTP_X_HUB_SIGNATURE_256' => $sig];
$res = $handler->handle($server, [], $body);
$this->assert($res['code'] === 200, "合法 X-Hub-Signature-256 签名应返回 HTTP 200");
}
/**
* 测试非 POST 请求被拒绝
*/
public function testNonPostMethodRejection()
{
$handler = new \app\common\library\WebhookHandler('test_secret_123');
$server = ['REQUEST_METHOD' => 'GET'];
$get = ['token' => 'test_secret_123'];
$res = $handler->handle($server, $get, '');
$this->assert($res['code'] === 405, "非 POST 请求应返回 HTTP 405");
}
/**
* 测试 Gitea Ping / Test 事件直接响应成功
*/
public function testPingEventSuccess()
{
$handler = new \app\common\library\WebhookHandler('test_secret_123');
$server = [
'REQUEST_METHOD' => 'POST',
'HTTP_X_GITEA_EVENT' => 'ping',
'HTTP_X_GITEA_TOKEN' => 'test_secret_123'
];
$res = $handler->handle($server, [], '{}');
$this->assert($res['code'] === 200, "Ping 事件应返回 HTTP 200");
$this->assert($res['data']['message'] === 'pong', "Ping 事件应返回 pong 响应");
}
/**
* 测试非目标分支自动跳过
*/
public function testIgnoredBranchSkipped()
{
$handler = new \app\common\library\WebhookHandler('test_secret_123', 'master');
$server = ['REQUEST_METHOD' => 'POST', 'HTTP_X_GITEA_TOKEN' => 'test_secret_123'];
$body = json_encode(['ref' => 'refs/heads/feature_test']);
$res = $handler->handle($server, [], $body);
$this->assert($res['code'] === 200, "非目标分支应返回 HTTP 200");
$this->assert($res['data']['status'] === 'skipped', "非目标分支状态应为 skipped");
}
/**
* 测试目标分支正确触发回调
*/
public function testTargetBranchTriggersDeploy()
{
$triggered = false;
$handler = new \app\common\library\WebhookHandler('test_secret_123', 'master', function () use (&$triggered) {
$triggered = true;
return "Deploy OK";
});
$server = ['REQUEST_METHOD' => 'POST', 'HTTP_X_GITEA_TOKEN' => 'test_secret_123'];
$body = json_encode(['ref' => 'refs/heads/master']);
$res = $handler->handle($server, [], $body);
$this->assert($res['code'] === 200, "目标分支应返回 HTTP 200");
$this->assert($triggered === true, "目标分支必须触发回调");
$this->assert($res['data']['status'] === 'success', "部署状态应为 success");
$this->assert($res['data']['output'] === 'Deploy OK', "输出应一致");
}
}
$test = new WebhookTest();
$test->run();