利用Jenkins预约执行部署任务
工作中需要使用Jenkins在没有插件的前提下实现预约执行任务,思考了两种思路并记录。
前提:
工作环境安装有Jenkins,但是无法安装预约功能的插件。使用简单的循环定时进行发布显然不是个好的选择。
思路:
需求:
需要预约(打开任务后)可以执行部署任务,但是只会在约定时间执行一次,执行完成后关闭任务。需要能支持多部署任务的执行。
思路及代码:
由于需求中需要执行、关闭其他任务,在我所知的范围内,最简单的是使用Jenkins的api。 然后是预约定时问题,考虑了两种逻辑。 逻辑1、 每分钟执行,执行后判断时间是否匹配,不匹配则不执行部署任务。
代码如下:
#!/bin/bash
#set -euo pipefail
set -uo pipefail
# ================== 配置区 ==================
#=====设置配置
JENKINS_URL="http://192.168.1.22:8086" # 替换为你的 Jenkins 地址
USERNAME="username" # 你的 Jenkins 用户名
API_TOKEN="117da895212c8e1582a4b5eb5ed6213cee" # 替换为你的 API Token
THIS_JOB_NAME="定时发布系统" # 当前调度任务名
#=====定时配置
TARGET_DATETIME="2026-02-05 07:40" # 目标触发时间(注意格式)
# 按执行顺序列出任务名(支持中文,依次按顺序执行)
JOBS=(
"代码同步_backend_prd"
"部署_webfrontend_prd"
"部署并启动_backend_prd"
)
MAX_WAIT_MINUTES=60 #超时时间,单位分钟
# ==========================================
# ===== 清理函数 =====
cleanup() {
# 禁用调度器
if [[ "$SHOULD_DISABLE_SCHEDULER" == "true" ]]; then
echo "[$(date)] Disabling scheduler task..."
curl -s -f -X POST
"${JENKINS_URL}/job/${THIS_JOB_NAME}/disable"
--user "${USERNAME}:${API_TOKEN}"
--output /dev/null && echo "Disabled." || echo "Warn: disable failed."
else
echo "[$(date)] Skip cleanup (not triggered)."
fi
}
# ========================
# ===== 注册 trap =====
# 在 EXIT 时总是执行 cleanup
trap cleanup EXIT
# =========================
# ===== 全局标志 =====
SHOULD_DISABLE_SCHEDULER=false
# =========================
CURRENT=$(date '+%Y-%m-%d %H:%M')
BUILD_NUMBER=${BUILD_NUMBER:-0} # Jenkins 注入的环境变量
# ===== 清理上一次的“跳过”构建,避免过多任务记录 =====
if [[ -f ".last_skip_build" ]]; then
LAST_SKIP=$(cat .last_skip_build)
if [[ -n "$LAST_SKIP" ]]; then
echo "[$(date)] Cleaning up skipped build #$LAST_SKIP"
curl -s -f -X POST
"${JENKINS_URL}/job/${THIS_JOB_NAME}/${LAST_SKIP}/doDelete"
--user "${USERNAME}:${API_TOKEN}"
--output /dev/null && echo "Deleted." || echo "Cleanup failed."
fi
rm -f .last_skip_build
fi
# ================================
echo "[$(date)] Current: $CURRENT, Target: $TARGET_DATETIME"
# 转为时间戳(兼容 Linux)
target_ts=$(date -d "$TARGET_DATETIME" +%s 2>/dev/null)
if [[ $? -ne 0 ]]; then
echo "[$(date)] ERROR: Failed to parse TARGET_DATETIME"
exit 1
fi
current_ts=$(date +%s)
deadline_ts=$(( target_ts + 60 )) #多一分钟窗口时间
if (( current_ts < target_ts )); then
# 还没到时间:跳过,不禁用
echo "[$(date)] Skip: before target time. Waiting..."
echo "$BUILD_NUMBER" > .last_skip_build
exit 0
elif (( current_ts > deadline_ts )); then
# 已超时:跳过 + 禁用
echo "[$(date)] ERROR: Missed target time . Disabling scheduler."
SHOULD_DISABLE_SCHEDULER=true # 触发 cleanup 中的 disable
echo "$BUILD_NUMBER" > .last_skip_build
exit 0
else
# 在窗口期内:继续执行
echo "[$(date)] Within grace period. Proceeding..."
fi
# ==========================
echo "Start sequential execution..."
# ===== 进入目标时间后,标记为需要禁用 =====
SHOULD_DISABLE_SCHEDULER=true
# ==============================================
trigger_and_wait() {
local job_name="$1"
echo "Triggering: $job_name"
# 安全获取 nextBuildNumber
local next_build_raw next_build
next_build_raw=$(curl -s --user "${USERNAME}:${API_TOKEN}"
"${JENKINS_URL}/job/${job_name}/api/json?tree=nextBuildNumber" 2>/dev/null || echo "{}")
next_build=$(echo "$next_build_raw" | grep -o '"nextBuildNumber":[[:space:]]*[0-9]+' | sed 's/[^0-9]*//g' || echo "")
if [[ -z "$next_build" ]]; then
echo "ERROR: Cannot get build number for $job_name"
exit 1
fi
echo "Next build: $next_build"
# 触发构建
if ! curl -s -f -X POST
"${JENKINS_URL}/job/${job_name}/build"
--user "${USERNAME}:${API_TOKEN}"
--output /dev/null; then
echo "ERROR: Failed to trigger $job_name"
exit 1
fi
# 等待完成
local check_count=0 max_checks=$(( MAX_WAIT_MINUTES * 6 ))
while true; do
sleep 10
((check_count++))
echo "Waiting... (check #$check_count)"
[[ $check_count -gt $max_checks ]] && { echo "TIMEOUT"; exit 1; }
local result_raw result
result_raw=$(curl -s --user "${USERNAME}:${API_TOKEN}"
"${JENKINS_URL}/job/${job_name}/${next_build}/api/json?tree=result" 2>/dev/null || echo '{"result":null}')
result=$(echo "$result_raw" | grep -o '"result":"[A-Z]*"' | cut -d'"' -f4 || echo "")
if [[ -z "$result" ]]; then
continue
elif [[ "$result" == "SUCCESS" ]]; then
echo "✓ Success"
return 0
else
echo "✗ Failed: $result"
exit 1
fi
done
}
for job in "${JOBS[@]}"; do
trigger_and_wait "$job"
done
echo "DONE."
优势:
可以指定具体年份
时间设置符合直觉
错误点击执行不会导致执行关联任务
劣势:
逻辑较为复杂,不简洁
每分钟执行一次,有性能占用
会有非常多的任务记录(后添加自动删除任务记录的逻辑)
逻辑2、 使用定时任务功能,预约时间后,任务会执行一次,然后禁用自己。
#!/bin/bash
#set -euo pipefail
set -uo pipefail
# =====================================================按执行顺序列出任务名(支持中文,依次按顺序执行)===============================
JOBS=(
"代码同步_backend_prd"
"部署_webfrontend_prd"
"部署并启动_backend_prd"
)
# ================== 配置区 ==================
JENKINS_URL="http://192.168.1.22:8086" # 替换为你的 Jenkins 地址
USERNAME="username" # 你的 Jenkins 用户名
API_TOKEN="117da895212c8e1582a4b5eb5ed6213cee" # 替换为你的 API Token
THIS_JOB_NAME="定时发布系统" # 当前调度任务名
MAX_WAIT_MINUTES=60 #超时时间,单位分钟
# ==========================================
# ===== 关键:立即禁用自己,确保只运行一次 =====
echo "[$(date)] Disabling scheduler task to ensure one-time execution..."
if curl -s -f -X POST
"${JENKINS_URL}//job/${THIS_JOB_NAME}/disable"
--user "${USERNAME}:${API_TOKEN}"
--output /dev/null; then
echo "[$(date)] Scheduler disabled successfully."
else
echo "[$(date)] [WARN] Failed to disable scheduler (may run again next year)."
fi
# ===================================================
# ===== 执行任务函数=====
trigger_and_wait() {
local job_name="$1"
echo "[$(date)] Triggering: $job_name"
# 获取下一次构建号
local next_build_raw next_build
next_build_raw=$(curl -s --user "${USERNAME}:${API_TOKEN}"
"${JENKINS_URL}/job/${job_name}/api/json?tree=nextBuildNumber" 2>/dev/null || echo "{}")
next_build=$(echo "$next_build_raw" | grep -o '"nextBuildNumber":[[:space:]]*[0-9]+' | sed 's/[^0-9]*//g' || echo "")
if [[ -z "$next_build" ]]; then
echo "[$(date)] ERROR: Cannot get build number for $job_name"
exit 1
fi
echo "[$(date)] Next build: $next_build"
# 触发构建
if ! curl -s -f -X POST
"${JENKINS_URL}/job/${job_name}/build"
--user "${USERNAME}:${API_TOKEN}"
--output /dev/null; then
echo "[$(date)] ERROR: Failed to trigger $job_name"
exit 1
fi
# 等待完成(带超时)
local check_count=0 max_checks=$(( MAX_WAIT_MINUTES * 6 ))
while true; do
sleep 10
((check_count++))
echo "[$(date)] Waiting... (check #$check_count)"
if (( check_count > max_checks )); then
echo "[$(date)] TIMEOUT: Job did not complete in ${MAX_WAIT_MINUTES} minutes"
exit 1
fi
local result_raw result
result_raw=$(curl -s --user "${USERNAME}:${API_TOKEN}"
"${JENKINS_URL}/job/${job_name}/${next_build}/api/json?tree=result" 2>/dev/null || echo '{"result":null}')
result=$(echo "$result_raw" | grep -o '"result":"[A-Z]*"' | cut -d'"' -f4 || echo "")
if [[ -z "$result" ]]; then
continue
elif [[ "$result" == "SUCCESS" ]]; then
echo "[$(date)] ✓ Success: $job_name"
return 0
else
echo "[$(date)] ✗ Failed: $job_name with result $result"
exit 1
fi
done
}
# ===================================================
# ===== 按顺序执行所有任务 =====
echo "[$(date)] Starting sequential execution of ${#JOBS[@]} jobs..."
for job in "${JOBS[@]}"; do
trigger_and_wait "$job"
done
echo "[$(date)] DONE. All jobs completed successfully."
优势:
代码更简单、高效
劣势:
不在设定时间点击执行任务会导致关联任务被执行
依赖Cron 表达式,个人不喜欢(主观)
