Add Gitea webhook auto-deploy for the production site.
Co-authored-by: Cursor <cursoragent@cursor.com>
这个提交包含在:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -25,3 +25,5 @@ node_modules
|
||||
.user.ini
|
||||
AGENTS.md
|
||||
/未命名/
|
||||
.webhook_secret
|
||||
/.deploy/
|
||||
|
||||
163
public/webhook.php
普通文件
163
public/webhook.php
普通文件
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
/**
|
||||
* Gitea 推送自动部署接收端
|
||||
*
|
||||
* GitCode 配置:
|
||||
* - URL: https://henglian.gynhc.com/webhook.php
|
||||
* - 内容类型: application/json
|
||||
* - Secret: 与项目根目录 .webhook_secret 一致
|
||||
* - 触发事件: Push Events
|
||||
* - 分支过滤: main
|
||||
*/
|
||||
|
||||
$projectDir = dirname(__DIR__);
|
||||
$allowBranches = array('main', 'master');
|
||||
$secretFile = $projectDir . '/.webhook_secret';
|
||||
$logFile = $projectDir . '/runtime/log/webhook.log';
|
||||
$triggerFile = $projectDir . '/runtime/deploy.trigger';
|
||||
$deployScript = $projectDir . '/scripts/deploy.sh';
|
||||
|
||||
function webhook_log($logFile, $msg)
|
||||
{
|
||||
$dir = dirname($logFile);
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0775, true);
|
||||
}
|
||||
$line = '[' . date('Y-m-d H:i:s') . '] [webhook] ' . $msg . PHP_EOL;
|
||||
@file_put_contents($logFile, $line, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
function json_exit($code, $message, $httpCode = 200, $extra = array())
|
||||
{
|
||||
http_response_code($httpCode);
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo json_encode(array_merge(array(
|
||||
'code' => $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,
|
||||
));
|
||||
153
scripts/deploy.sh
可执行文件
153
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
|
||||
在新工单中引用
屏蔽一个用户