建立推送机制 webhook
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
# 复制为 auto-release.env(勿提交 Git)
|
||||
# 服务器路径: /opt/dukang-haoke/deploy/auto-release.env
|
||||
|
||||
APP_ROOT=/opt/dukang-haoke
|
||||
GIT_REMOTE=origin
|
||||
GIT_BRANCH=dev
|
||||
DEPLOY_GIT_REF=refs/heads/dev
|
||||
|
||||
# Webhook 密钥(CodeUp 配置「Secret Token」时使用同一值)
|
||||
DEPLOY_WEBHOOK_SECRET=change-me-to-a-long-random-string
|
||||
|
||||
DEPLOY_WEBHOOK_HOST=127.0.0.1
|
||||
DEPLOY_WEBHOOK_PORT=8095
|
||||
DEPLOY_LOG_FILE=/var/log/dukang/deploy.log
|
||||
DEPLOY_LOCK_FILE=/var/run/dukang-deploy.lock
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env bash
|
||||
# 自动发版:git pull + remote-release(由 webhook 或手动触发)
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ENV_FILE="$SCRIPT_DIR/auto-release.env"
|
||||
|
||||
APP_ROOT="${APP_ROOT:-/opt/dukang-haoke}"
|
||||
GIT_REMOTE="${GIT_REMOTE:-origin}"
|
||||
GIT_BRANCH="${GIT_BRANCH:-dev}"
|
||||
LOCK_FILE="${DEPLOY_LOCK_FILE:-/var/run/dukang-deploy.lock}"
|
||||
LOG_FILE="${DEPLOY_LOG_FILE:-/var/log/dukang/deploy.log}"
|
||||
|
||||
if [[ -f "$ENV_FILE" ]]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "$ENV_FILE"
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$LOG_FILE")"
|
||||
|
||||
log() {
|
||||
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
|
||||
}
|
||||
|
||||
exec 9>"$LOCK_FILE"
|
||||
if ! flock -n 9; then
|
||||
log "SKIP: another deploy is in progress"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "START auto-release (branch=$GIT_BRANCH)"
|
||||
|
||||
cd "$APP_ROOT"
|
||||
|
||||
if [[ ! -d .git ]]; then
|
||||
log "ERROR: $APP_ROOT is not a git repository"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
git fetch "$GIT_REMOTE"
|
||||
git checkout "$GIT_BRANCH"
|
||||
|
||||
LOCAL_SHA="$(git rev-parse HEAD)"
|
||||
REMOTE_SHA="$(git rev-parse "$GIT_REMOTE/$GIT_BRANCH")"
|
||||
|
||||
if [[ "$LOCAL_SHA" == "$REMOTE_SHA" ]]; then
|
||||
log "SKIP: already up to date ($LOCAL_SHA)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "PULL $LOCAL_SHA -> $REMOTE_SHA"
|
||||
git pull --ff-only "$GIT_REMOTE" "$GIT_BRANCH"
|
||||
|
||||
# 修复 Windows 换行
|
||||
find "$APP_ROOT/deploy" -maxdepth 1 -name '*.sh' -exec sed -i 's/\r$//' {} + 2>/dev/null || true
|
||||
|
||||
log "RUN remote-release.sh"
|
||||
bash "$APP_ROOT/deploy/remote-release.sh"
|
||||
|
||||
log "DONE auto-release ($REMOTE_SHA)"
|
||||
@@ -6,4 +6,4 @@ DEPLOY_PORT=22
|
||||
|
||||
APP_ROOT=/opt/dukang-haoke
|
||||
GIT_REMOTE=origin
|
||||
GIT_BRANCH=main
|
||||
GIT_BRANCH=dev
|
||||
|
||||
@@ -50,5 +50,16 @@ module.exports = {
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
},
|
||||
{
|
||||
name: 'dukang-deploy-webhook',
|
||||
cwd: `${APP_ROOT}/deploy`,
|
||||
script: 'webhook-server.mjs',
|
||||
interpreter: 'node',
|
||||
instances: 1,
|
||||
exec_mode: 'fork',
|
||||
env: {
|
||||
NODE_ENV: 'production',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# 杜康好客 — CodeUp Webhook 反代(挂到 dkapi.runxian.top 443/80 server 块内)
|
||||
# setup-webhook.sh 会自动 include 此文件
|
||||
|
||||
location = /hooks/deploy {
|
||||
proxy_pass http://127.0.0.1:8095/deploy;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Gitlab-Token $http_x_gitlab_token;
|
||||
proxy_set_header X-Codeup-Token $http_x_codeup_token;
|
||||
proxy_set_header X-Deploy-Token $http_x_deploy_token;
|
||||
client_max_body_size 1m;
|
||||
}
|
||||
@@ -42,6 +42,7 @@ corepack prepare pnpm@11.2.2 --activate 2>/dev/null || true
|
||||
|
||||
echo "==> 2. 安装依赖"
|
||||
export NODE_OPTIONS="${NODE_OPTIONS:---max-old-space-size=2048}"
|
||||
pnpm approve-builds --all 2>/dev/null || true
|
||||
pnpm install --frozen-lockfile 2>/dev/null || pnpm install
|
||||
|
||||
if [[ "$SKIP_BUILD" == false ]]; then
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#!/usr/bin/env bash
|
||||
# 在服务器上执行:安装 Webhook 自动发版
|
||||
set -euo pipefail
|
||||
|
||||
APP_ROOT="${APP_ROOT:-/opt/dukang-haoke}"
|
||||
DEPLOY_DIR="$APP_ROOT/deploy"
|
||||
ENV_FILE="$DEPLOY_DIR/auto-release.env"
|
||||
LOG_DIR="/var/log/dukang"
|
||||
|
||||
echo "==> 1. 准备目录"
|
||||
mkdir -p "$LOG_DIR" /var/run
|
||||
chmod +x "$DEPLOY_DIR/auto-release.sh" "$DEPLOY_DIR/setup-webhook.sh" 2>/dev/null || true
|
||||
sed -i 's/\r$//' "$DEPLOY_DIR"/*.sh "$DEPLOY_DIR"/*.mjs 2>/dev/null || true
|
||||
|
||||
echo "==> 2. 生成 webhook 密钥(若不存在)"
|
||||
if [[ ! -f "$ENV_FILE" ]]; then
|
||||
SECRET="$(openssl rand -hex 24)"
|
||||
cp "$DEPLOY_DIR/auto-release.env.example" "$ENV_FILE"
|
||||
sed -i "s/change-me-to-a-long-random-string/$SECRET/" "$ENV_FILE"
|
||||
chmod 600 "$ENV_FILE"
|
||||
echo " 已创建 $ENV_FILE"
|
||||
else
|
||||
echo " 保留已有 $ENV_FILE"
|
||||
fi
|
||||
|
||||
echo "==> 3. Nginx webhook 路由"
|
||||
HOOK_CONF="$DEPLOY_DIR/nginx-deploy-webhook.conf"
|
||||
# 勿放在 /etc/nginx/conf.d/(会被 http 上下文直接加载)
|
||||
rm -f /etc/nginx/conf.d/dukang-deploy-webhook.conf
|
||||
|
||||
MARKER="include $HOOK_CONF;"
|
||||
for conf in /etc/nginx/conf.d/dukang-runxian.conf /etc/nginx/conf.d/dukang-runxian-ssl.conf; do
|
||||
if [[ -f "$conf" ]] && grep -q 'server_name dkapi.runxian.top' "$conf"; then
|
||||
# 清理旧错误 include
|
||||
sed -i '\|include /etc/nginx/conf.d/dukang-deploy-webhook.conf;|d' "$conf"
|
||||
if ! grep -qF "$MARKER" "$conf"; then
|
||||
sed -i "/server_name dkapi.runxian.top;/a\\ $MARKER" "$conf"
|
||||
echo " 已 patch $conf"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
nginx -t
|
||||
systemctl reload nginx
|
||||
|
||||
echo "==> 4. 启动 PM2 webhook"
|
||||
cd "$APP_ROOT"
|
||||
if pm2 describe dukang-deploy-webhook &>/dev/null; then
|
||||
pm2 restart dukang-deploy-webhook
|
||||
else
|
||||
pm2 start "$DEPLOY_DIR/ecosystem.config.cjs" --only dukang-deploy-webhook
|
||||
fi
|
||||
pm2 save
|
||||
|
||||
echo "==> 5. 健康检查"
|
||||
sleep 1
|
||||
curl -sf http://127.0.0.1:8095/health | head -c 200
|
||||
echo
|
||||
|
||||
SECRET="$(grep DEPLOY_WEBHOOK_SECRET "$ENV_FILE" | cut -d= -f2- | tr -d '\"')"
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Webhook 已就绪"
|
||||
echo " URL: https://dkapi.runxian.top/hooks/deploy"
|
||||
echo " Secret: $SECRET"
|
||||
echo " Branch: dev (refs/heads/dev)"
|
||||
echo " Log: $LOG_DIR/deploy.log"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "CodeUp 配置:仓库 → 设置 → Webhooks → 添加"
|
||||
echo " URL: https://dkapi.runxian.top/hooks/deploy"
|
||||
echo " Secret Token: (与上方 Secret 相同)"
|
||||
echo " 触发事件: Push events"
|
||||
echo " 分支过滤: dev"
|
||||
@@ -0,0 +1,120 @@
|
||||
/**
|
||||
* CodeUp / GitLab 兼容的部署 Webhook 接收器
|
||||
* 监听 127.0.0.1:8095,由 Nginx 反代 /hooks/deploy
|
||||
*/
|
||||
import http from 'http';
|
||||
import { spawn } from 'child_process';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { dirname, join } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
function loadEnv() {
|
||||
const envPath = join(__dirname, 'auto-release.env');
|
||||
if (!existsSync(envPath)) return;
|
||||
for (const line of readFileSync(envPath, 'utf8').split('\n')) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||
const eq = trimmed.indexOf('=');
|
||||
if (eq === -1) continue;
|
||||
const key = trimmed.slice(0, eq).trim();
|
||||
let val = trimmed.slice(eq + 1).trim();
|
||||
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
||||
val = val.slice(1, -1);
|
||||
}
|
||||
if (!(key in process.env)) process.env[key] = val;
|
||||
}
|
||||
}
|
||||
|
||||
loadEnv();
|
||||
|
||||
const PORT = Number(process.env.DEPLOY_WEBHOOK_PORT || 8095);
|
||||
const HOST = process.env.DEPLOY_WEBHOOK_HOST || '127.0.0.1';
|
||||
const SECRET = process.env.DEPLOY_WEBHOOK_SECRET || '';
|
||||
const ALLOWED_REF = process.env.DEPLOY_GIT_REF || 'refs/heads/dev';
|
||||
const APP_ROOT = process.env.APP_ROOT || '/opt/dukang-haoke';
|
||||
const RELEASE_SCRIPT = join(APP_ROOT, 'deploy', 'auto-release.sh');
|
||||
|
||||
function readBody(req) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
req.on('data', (c) => chunks.push(c));
|
||||
req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function getToken(req) {
|
||||
return (
|
||||
req.headers['x-gitlab-token'] ||
|
||||
req.headers['x-codeup-token'] ||
|
||||
req.headers['x-deploy-token'] ||
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
function json(res, status, data) {
|
||||
const body = JSON.stringify(data);
|
||||
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
|
||||
res.end(body);
|
||||
}
|
||||
|
||||
function triggerRelease(trigger) {
|
||||
const child = spawn('bash', [RELEASE_SCRIPT], {
|
||||
detached: true,
|
||||
stdio: 'ignore',
|
||||
env: { ...process.env, DEPLOY_TRIGGER: trigger },
|
||||
});
|
||||
child.unref();
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
||||
|
||||
if (req.method === 'GET' && url.pathname === '/health') {
|
||||
return json(res, 200, { ok: true, service: 'dukang-deploy-webhook' });
|
||||
}
|
||||
|
||||
if (req.method !== 'POST' || url.pathname !== '/deploy') {
|
||||
return json(res, 404, { ok: false, message: 'not found' });
|
||||
}
|
||||
|
||||
if (!SECRET) {
|
||||
return json(res, 503, { ok: false, message: 'webhook secret not configured' });
|
||||
}
|
||||
|
||||
const token = getToken(req);
|
||||
if (token !== SECRET) {
|
||||
return json(res, 403, { ok: false, message: 'invalid token' });
|
||||
}
|
||||
|
||||
let payload = {};
|
||||
try {
|
||||
const raw = await readBody(req);
|
||||
if (raw) payload = JSON.parse(raw);
|
||||
} catch {
|
||||
return json(res, 400, { ok: false, message: 'invalid json body' });
|
||||
}
|
||||
|
||||
const ref = payload.ref || payload.object_attributes?.ref || '';
|
||||
if (ref && ref !== ALLOWED_REF) {
|
||||
return json(res, 200, {
|
||||
ok: true,
|
||||
skipped: true,
|
||||
message: `ignored ref: ${ref} (allowed: ${ALLOWED_REF})`,
|
||||
});
|
||||
}
|
||||
|
||||
triggerRelease(ref || 'manual');
|
||||
return json(res, 202, {
|
||||
ok: true,
|
||||
accepted: true,
|
||||
message: 'deploy started',
|
||||
ref: ref || ALLOWED_REF,
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(PORT, HOST, () => {
|
||||
console.log(`dukang deploy webhook listening on http://${HOST}:${PORT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user