diff --git a/.gitignore b/.gitignore index 994a3c1..1913808 100755 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,5 @@ node_modules .user.ini AGENTS.md /未命名/ +.webhook_secret +/.deploy/ diff --git a/public/webhook.php b/public/webhook.php new file mode 100644 index 0000000..8190f1e --- /dev/null +++ b/public/webhook.php @@ -0,0 +1,163 @@ + $code, + 'message' => $message, + 'time' => date('Y-m-d H:i:s'), + ), $extra), JSON_UNESCAPED_UNICODE); + exit; +} + +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + json_exit(405, 'Method Not Allowed', 405); +} + +$secret = ''; +if (is_readable($secretFile)) { + $secret = trim((string)file_get_contents($secretFile)); +} +if ($secret === '') { + webhook_log($logFile, '未配置 .webhook_secret'); + json_exit(500, 'Webhook secret not configured', 500); +} + +$payload = file_get_contents('php://input'); +if ($payload === false) { + $payload = ''; +} + +$remoteIp = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : 'unknown'; +$event = ''; +if (!empty($_SERVER['HTTP_X_GITEA_EVENT'])) { + $event = $_SERVER['HTTP_X_GITEA_EVENT']; +} elseif (!empty($_SERVER['HTTP_X_GITHUB_EVENT'])) { + $event = $_SERVER['HTTP_X_GITHUB_EVENT']; +} + +$signature = ''; +if (!empty($_SERVER['HTTP_X_GITEA_SIGNATURE'])) { + $signature = $_SERVER['HTTP_X_GITEA_SIGNATURE']; +} elseif (!empty($_SERVER['HTTP_X_GOGS_SIGNATURE'])) { + $signature = $_SERVER['HTTP_X_GOGS_SIGNATURE']; +} elseif (!empty($_SERVER['HTTP_X_HUB_SIGNATURE_256'])) { + $signature = preg_replace('/^sha256=/', '', $_SERVER['HTTP_X_HUB_SIGNATURE_256']); +} + +$signatureMatched = false; +if ($signature !== '') { + $expected = hash_hmac('sha256', $payload, $secret); + if (hash_equals($expected, $signature)) { + $signatureMatched = true; + } +} + +$token = ''; +if (!empty($_GET['token'])) { + $token = $_GET['token']; +} elseif (!empty($_SERVER['HTTP_X_GITEA_TOKEN'])) { + $token = $_SERVER['HTTP_X_GITEA_TOKEN']; +} elseif (!empty($_SERVER['HTTP_X_TOKEN'])) { + $token = $_SERVER['HTTP_X_TOKEN']; +} elseif (!empty($_SERVER['HTTP_AUTHORIZATION'])) { + if (preg_match('/Bearer\s+(.+)/i', $_SERVER['HTTP_AUTHORIZATION'], $m)) { + $token = $m[1]; + } +} + +$tokenMatched = ($token !== '' && hash_equals($secret, $token)); + +if (!$signatureMatched && !$tokenMatched) { + webhook_log($logFile, '密钥验证失败 IP=' . $remoteIp); + json_exit(403, 'Forbidden', 403); +} + +$data = json_decode($payload, true); +if (!is_array($data)) { + $data = array(); +} + +$ref = isset($data['ref']) ? $data['ref'] : ''; +$branch = $ref !== '' ? preg_replace('#^refs/heads/#', '', $ref) : 'main'; + +if ($ref !== '' && strpos($ref, 'refs/heads/') !== 0) { + webhook_log($logFile, '忽略非分支推送: ' . $ref); + json_exit(0, 'Ref ignored', 200, array('ref' => $ref)); +} + +if ($ref !== '' && !in_array($branch, $allowBranches, true)) { + webhook_log($logFile, '分支不在白名单: ' . $branch); + json_exit(0, 'Branch ignored', 200, array('branch' => $branch)); +} + +$pusher = 'unknown'; +if (!empty($data['pusher']['login'])) { + $pusher = $data['pusher']['login']; +} elseif (!empty($data['pusher']['username'])) { + $pusher = $data['pusher']['username']; +} elseif (!empty($data['pusher']['name'])) { + $pusher = $data['pusher']['name']; +} elseif (!empty($data['sender']['login'])) { + $pusher = $data['sender']['login']; +} + +$after = isset($data['after']) ? $data['after'] : ''; +$trigger = json_encode(array( + 'time' => date('Y-m-d H:i:s'), + 'event' => $event !== '' ? $event : 'push', + 'branch' => $branch, + 'pusher' => $pusher, + 'after' => $after, + 'ip' => $remoteIp, +), JSON_UNESCAPED_UNICODE); + +if (@file_put_contents($triggerFile, $trigger) === false) { + webhook_log($logFile, '写入触发文件失败: ' . $triggerFile); + json_exit(500, 'Failed to write trigger', 500); +} +@chmod($triggerFile, 0664); + +$started = false; +if (is_file($deployScript) && function_exists('exec') && !in_array('exec', array_map('trim', explode(',', (string)ini_get('disable_functions'))), true)) { + $cmd = '/bin/bash ' . escapeshellarg($deployScript) . ' --if-triggered > /dev/null 2>&1 &'; + @exec($cmd); + $started = true; +} + +webhook_log($logFile, '部署已触发 branch=' . $branch . ' pusher=' . $pusher . ' after=' . substr($after, 0, 12) . ' exec=' . ($started ? '1' : '0')); + +json_exit(0, $started ? '部署已触发' : '部署任务已写入,等待 cron 执行', 200, array( + 'branch' => $branch, + 'pusher' => $pusher, + 'started' => $started, +)); diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100755 index 0000000..e8bc437 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,153 @@ +#!/bin/bash +# 恒联数通 - 推送自动部署 +# Webhook: public/webhook.php -> runtime/deploy.trigger -> 本脚本 +# 兜底: root cron 每分钟执行 --if-triggered + +set -u + +PROJECT_DIR="/www/wwwroot/henglian.gynhc.com" +BRANCH="main" +GIT_REMOTE="ssh://git@gitcode.gynhc.com:2222/wikiboss/Henglianshutong.git" +LOG_FILE="${PROJECT_DIR}/runtime/log/deploy.log" +TRIGGER_FILE="${PROJECT_DIR}/runtime/deploy.trigger" +WEB_USER="www" +PHP_BIN="/www/server/php/80/bin/php" +COMPOSER_BIN="/usr/bin/composer" +LOCK_FILE="/tmp/henglian_deploy.lock" +DEPLOY_KEY="${PROJECT_DIR}/.deploy/id_ed25519" + +GIT="git -c safe.directory=${PROJECT_DIR}" +if [ -f "$DEPLOY_KEY" ]; then + export GIT_SSH_COMMAND="ssh -i ${DEPLOY_KEY} -p 2222 -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" +fi + +log() { + local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1" + mkdir -p "$(dirname "$LOG_FILE")" + echo "$msg" | tee -a "$LOG_FILE" +} + +mkdir -p "${PROJECT_DIR}/runtime/log" + +if [ "${1:-}" = "--if-triggered" ]; then + NEED_DEPLOY=0 + + if [ -f "$TRIGGER_FILE" ]; then + log "检测到 Webhook 触发: $(cat "$TRIGGER_FILE" 2>/dev/null || true)" + rm -f "$TRIGGER_FILE" + NEED_DEPLOY=1 + fi + + if [ "$NEED_DEPLOY" -eq 0 ] && [ -d "${PROJECT_DIR}/.git" ]; then + cd "$PROJECT_DIR" || exit 0 + $GIT fetch origin "$BRANCH" --quiet 2>/dev/null || true + LOCAL_REV=$($GIT rev-parse HEAD 2>/dev/null || echo "") + REMOTE_REV=$($GIT rev-parse "origin/${BRANCH}" 2>/dev/null || echo "") + if [ -n "$LOCAL_REV" ] && [ -n "$REMOTE_REV" ] && [ "$LOCAL_REV" != "$REMOTE_REV" ]; then + NEED_DEPLOY=1 + log "检测到远程新提交: ${LOCAL_REV:0:7} -> ${REMOTE_REV:0:7}" + fi + fi + + if [ "$NEED_DEPLOY" -eq 0 ]; then + exit 0 + fi +fi + +if [ -f "$LOCK_FILE" ]; then + LOCK_PID=$(cat "$LOCK_FILE" 2>/dev/null || echo "") + if [ -n "$LOCK_PID" ] && kill -0 "$LOCK_PID" 2>/dev/null; then + log "已有部署进程运行中 (pid=${LOCK_PID}),跳过" + exit 0 + fi + rm -f "$LOCK_FILE" +fi +echo $$ > "$LOCK_FILE" +trap 'rm -f "$LOCK_FILE"' EXIT + +log "=============================" +log "开始部署恒联数通..." + +cd "$PROJECT_DIR" || { log "错误: 项目目录不存在"; exit 1; } + +if [ -f "${PROJECT_DIR}/.env" ]; then + cp -a "${PROJECT_DIR}/.env" "${PROJECT_DIR}/runtime/.env.deploy.bak" +fi +if [ -f "${PROJECT_DIR}/public/.user.ini" ]; then + cp -a "${PROJECT_DIR}/public/.user.ini" "${PROJECT_DIR}/runtime/.user.ini.deploy.bak" +fi + +chattr -i "${PROJECT_DIR}/public/.user.ini" 2>/dev/null || true + +if [ ! -d "${PROJECT_DIR}/.git" ]; then + log "初始化 Git 仓库..." + $GIT init + $GIT checkout -B "$BRANCH" 2>/dev/null || true + $GIT remote add origin "$GIT_REMOTE" 2>/dev/null || $GIT remote set-url origin "$GIT_REMOTE" +fi + +CURRENT_REMOTE=$($GIT remote get-url origin 2>/dev/null || true) +if [ "$CURRENT_REMOTE" != "$GIT_REMOTE" ]; then + log "更新远程仓库地址..." + $GIT remote set-url origin "$GIT_REMOTE" 2>/dev/null || $GIT remote add origin "$GIT_REMOTE" +fi + +log "拉取 ${BRANCH} 分支最新代码..." +if ! $GIT fetch origin "$BRANCH" 2>&1 | tee -a "$LOG_FILE"; then + log "Git fetch 失败" + exit 1 +fi + +log "强制同步到 origin/${BRANCH}..." +if ! $GIT reset --hard "origin/${BRANCH}" 2>&1 | tee -a "$LOG_FILE"; then + log "Git reset 失败" + exit 1 +fi +$GIT checkout -B "$BRANCH" "origin/${BRANCH}" >/dev/null 2>&1 || true + +if [ ! -f "${PROJECT_DIR}/.env" ] && [ -f "${PROJECT_DIR}/runtime/.env.deploy.bak" ]; then + log "恢复 .env 配置..." + cp -a "${PROJECT_DIR}/runtime/.env.deploy.bak" "${PROJECT_DIR}/.env" +fi +if [ -f "${PROJECT_DIR}/runtime/.user.ini.deploy.bak" ]; then + cp -a "${PROJECT_DIR}/runtime/.user.ini.deploy.bak" "${PROJECT_DIR}/public/.user.ini" +fi + +if [ -f "${PROJECT_DIR}/composer.json" ]; then + if [ ! -d "${PROJECT_DIR}/vendor" ] || [ ! -f "${PROJECT_DIR}/vendor/autoload.php" ]; then + log "vendor 缺失,执行 composer install..." + if [ -x "$PHP_BIN" ] && [ -f "$COMPOSER_BIN" ]; then + $PHP_BIN "$COMPOSER_BIN" install --no-dev --optimize-autoloader --working-dir="$PROJECT_DIR" 2>&1 | tee -a "$LOG_FILE" || log "composer install 失败(已跳过)" + else + log "未找到 PHP 或 composer,跳过依赖安装" + fi + fi +fi + +log "设置文件权限..." +chown -R ${WEB_USER}:${WEB_USER} "$PROJECT_DIR" 2>/dev/null || true +chattr +i "${PROJECT_DIR}/public/.user.ini" 2>/dev/null || true +chmod +x "${PROJECT_DIR}/scripts/deploy.sh" 2>/dev/null || true +chmod -R 775 "${PROJECT_DIR}/runtime" 2>/dev/null || true +mkdir -p "${PROJECT_DIR}/public/uploads" +chmod -R 775 "${PROJECT_DIR}/public/uploads" 2>/dev/null || true +if [ -f "${PROJECT_DIR}/.webhook_secret" ]; then + chown ${WEB_USER}:${WEB_USER} "${PROJECT_DIR}/.webhook_secret" 2>/dev/null || true + chmod 640 "${PROJECT_DIR}/.webhook_secret" 2>/dev/null || true +fi +if [ -d "${PROJECT_DIR}/.deploy" ]; then + chown -R ${WEB_USER}:${WEB_USER} "${PROJECT_DIR}/.deploy" 2>/dev/null || true + chmod 700 "${PROJECT_DIR}/.deploy" 2>/dev/null || true + chmod 600 "${PROJECT_DIR}/.deploy/"* 2>/dev/null || true +fi + +log "清理运行时缓存..." +rm -rf "${PROJECT_DIR}/runtime/cache/"* 2>/dev/null || true +rm -rf "${PROJECT_DIR}/runtime/temp/"* 2>/dev/null || true + +COMMIT=$($GIT rev-parse --short HEAD 2>/dev/null || echo "unknown") +log "当前版本: ${COMMIT}" +log "部署成功" +log "=============================" + +exit 0