场景:企业内网隔离、开发机不能直连 Git、需定时拉取最新代码并自动构建打包,构建成功后发送精美邮件通知。
一、背景与痛点
公司网络拓扑如下:
- 服务器 A(生产环境):不联网,仅内网,用于最终部署。
- 服务器 B (构建机):可联网,可访问 A,但不能访问外网 Git 仓库。
- 笔记本电脑 C (开发人员机器):可联网,可访问外网 Git,可主动连接 B 和 A(但 B/A 不能主动连 C)。
需求:
- C 定时检测 Git 仓库是否有新提交,若有,则触发 B 上的 Jenkins 构建。
- B 负责拉取代码(通过 C 推送或共享目录)、安装依赖(需联网)、打包构建,并将产物部署到 A(本方案中省略 A,直接在 B 本地归档)。
- 构建成功后,Jenkins 自动发送自定义内容邮件给指定收件人,邮件样式美观。
最终,我们完成了一套全自动、低性能消耗(C 仅运行轻量级 PowerShell 轮询)、高可维护的方案。
二、整体架构设计
┌─────────────────────────────────────────────────────────┐
│ Git 远程仓库(GitHub/GitLab/自建) │
└─────────────────────────────────────────────────────────┘
↑ 定时 fetch(C主动)
┌─────────────────────────────────────────────────────────┐
│ 笔记本电脑 C(Windows 10/11) │
│ - 映射 B 的共享目录为 Z: 盘(代码仓库物理位于 B) │
│ - PowerShell 脚本每5分钟检测更新 │
│ - 有更新则 git pull 并调用 Jenkins API 触发构建 │
└─────────────────────────────────────────────────────────┘
│ 网络映射 + HTTP API
↓
┌─────────────────────────────────────────────────────────┐
│ 服务器 B(Windows Server) │
│ - 共享目录 D:\Share\project\xxx (代码仓库) │
│ - Jenkins 服务(运行于本地) │
│ - 直接本地构建(无需额外拉取) │
│ - 构建成功后发送邮件 │
└─────────────────────────────────────────────────────────┘
关键点:
- 代码仓库物理存储在 B,C 通过映射网络驱动器操作,实际上是 git pull 更新 B 上的仓库。
- Jenkins 构建时直接读取本地目录,无需二次传输。
- C 只做轻量检测和触发,性能消耗极低。
三、准备工作(软件安装与配置)
3.1 服务器 B 环境搭建
- 操作系统:Windows Server 2019/2022
- 安装 OpenSSH 客户端(可选,用于后续扩展)
- 安装 Git for Windows (确保
git命令在系统 PATH 中) - 安装 Node.js 和 pnpm/npm(根据项目需要)
- 安装 Jenkins LTS (推荐
.msi安装为 Windows 服务)- 安装时选择默认端口
8080,安装完后访问http://B_IP:8080完成初始化
- 安装时选择默认端口
- 安装必要 Jenkins 插件:
- Pipeline(自带)
- Git(自带)
- Email Extension Plugin(用于高级邮件通知)
- Build User Vars Plugin(可选,获取触发者信息)
3.2 目录与共享设置
在 B 上创建项目目录,例如 D:\Share\project\your_project,并确保该目录是一个 Git 仓库(可先通过 C 克隆或复制过来)。
将 D:\Share\project 设置为共享,共享名为 ProjectShare,赋予 Everyone 或特定账户完全控制权限(因为 C 需要写入)。
在 C 电脑上,将 \\B_IP\ProjectShare 映射为网络驱动器 Z:,这样 C 可以直接操作 B 上的文件。
3.3 Git 凭证配置(重要)
由于 git pull 操作在 C 上执行,且仓库可能需要认证,需在 C 上配置 Git 凭证缓存。执行:
bash
git config --global credential.helper wincred
然后手动在 C 上执行一次 git pull,输入账号密码,之后会被 Windows 凭据管理器记住。
四、核心脚本与任务配置
4.1 C 电脑轮询脚本:GitPoller.ps1
该脚本负责:
- 通过映射的 Z 盘,进入代码目录
git fetch获取远程最新提交- 比对本地存储的上次 Commit ID
- 若有变化,则执行
git pull更新本地仓库 - 调用 Jenkins API 触发构建(使用 API Token 认证,避免 CSRF 问题)
- 更新记录文件
脚本完整内容(已脱敏):
powershell
# ========== 配置区域(修改为实际值)==========
$RepoPath = "Z:\" # 映射盘根目录(即B共享目录)
$Branch = "main" # 监听分支
$HashFile = "C:\Trigger\last_commit.txt"
$JenkinsUrl = "http://JENKINS_IP:8080/job/PROJECT_NAME/build?token=BUILD_TOKEN"
$LogFile = "C:\Trigger\poller.log"
# Jenkins API Token 认证(推荐)
$JenkinsUser = "YOUR_USERNAME"
$JenkinsToken = "YOUR_API_TOKEN" # 在Jenkins用户配置中生成
# ===========================================
function Write-Log {
param([string]$Msg)
$Time = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
"[$Time] $Msg" | Out-File -FilePath $LogFile -Append -Encoding UTF8
}
Set-Location $RepoPath
# 1. Fetch 最新提交
Write-Log "git fetch origin $Branch"
git fetch origin $Branch --quiet
if ($LASTEXITCODE -ne 0) {
Write-Log "ERROR: git fetch failed"
exit 1
}
# 2. 获取最新 Commit ID
$NewHash = git rev-parse origin/$Branch
$NewHash = $NewHash.Trim()
Write-Log "Latest remote commit: $NewHash"
# 3. 读取上次记录的 Commit
$OldHash = ""
if (Test-Path $HashFile) {
$OldHash = Get-Content $HashFile -Raw -Encoding UTF8
$OldHash = $OldHash.Trim()
}
Write-Log "Previous recorded commit: $OldHash"
# 4. 对比
if ($NewHash -eq $OldHash) {
Write-Log "No new commit, exiting"
exit 0
}
Write-Log "New commit detected! Triggering Jenkins build..."
# 5. 更新本地工作区(快进合并)
Write-Log "git pull origin $Branch --ff-only"
git pull origin $Branch --ff-only
if ($LASTEXITCODE -ne 0) {
Write-Log "ERROR: git pull failed"
exit 1
}
# 6. 调用 Jenkins API(携带 Basic Auth)
$Pair = "${JenkinsUser}:${JenkinsToken}"
$EncodedCreds = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($Pair))
$Headers = @{ Authorization = "Basic $EncodedCreds" }
try {
$Response = Invoke-WebRequest -Uri $JenkinsUrl -Method Post -Headers $Headers -TimeoutSec 10
if ($Response.StatusCode -eq 201 -or $Response.StatusCode -eq 200) {
Write-Log "Jenkins triggered successfully (HTTP $($Response.StatusCode))"
} else {
Write-Log "Jenkins returned unexpected status: $($Response.StatusCode)"
}
} catch {
Write-Log "ERROR: Jenkins API call failed: $($_.Exception.Message)"
}
# 7. 更新记录
$NewHash | Out-File -FilePath $HashFile -Encoding UTF8 -Force
Write-Log "Process completed"
4.2 避免 PowerShell 窗口闪烁(静默运行)
使用 VBS 包装器 调用 PowerShell,彻底隐藏窗口。
创建 C:\Trigger\run_hidden.vbs:
vbs
CreateObject("WScript.Shell").Run "powershell.exe -ExecutionPolicy Bypass -NoProfile -WindowStyle Hidden -File ""C:\Trigger\GitPoller.ps1""", 0, False
然后在任务计划程序中创建任务:
- 触发器:每 5 分钟重复一次
- 操作:启动程序
wscript.exe,参数"C:\Trigger\run_hidden.vbs" - 安全选项:选择"不管用户是否登录都要运行",使用当前用户账户
这样任务将完全静默,不产生任何窗口。
五、Jenkins 配置(Pipeline Job)
5.1 创建 Pipeline 任务
- 新建任务 → 输入名称
your_project→ 选择 Pipeline - 在 构建触发器 中勾选 "触发远程构建" ,设置 Token,例如
auto-build - 在 Pipeline 定义中,选择 "Pipeline script",将以下脚本粘贴(根据项目调整构建命令和产物路径)
5.2 Pipeline 脚本完整内容
groovy
pipeline {
agent any
environment {
PROJECT_PATH = 'D:\\Share\\project\\your_project'
BUILD_OUTPUT = 'D:\\Share\\BuildOutput'
}
stages {
stage('打包构建') {
steps {
dir("${env.PROJECT_PATH}") {
bat 'npm install' // 若用 pnpm 可替换
bat 'npm run build'
}
}
}
stage('发布产物') {
steps {
script {
bat """
if exist "${env.PROJECT_PATH}\\dist" (
xcopy /E /Y "${env.PROJECT_PATH}\\dist" "${env.BUILD_OUTPUT}\\"
) else if exist "${env.PROJECT_PATH}\\Realease" (
xcopy /E /Y "${env.PROJECT_PATH}\\Realease" "${env.BUILD_OUTPUT}\\"
)
"""
}
}
}
}
post {
success {
echo '构建成功!'
script {
// 获取 Git 提交信息(使用 PowerShell 避免回显)
def commitMsg = powershell(script: "git -C \"${env.PROJECT_PATH}\" log -1 --pretty=%B", returnStdout: true).trim()
def commitAuthor = powershell(script: "git -C \"${env.PROJECT_PATH}\" log -1 --pretty=%an", returnStdout: true).trim()
def commitInfo = "作者: ${commitAuthor}\n提交信息: ${commitMsg}"
emailext (
to: 'recipient@company.com',
subject: "构建成功: ${env.JOB_NAME} - Build #${env.BUILD_NUMBER}",
body: """
<html>
<head>
<style>
body { font-family: 'Segoe UI', Arial, sans-serif; margin: 20px; }
.container { max-width: 600px; margin: auto; border: 1px solid #ddd; border-radius: 8px; padding: 20px; background: #fafafa; }
h2 { color: #2c3e50; border-bottom: 2px solid #27ae60; padding-bottom: 10px; }
.success { color: #27ae60; font-weight: bold; }
.info-table { width: 100%; border-collapse: collapse; margin-top: 15px; }
.info-table td { padding: 10px 8px; border-bottom: 1px solid #eee; }
.info-table .label { font-weight: bold; width: 30%; color: #555; }
.info-table .value { color: #333; }
.commit-message { background: #f0f0f0; padding: 12px; border-radius: 4px; font-family: monospace; white-space: pre-wrap; }
.footer { margin-top: 25px; font-size: 12px; color: #999; text-align: center; }
</style>
</head>
<body>
<div class="container">
<h2>🏗️ 构建成功</h2>
<table class="info-table">
<tr><td class="label">项目</td><td class="value">${env.JOB_NAME}</td></tr>
<tr><td class="label">构建编号</td><td class="value">#${env.BUILD_NUMBER}</td></tr>
<tr><td class="label">状态</td><td class="value success">✅ 成功</td></tr>
<tr><td class="label">触发者</td><td class="value">${env.BUILD_USER_ID ?: '系统/定时触发'}</td></tr>
<tr><td class="label">耗时</td><td class="value">${currentBuild.durationString}</td></tr>
<tr><td class="label">详情</td><td class="value"><a href="${env.BUILD_URL}">${env.BUILD_URL}</a></td></tr>
</table>
<h3>📝 提交信息</h3>
<div class="commit-message">${commitInfo}</div>
<div class="footer">此邮件由 Jenkins 自动发送</div>
</div>
</body>
</html>
""",
mimeType: 'text/html'
)
}
}
failure {
echo '构建失败,请检查日志。'
}
}
}
5.3 解决 Git 安全目录问题
在 B 上,以 Jenkins 服务运行账户(通常是 SYSTEM)执行:
cmd
git config --global --add safe.directory D:/Share/project/your_project
否则在获取提交信息时会报 detected dubious ownership 错误。
5.4 配置邮件通知(Extended E-mail)
- 进入 Jenkins 系统管理 → 系统配置
- 在 Extended E-mail Notification 区域填写 SMTP 服务器、端口、SSL、认证(用户名+授权码)
- 保存,测试发送(点击"通过发送测试邮件测试配置")
注意:此配置与"E-mail Notification"独立,Pipeline 中的 emailext 使用前者。
六、踩坑记录与解决方案
6.1 触发构建返回 403 No valid crumb
- 原因:未携带 CSRF Crumb 或未认证。
- 解决 :使用 API Token + Basic Auth 发起请求(见脚本),绕过 CSRF。
6.2 邮件发送失败,但全局测试成功
- 原因 :
emailext使用的是"Extended E-mail Notification"配置,而非"E-mail Notification"。 - 解决:填写 Extended E-mail Notification 的 SMTP 参数,保持一致。
6.3 任务计划程序运行时弹出 PowerShell 窗口
- 解决 :使用 VBS 包装器,以
0隐藏窗口启动,并设置"不管用户是否登录都要运行"。
6.4 邮件内容含命令回显(如 C:\...>git -C)
- 原因 :
bat步骤的returnStdout会包含命令自身。 - 解决 :改用
powershell步骤执行git log,无回显。
6.5 Git 目录安全归属错误
- 解决 :在 Jenkins 节点上执行
git config --global --add safe.directory <绝对路径>。
七、最终效果
- C 电脑每 5 分钟检测一次 Git 更新,有变化则自动拉取并触发 Jenkins 构建。
- Jenkins 构建过程完全自动化,构建成功后发送排版精美的 HTML 邮件,包含提交作者、提交信息、构建链接等。
- 整个过程 C 电脑无可见窗口,资源占用极低,满足内网隔离环境要求。
八、扩展建议
- 若需将产物部署到服务器 A,可在 Pipeline 中添加 SCP 步骤(B 能连 A)。
- 可增加构建失败邮件通知,在
post failure中配置。 - 若项目依赖私有 npm 包,可在 B 上配置
.npmrc或使用镜像。 - 建议将 Jenkins 配置为 HTTPS,提高安全性。
九、总结
通过巧妙利用 Windows 共享目录、PowerShell 定时任务、Jenkins Pipeline 和 API Token 认证,我们实现了一套低成本的自动化 CI 流程。该方案无需复杂网络穿透,适用于大多数内外网隔离场景。希望本文能帮助到有类似需求的同行。
本文所有脚本均已脱敏,可根据实际环境替换 IP、用户名、路径、Token 等信息。
如有疑问,欢迎在评论区交流。
