From ffd91803155bf0d1b8bb67b0bee519a9a8ae050b Mon Sep 17 00:00:00 2001 From: jacy-dukang Date: Fri, 10 Jul 2026 12:44:49 +0800 Subject: [PATCH] fix(deploy): improve webhook auto-release reliability and deploy.sh defaults Co-authored-by: Cursor --- deploy/auto-release.sh | 29 ++++++++++++++++++++++++++++- deploy/deploy.sh | 23 +++++++++++------------ deploy/webhook-server.mjs | 24 ++++++++++++++++++++---- 3 files changed, 59 insertions(+), 17 deletions(-) diff --git a/deploy/auto-release.sh b/deploy/auto-release.sh index 3119f61..0612018 100644 --- a/deploy/auto-release.sh +++ b/deploy/auto-release.sh @@ -9,7 +9,10 @@ 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}" +PENDING_FILE="${DEPLOY_PENDING_FILE:-/var/run/dukang-deploy.pending}" LOG_FILE="${DEPLOY_LOG_FILE:-/var/log/dukang/deploy.log}" +WEBHOOK_FETCH_RETRIES="${WEBHOOK_FETCH_RETRIES:-5}" +WEBHOOK_FETCH_DELAY_SEC="${WEBHOOK_FETCH_DELAY_SEC:-3}" if [[ -f "$ENV_FILE" ]]; then # shellcheck disable=SC1090 @@ -24,7 +27,12 @@ log() { exec 9>"$LOCK_FILE" if ! flock -n 9; then - log "SKIP: another deploy is in progress" + if [[ -n "${DEPLOY_TRIGGER:-}" ]]; then + date '+%s' >"$PENDING_FILE" + log "QUEUED: another deploy is in progress (webhook will retry after current run)" + else + log "SKIP: another deploy is in progress" + fi exit 0 fi @@ -43,6 +51,19 @@ git checkout "$GIT_BRANCH" LOCAL_SHA="$(git rev-parse HEAD)" REMOTE_SHA="$(git rev-parse "$GIT_REMOTE/$GIT_BRANCH")" +# CodeUp 常在 push 完成前连发 webhook,首次 fetch 可能仍拿到旧 SHA +if [[ "$LOCAL_SHA" == "$REMOTE_SHA" ]] && [[ -n "${DEPLOY_TRIGGER:-}" ]]; then + for ((i = 1; i <= WEBHOOK_FETCH_RETRIES; i++)); do + log "WEBHOOK retry fetch $i/$WEBHOOK_FETCH_RETRIES (waiting ${WEBHOOK_FETCH_DELAY_SEC}s for remote)" + sleep "$WEBHOOK_FETCH_DELAY_SEC" + git fetch "$GIT_REMOTE" + REMOTE_SHA="$(git rev-parse "$GIT_REMOTE/$GIT_BRANCH")" + if [[ "$LOCAL_SHA" != "$REMOTE_SHA" ]]; then + break + fi + done +fi + if [[ "$LOCAL_SHA" == "$REMOTE_SHA" ]]; then log "SKIP: already up to date ($LOCAL_SHA)" exit 0 @@ -75,3 +96,9 @@ fi bash "$APP_ROOT/deploy/remote-release.sh" "${RELEASE_ARGS[@]}" log "DONE auto-release ($REMOTE_SHA)" + +if [[ -f "$PENDING_FILE" ]]; then + rm -f "$PENDING_FILE" + log "RETRY: running queued webhook deploy" + DEPLOY_TRIGGER="${DEPLOY_TRIGGER:-queued}" bash "$0" +fi diff --git a/deploy/deploy.sh b/deploy/deploy.sh index f65eab4..0f82bfd 100644 --- a/deploy/deploy.sh +++ b/deploy/deploy.sh @@ -11,7 +11,7 @@ DEPLOY_PORT="22" DEPLOY_SSH_KEY="" APP_ROOT="/opt/dukang-haoke" GIT_REMOTE="origin" -GIT_BRANCH="main" +GIT_BRANCH="dev" CHECK_ONLY=false RELEASE_ARGS=() @@ -27,7 +27,7 @@ usage() { --user USER SSH 用户,默认 root --port PORT SSH 端口,默认 22 --key PATH SSH 私钥路径 - --branch BRANCH 发布分支,默认 main + --branch BRANCH 发布分支,默认 dev --check 仅 SSH 连接并查看 PM2 / 端口状态 -h, --help 显示帮助 @@ -108,17 +108,16 @@ if [[ ! -d .git ]]; then exit 1 fi -echo "==> 拉取代码" -git fetch "\$GIT_REMOTE" -git checkout "\$GIT_BRANCH" -git pull --ff-only "\$GIT_REMOTE" "\$GIT_BRANCH" - -if [[ ! -f deploy/remote-release.sh ]]; then - echo "错误: deploy/remote-release.sh 不存在,请先推送部署脚本到仓库" >&2 - exit 1 +if [[ -n "\$RELEASE_ARGS" ]]; then + echo "==> 拉取代码" + git fetch "\$GIT_REMOTE" + git checkout "\$GIT_BRANCH" + git pull --ff-only "\$GIT_REMOTE" "\$GIT_BRANCH" + bash deploy/remote-release.sh \$RELEASE_ARGS +else + echo "==> 拉取代码并发布(auto-release)" + bash deploy/auto-release.sh fi - -bash deploy/remote-release.sh \$RELEASE_ARGS EOF echo "==> 本地发布命令已完成" diff --git a/deploy/webhook-server.mjs b/deploy/webhook-server.mjs index 93dddba..eca2873 100644 --- a/deploy/webhook-server.mjs +++ b/deploy/webhook-server.mjs @@ -14,7 +14,7 @@ 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(); + const trimmed = line.replace(/\r$/, '').trim(); if (!trimmed || trimmed.startsWith('#')) continue; const eq = trimmed.indexOf('='); if (eq === -1) continue; @@ -31,10 +31,14 @@ 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 SECRET = (process.env.DEPLOY_WEBHOOK_SECRET || '').replace(/\r$/, '').trim(); 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'); +const DEBOUNCE_MS = Number(process.env.DEPLOY_WEBHOOK_DEBOUNCE_MS || 15000); + +let lastTriggerAt = 0; +let lastTriggerRef = ''; function readBody(req) { return new Promise((resolve, reject) => { @@ -61,12 +65,21 @@ function json(res, status, data) { } function triggerRelease(trigger) { + const now = Date.now(); + if (now - lastTriggerAt < DEBOUNCE_MS && lastTriggerRef === trigger) { + console.log(`[webhook] debounced duplicate trigger ref=${trigger}`); + return false; + } + lastTriggerAt = now; + lastTriggerRef = trigger; + console.log(`[webhook] trigger deploy ref=${trigger}`); const child = spawn('bash', [RELEASE_SCRIPT], { detached: true, stdio: 'ignore', env: { ...process.env, DEPLOY_TRIGGER: trigger }, }); child.unref(); + return true; } const server = http.createServer(async (req, res) => { @@ -86,6 +99,7 @@ const server = http.createServer(async (req, res) => { const token = getToken(req); if (token !== SECRET) { + console.log(`[webhook] rejected invalid token from ${req.headers['x-real-ip'] || req.socket.remoteAddress || 'unknown'}`); return json(res, 403, { ok: false, message: 'invalid token' }); } @@ -99,6 +113,7 @@ const server = http.createServer(async (req, res) => { const ref = payload.ref || payload.object_attributes?.ref || ''; if (ref && ref !== ALLOWED_REF) { + console.log(`[webhook] ignored ref=${ref} (allowed=${ALLOWED_REF})`); return json(res, 200, { ok: true, skipped: true, @@ -106,11 +121,12 @@ const server = http.createServer(async (req, res) => { }); } - triggerRelease(ref || 'manual'); + const started = triggerRelease(ref || 'manual'); return json(res, 202, { ok: true, accepted: true, - message: 'deploy started', + started, + message: started ? 'deploy started' : 'deploy debounced (duplicate webhook)', ref: ref || ALLOWED_REF, }); });