fix(deploy): improve webhook auto-release reliability and deploy.sh defaults
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+28
-1
@@ -9,7 +9,10 @@ APP_ROOT="${APP_ROOT:-/opt/dukang-haoke}"
|
|||||||
GIT_REMOTE="${GIT_REMOTE:-origin}"
|
GIT_REMOTE="${GIT_REMOTE:-origin}"
|
||||||
GIT_BRANCH="${GIT_BRANCH:-dev}"
|
GIT_BRANCH="${GIT_BRANCH:-dev}"
|
||||||
LOCK_FILE="${DEPLOY_LOCK_FILE:-/var/run/dukang-deploy.lock}"
|
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}"
|
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
|
if [[ -f "$ENV_FILE" ]]; then
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
@@ -24,7 +27,12 @@ log() {
|
|||||||
|
|
||||||
exec 9>"$LOCK_FILE"
|
exec 9>"$LOCK_FILE"
|
||||||
if ! flock -n 9; then
|
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
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -43,6 +51,19 @@ git checkout "$GIT_BRANCH"
|
|||||||
LOCAL_SHA="$(git rev-parse HEAD)"
|
LOCAL_SHA="$(git rev-parse HEAD)"
|
||||||
REMOTE_SHA="$(git rev-parse "$GIT_REMOTE/$GIT_BRANCH")"
|
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
|
if [[ "$LOCAL_SHA" == "$REMOTE_SHA" ]]; then
|
||||||
log "SKIP: already up to date ($LOCAL_SHA)"
|
log "SKIP: already up to date ($LOCAL_SHA)"
|
||||||
exit 0
|
exit 0
|
||||||
@@ -75,3 +96,9 @@ fi
|
|||||||
bash "$APP_ROOT/deploy/remote-release.sh" "${RELEASE_ARGS[@]}"
|
bash "$APP_ROOT/deploy/remote-release.sh" "${RELEASE_ARGS[@]}"
|
||||||
|
|
||||||
log "DONE auto-release ($REMOTE_SHA)"
|
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
|
||||||
|
|||||||
+11
-12
@@ -11,7 +11,7 @@ DEPLOY_PORT="22"
|
|||||||
DEPLOY_SSH_KEY=""
|
DEPLOY_SSH_KEY=""
|
||||||
APP_ROOT="/opt/dukang-haoke"
|
APP_ROOT="/opt/dukang-haoke"
|
||||||
GIT_REMOTE="origin"
|
GIT_REMOTE="origin"
|
||||||
GIT_BRANCH="main"
|
GIT_BRANCH="dev"
|
||||||
|
|
||||||
CHECK_ONLY=false
|
CHECK_ONLY=false
|
||||||
RELEASE_ARGS=()
|
RELEASE_ARGS=()
|
||||||
@@ -27,7 +27,7 @@ usage() {
|
|||||||
--user USER SSH 用户,默认 root
|
--user USER SSH 用户,默认 root
|
||||||
--port PORT SSH 端口,默认 22
|
--port PORT SSH 端口,默认 22
|
||||||
--key PATH SSH 私钥路径
|
--key PATH SSH 私钥路径
|
||||||
--branch BRANCH 发布分支,默认 main
|
--branch BRANCH 发布分支,默认 dev
|
||||||
--check 仅 SSH 连接并查看 PM2 / 端口状态
|
--check 仅 SSH 连接并查看 PM2 / 端口状态
|
||||||
-h, --help 显示帮助
|
-h, --help 显示帮助
|
||||||
|
|
||||||
@@ -108,17 +108,16 @@ if [[ ! -d .git ]]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "==> 拉取代码"
|
if [[ -n "\$RELEASE_ARGS" ]]; then
|
||||||
git fetch "\$GIT_REMOTE"
|
echo "==> 拉取代码"
|
||||||
git checkout "\$GIT_BRANCH"
|
git fetch "\$GIT_REMOTE"
|
||||||
git pull --ff-only "\$GIT_REMOTE" "\$GIT_BRANCH"
|
git checkout "\$GIT_BRANCH"
|
||||||
|
git pull --ff-only "\$GIT_REMOTE" "\$GIT_BRANCH"
|
||||||
if [[ ! -f deploy/remote-release.sh ]]; then
|
bash deploy/remote-release.sh \$RELEASE_ARGS
|
||||||
echo "错误: deploy/remote-release.sh 不存在,请先推送部署脚本到仓库" >&2
|
else
|
||||||
exit 1
|
echo "==> 拉取代码并发布(auto-release)"
|
||||||
|
bash deploy/auto-release.sh
|
||||||
fi
|
fi
|
||||||
|
|
||||||
bash deploy/remote-release.sh \$RELEASE_ARGS
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo "==> 本地发布命令已完成"
|
echo "==> 本地发布命令已完成"
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ function loadEnv() {
|
|||||||
const envPath = join(__dirname, 'auto-release.env');
|
const envPath = join(__dirname, 'auto-release.env');
|
||||||
if (!existsSync(envPath)) return;
|
if (!existsSync(envPath)) return;
|
||||||
for (const line of readFileSync(envPath, 'utf8').split('\n')) {
|
for (const line of readFileSync(envPath, 'utf8').split('\n')) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.replace(/\r$/, '').trim();
|
||||||
if (!trimmed || trimmed.startsWith('#')) continue;
|
if (!trimmed || trimmed.startsWith('#')) continue;
|
||||||
const eq = trimmed.indexOf('=');
|
const eq = trimmed.indexOf('=');
|
||||||
if (eq === -1) continue;
|
if (eq === -1) continue;
|
||||||
@@ -31,10 +31,14 @@ loadEnv();
|
|||||||
|
|
||||||
const PORT = Number(process.env.DEPLOY_WEBHOOK_PORT || 8095);
|
const PORT = Number(process.env.DEPLOY_WEBHOOK_PORT || 8095);
|
||||||
const HOST = process.env.DEPLOY_WEBHOOK_HOST || '127.0.0.1';
|
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 ALLOWED_REF = process.env.DEPLOY_GIT_REF || 'refs/heads/dev';
|
||||||
const APP_ROOT = process.env.APP_ROOT || '/opt/dukang-haoke';
|
const APP_ROOT = process.env.APP_ROOT || '/opt/dukang-haoke';
|
||||||
const RELEASE_SCRIPT = join(APP_ROOT, 'deploy', 'auto-release.sh');
|
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) {
|
function readBody(req) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@@ -61,12 +65,21 @@ function json(res, status, data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function triggerRelease(trigger) {
|
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], {
|
const child = spawn('bash', [RELEASE_SCRIPT], {
|
||||||
detached: true,
|
detached: true,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
env: { ...process.env, DEPLOY_TRIGGER: trigger },
|
env: { ...process.env, DEPLOY_TRIGGER: trigger },
|
||||||
});
|
});
|
||||||
child.unref();
|
child.unref();
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = http.createServer(async (req, res) => {
|
const server = http.createServer(async (req, res) => {
|
||||||
@@ -86,6 +99,7 @@ const server = http.createServer(async (req, res) => {
|
|||||||
|
|
||||||
const token = getToken(req);
|
const token = getToken(req);
|
||||||
if (token !== SECRET) {
|
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' });
|
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 || '';
|
const ref = payload.ref || payload.object_attributes?.ref || '';
|
||||||
if (ref && ref !== ALLOWED_REF) {
|
if (ref && ref !== ALLOWED_REF) {
|
||||||
|
console.log(`[webhook] ignored ref=${ref} (allowed=${ALLOWED_REF})`);
|
||||||
return json(res, 200, {
|
return json(res, 200, {
|
||||||
ok: true,
|
ok: true,
|
||||||
skipped: 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, {
|
return json(res, 202, {
|
||||||
ok: true,
|
ok: true,
|
||||||
accepted: true,
|
accepted: true,
|
||||||
message: 'deploy started',
|
started,
|
||||||
|
message: started ? 'deploy started' : 'deploy debounced (duplicate webhook)',
|
||||||
ref: ref || ALLOWED_REF,
|
ref: ref || ALLOWED_REF,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user