Administrator
发布于 2025-06-13 / 6 阅读
0
0

用 Jenkins 打造前端热部署流水线:Publish Over SSH + Discord Webhook 的实战笔记

在前后端分离、电商业务频繁变更的场景下,仅靠手工打包、手工上传、手工重启容器很难支撑快速迭代,也无法满足对可观测性和反馈速度的要求。 这篇文章围绕一个真实的 B2C 前端项目,展示如何用 Jenkins Pipeline 打造“一键热部署 + Discord 实时通知”的前端部署流水线。

通过将流水线写成pipeline,不仅可以版本化管理 CI/CD 流程,还能将工程化能力显式地沉淀在仓库中,方便团队复用和评审。

以下主要解决三件事:自动构建产物、将产物安全地分发到远端环境、以及在构建结束后主动推送通知到 discord。pipeline作为主干结构,保持了配置的可读性,同时用少量 script 区块处理 shell 命令和 JSON 组装等“不可避免的脚本化逻辑”。

pipeline {
    agent { label 'built' }

    environment {
        TAR_PKG_NAME                 = 'storefront.tar.gz'
        REMOTE_MACHINE_CONFIG_NAME   = 'dev-52'
        BASE_DIR                     = '/opt/dev'
        WORK_DIR                     = '/opt/dev/yc/mall-b2c-front'
        DOCKER_COMPOSE_NAME          = 'docker-compose.yaml'
        WEBHOOK_URL                  = "https://discord.com/api/webhooks/xxxx/xxxx" // 示例占位
        TITLE                        = "b2c_frontend_dev_hot_deploy"
        PROXY_URL                    = "http://192.168.2.30:10808"
        GIT_REPOSITORY               = "http://gitea.repository.yc/mall-b2c/mall-b2c-front.git"
        GIT_REPOSITORY_CREDENTIALID  = "gitea_devops_pwd"
        GIT_REPOSITORY_BRANCH        = "dev"
    }

    stages {
        stage('Checkout') {
            steps {
                git branch: "${GIT_REPOSITORY_BRANCH}",
                    credentialsId: "${GIT_REPOSITORY_CREDENTIALID}",
                    url: "${GIT_REPOSITORY}"
            }
        }

        stage('Package') {
            steps {
                sh """
                    tar -czvf ${TAR_PKG_NAME} \\
                        --exclude=.git \\
                        --exclude=${TAR_PKG_NAME} \\
                        ./
                """
            }
        }

        stage('delivery and deploy') {
            steps {
                sshPublisher(publishers: [
                    sshPublisherDesc(
                        configName: "${REMOTE_MACHINE_CONFIG_NAME}",
                        transfers: [
                            sshTransfer(
                                cleanRemote: false,
                                excludes: '.git/**',
                                execCommand: """
mkdir -p ${WORK_DIR}
cd ${BASE_DIR}
tar -xf ${TAR_PKG_NAME} --strip-components=1 -C ${WORK_DIR}
cd ${WORK_DIR}
docker cp src storefront-dev:/app/
docker compose restart
""",
                                execTimeout: 1800000,
                                flatten: false,
                                makeEmptyDirs: false,
                                noDefaultExcludes: false,
                                patternSeparator: '[, ]+',
                                remoteDirectory: '',
                                remoteDirectorySDF: false,
                                removePrefix: '',
                                sourceFiles: "${TAR_PKG_NAME}"
                            )
                        ],
                        usePromotionTimestamp: false,
                        useWorkspaceInPromotion: false,
                        verbose: true
                    )
                ])
            }
        }
    }

    post {
        success {
            script {
                sendDiscordNotification('success')
            }
        }

        failure {
            script {
                sendDiscordNotification('failure')
            }
        }
    }
}

def sendDiscordNotification(String status) {
    script {
        try {
            def epoch         = sh(script: 'date +%s', returnStdout: true).trim()
            def commitMessage = sh(script: 'git show -s --pretty=%s', returnStdout: true).trim()
            def commitUser    = sh(script: 'git show --format="%an <%ae>" --no-patch', returnStdout: true).trim()

            // 转义特殊字符,避免 JSON 解析错误
            commitMessage = commitMessage.replace('"', '\\"').replace('\n', '\\n')
            commitUser    = commitUser.replace('"', '\\"')

            def color      = status == 'success' ? '3066993'  : '15158332'
            def emoji      = status == 'success' ? '🎉'       : '❌'
            def statusText = status == 'success' ? '✅ Success' : '❌ Failed'
            def actionText = status == 'success' ? '热部署完成' : '热部署失败'

            def jsonFile = 'discord_payload.json'
            writeFile file: jsonFile, text: """
{
  "content": "@everyone",
  "username": "CI · Build Bot",
  "avatar_url": "https://i.imgur.com/your-bot-avatar.png",
  "embeds": [
    {
      "title": "${emoji} ${TITLE} ${actionText}",
      "description": "**状态:** ${statusText}\\n**结束时间:** <t:${epoch}:F>\\n**提交描述:** ${commitMessage}\\n**提交用户:** ${commitUser}",
      "color": ${color}
    }
  ]
}
"""

            echo "发送 Discord 通知..."
            echo "----------------------------------------------------------------------------------"

            def curlResult = sh(
                script: """
                    curl --connect-timeout 10 --retry 5 --retry-all-errors --retry-delay 2 \\
                         --proxy "${PROXY_URL}" \\
                         -s -w "%{http_code}" -o /tmp/discord_response.txt \\
                         -X POST \\
                         -H "Content-Type: application/json" \\
                         -d @${jsonFile} \\
                         "${WEBHOOK_URL}"
                """,
                returnStdout: true
            ).trim()

            if (curlResult == "200" || curlResult == "204") {
                echo "✅ Discord 通知发送成功 (HTTP ${curlResult})"
            } else {
                error "❌ Discord 通知发送失败 (HTTP ${curlResult})"
            }

            sh "rm -f ${jsonFile} /tmp/discord_response.txt"
        } catch (Exception e) {
            echo "⚠️ Discord 通知发送失败: ${e.getMessage()}"
            echo "继续执行流水线..."
        }
    }
}

基于当前 Pipeline,很容易进一步增强 devops 能力,比如加入测试阶段,只允许所有用例通过后才打包上传;再比如将环境信息参数化,让同一套 Jenkinsfile 通过参数即可部署到 dev / staging / prod 多个环境。


评论