实战:内外网隔离环境下基于 Jenkins + PowerShell 的自动化 CI 构建与邮件通知方案

场景:企业内网隔离、开发机不能直连 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.jspnpm/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 任务

  1. 新建任务 → 输入名称 your_project → 选择 Pipeline
  2. 构建触发器 中勾选 "触发远程构建" ,设置 Token,例如 auto-build
  3. 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 等信息。

如有疑问,欢迎在评论区交流。

相关推荐
k4m7v2pz38 分钟前
多开云网页操作指南(供豆包云电脑 / 自动化 Agent 执行)
运维·自动化·电脑
天空属于哈夫克339 分钟前
企业微信API:RPA自动化接口开发实践
自动化·企业微信·rpa
dunge202644 分钟前
ChatGPT Plus / Pro + Codex 深度实战指南:2026年9月2日 从智能编程到自动化工作流的完整技术手册
运维·chatgpt·自动化
tianyuanwo13 小时前
Jenkins Pipeline 沙箱与非沙箱:CPS、序列化与白名单的深度解读(含 `take()` 与 `subList()` 实战对比)
pipeline·jenkins·groovy·take·sublist
代码方舟15 小时前
零信任架构实战:基于天远车辆估值构建自动化二手车评估网关
运维·人工智能·架构·自动化
xing251617 小时前
Playwright Python浏览器自动化实战
自动化
代码方舟18 小时前
零信任架构实战:基于天远车辆过户详版查询构建自动化车辆估值网关
运维·人工智能·架构·自动化
MBD进行时19 小时前
生产制造 & 质量检测自动化:MBDVidia 检查修复产品 MBD 数据有效改进工业制造企业上下游协作!
自动化·制造·mbd模型质量管理·pmi自动检查与修复·mbdvidia·mbd数据检查与修复·工业制造企业上下游协作
liuyicenysabel19 小时前
从 0 到 1:一套 GitHub + GHCR + k3s 的全自动 CI/CD 流水线(Flask 项目实战)
ci/cd·flask·github