告别手动构建!Jenkins 与 Gitlab 完美协作,根据参数自动化触发CI/CD流水线实践

发布于:2025-09-15 ⋅ 阅读:(21) ⋅ 点赞:(0)

[ 知识是人生的灯塔,只有不断学习,才能照亮前行的道路 ]

📢 大家好,我是 WeiyiGeek,一名深耕安全运维开发(SecOpsDev)领域的技术从业者,致力于探索DevOps与安全的融合(DevSecOps),自动化运维工具开发与实践,企业网络安全防护,欢迎各位道友一起学习交流、一起进步 🚀,若此文对你有帮助,一定记得倒点个关注⭐与小红星❤️,收藏学习不迷路 😋 。

  • 0x00 前言简述

    • 设计方案

  • 0x01 方案实战

    • 环境准备

    • Jenkins Generic Webhook Trigger 配置

    • GitLab 配置

    • Jenkins Pipeline 流水线

    • 测试验证

0x00 前言简述

描述: 目前我们使用的是Jenkins+ArgoCD做为CI/CD的工具,Jenkins 作为一个老牌的CI/CD工具,拥有众多功能强大的插件,在业界有着广泛的应用。随着我司开发业务越来越多,需要持续集成和持续部署的场景越来越多,原本通过手动(半自动)的触发Jenkins Pipeline的方式已经不能满足需求,因此希望通过开发上传代码到Gitlab时,根据一定的规则来实现触发流水线,并且根据获取到的参数进行自动化选择测试环境与正式环境的进行构建,并利用企业微信进行项目构建消息通知。

正是基于上述需求,我参考了网上的一些资料,并结合我司的实际需求,设计了两种方案来实现基于Gitlab Webhook触发 Jenkins Pipeline 的需求,希望对各位看友有帮助。

温馨提示:作者在本文实践的Jenkins Pipeline 流水线可在我《全栈工程师修炼指南》知识星球(文末:点击 阅读原文)中获取,欢迎各位看友加入知识星球与我一起学习交流。

效果展示,自动触发构建:

6f7565fbbfb72b9e46d65236a58d832d.png

手动触发构建:

0ab0490419adeb0e7c95e7b44b86c66b.png

构建消息通知:

285a0926c150a4da10d167436ff32bee.png

设计方案

方案1.使用Gitlab集成功能对接 Jenkins gitlab-plugin 插件实现自动构建

Jenkins 中的 GitLab Plugin 🛠️ 是一个方便 Jenkins 和 GitLab 协同工作的桥梁,它主要实现了自动化构建触发和构建状态回传两大核心功能,能帮助你更好地实践 CI/CD。

插件文档:https://plugins.jenkins.io/gitlab-plugin

GitLab Plugin 核心功能 🔌:

  • 自动化构建触发:当 GitLab 仓库中发生特定事件(如推送代码、创建合并请求等)时,GitLab 会通过 Webhook 通知 Jenkins,从而自动触发 Jenkins 的构建任务

  • 构建状态反馈:Jenkins 在构建结束后,可以将构建状态(成功、失败等)通过 GitLab API 回写到对应的 GitLab 提交或合并请求上。这样,开发团队可以直接在 GitLab 的界面上清晰地了解每次提交或合并请求的构建结果

  • 支持丰富的事件触发:插件支持由多种 GitLab 事件触发构建,例如推送代码、创建合并请求、评论等

  • 提供过滤与优化选项:插件提供了灵活的配置选项以精细化控制构建触发,例如:通过分支名、提交者等信息过滤构建触发条件,或者根据特定的标签(如 [ci skip])来决定是否忽略某些推送事件等(⚠️ 特别注意此处,这将是作者选择方案2的原因

简单的说,Jenkins 的 GitLab Plugin 是衔接 Jenkins 和 GitLab 的关键插件。它通过自动化触发构建和实时反馈状态,有力地支撑了持续集成和持续交付流程,帮助开发团队提升效率和代码质量。

GitLab Plugin 插件是可以直接与 Github 集成使用的,首先,你需要自行安装插件(很简单、不累述)并在 Pipeline 流水线中启用配置相关参数,例如:在发生 Push 事件时触发构建,或者在创建合并请求,或根据提交的评论关键字进行自动运行测试等。

图片.png

 w eiyigeek.top-gitlab-plugin插件图

启用后,Jenkins 会监听来自 GitLab (实践版本:v17.8.4) 的 Webhook 事件,然后还需在 Gitlab 项目中通过 Jenkins 集成配置 Webhook,以便 Jenkins 能够接收到来自 GitLab 的构建触发事件。

fe5299a1336dce9d9a8564c324e84ba8.png
weiyigeek.top-Gitlab集成Jenkins图

首先,配置触发器满足触发条件时(例如:代码被推送到仓库、标签推送),其次配置Jenkins 服务器,流水线名称(注意需和Jenkins流水线名称一致),账号密码,最后保存更改并可测试设置是否生效,Jenkins 将自动执行构建任务。

28f27ad628ce477930fcc9bf2205acd6.png weiyigeek.top-Jenkins集成配置图

是不是非常方便对吗?所以这也是也是作者最初选择的方案,当然,你也可以通过为上传代码打标签(Tag)时触发测试或正式环境流水线,例如:标签v版本号-RELEASE表示触发正式环境流水线,而使用标签v版本号-TEST表示触发测试环境流水线,实际上其正式或测试环境的选择可通过 gitlab-plugin 插件内置的 gitlabSourceBranch 变量来获取标签名称,再通过-符号分隔字符串截取TEST或者RELEASE关键字,再到流水线中进行逻辑判断或判断是否包含,然后根据不同的环境配置构建参数。

不过随着学习实践的深入,作者发现,此插件在某些场景下并不能满足需求:

场景1,在使用代码打标签事件触发时,每次打标签都会触发流水线,而当不同开发人员没有统一提交代码的标签格式时,流水线肯定会报错,这显然不是我们想要的结果。

场景2:我想要实现当代码推送时,根据特定的规则(例如:在提交信息中包含 [ ci-test ] 或者 [ ci-prod ] 关键字时触发构建,但是GitLab Plugin并不能直接支持这样的需求。

因此,我需要一个更灵活的方式来控制构建的触发条件。例如:我希望在推送代码时根据提交信息中的关键字来决定是触发测试环境还是生产环境的构建流程,或者说,只有在使用特定标签时才触发构建,而不是每次推送代码都触发流水线构建,这样保证流水线的健壮性,以及养成开发人员打Tag的习惯。

所以,我采用下面方案2来实现更精细化的自动构建控制,后续方案实践中也将采用方案2,在后续文章中会有详细介绍。


方案2.使用 Gitlab Webhook 功能对接 Jenkins Generic Webhook Trigger Plugin 插件实现更精细化的自动构建

描述:Jenkins 的 Generic Webhook Trigger Plugin 是一款非常强大且灵活的插件,它允许你通过接收来自任何外部系统的 HTTP Webhook 请求(通常包含 JSON 或 XML 格式的负载数据)来触发 Jenkins 项目的构建,并能根据预设规则进行精细化控制。这意味着它不仅能对接 GitLab、GitHub、Gitee 等代码托管平台,还能与任何能发送 Webhook 的系统集成。

GitLab Plugin 核心功能 🔌:

  • 灵活提取请求数据:件支持从 HTTP POST 请求的 Body(支持 JSONPath 或 XPath 表达式)、URL 参数以及 Header 中提取特定的值,并将其转化为 Jenkins 任务的参数或环境变量,这使得你可以利用 Webhook 负载中的丰富信息(如分支名、提交ID、项目名等)。

  • 精确触发控制:你可以配置过滤规则(通常基于正则表达式),只有满足条件的请求才会触发构建。例如,可以设置仅当推送特定分支(如 refs/heads/master)或发生特定事件(如 Push Events)时才触发构建,避免不必要的构建。

  • 支持自定义 Token 安全验证:你可以为 Jenkins 任务配置一个 Token。外部系统在调用 Webhook 时需提供此 Token(可通过查询参数、Header 等方式),从而确保只有授权的请求才能触发任务,增强安全性

  • 调试与日志记录:插件支持打印贡献的变量(打印提取出的参数值)和打印 POST 内容(打印接收到的原始负载),这极大方便了配置和调试过程中的问题排查。

简单的说,Jenkins 的 Generic Webhook Trigger Plugin 的核心价值在于其通用性和灵活性。它几乎能将任何能够发送 Webhook 的系统与 Jenkins 的 CI/CD 流程无缝衔接,实现高度自定义的自动化构建触发。无论是常见的代码托管平台,还是各类自动化工具、监控系统或自定义脚本,它都能很好地胜任“桥梁”工作。

Generic Webhook Trigger Plugin 插件文档:https://plugins.jenkins.io/generic-webhook-trigger

0x01 方案实战

描述:作者将以方案二为例,详细介绍如何通过 GitLab Webhook 触发 Jenkins Pipeline 的构建,并根据不同的提交信息或标签来选择触发测试环境还是生产环境的构建流程,此外除了由Gitlab触发构建外,还可通过手动选择参数进行构建。这将帮助你实现更精细化的自动化控制,提升 CI/CD 流程的灵活性和效率。

其次,授人以鱼不如授人以渔,作者将以重要的几点进行讲解,帮助你快速上手,大家也可举一反三,根据自身实际需求出发进行改造,需要作者企业实践的 Jenkins Pipeline 流水线可在我《全栈工程师修炼指南》知识星球中获取到,连接直达:https://articles.zsxq.com/id_gzoww51ghekx.html

若还有不了解 jenkins 的童鞋作者也提供了快速入门指南,包含作者从初学到打怪的记录,点击访问 Jenkins 学习指南专栏 。

环境准备

操作系统:KylinOS V10 SP3 已安全加固(满足等保 2.0 主机安全),加固文档:网安等保 | 主机安全之KylinOS银河麒麟服务器配置优化与安全加固基线文档脚本分享

Docker:docker-ce-24.0.4 请参考下面 Harbor 或者 Gitlab 安装文档,或者参考官方文档快速安装,很简单的我就不老生常谈了,若还有不了解 Docker 的童鞋作者也提供了快速入门指南,可关注公众号【全栈工程师修炼指南】回复【docker】关键字获取相关资料。

Harbor: v2.8.2-d4c34dcc GitOps实践 | 快速在银河麒麟KylinOS国产系统部署最新Harbor企业私有镜像仓库

Gitlab: v17.8.4  GitOps实践 | 快速在银河麒麟KylinOS国产系统部署最新Gitlab-CE企业私有代码仓库

Jenkins: Jenkins 2.387.3 ,注意自行构建的 jenkins-agent 需要运行在JDK11以上,作者当时升级后到CI/CD不能用,只能重新构建agent镜像,当前极度痛苦,此处建议使用 docker-compose 工具进行部署 Jenkins server。

# 创建持久化目录以及证书目录
mkdir -vp /storage/nfs/docker/jenkins/{data,cert}

# 资源清单
tee docker-compose.yml <<'EOF'
# Docker deploy Jenkins Server
version: '3.2'
services:
  jenkins:
    image: jenkins/jenkins:2.387.3-alpine
    container_name: jenkins_server
    user: jenkins
    labels:
      - "app=jenkins"
      - "author=WeiyiGeek"
    environment:
      TZ: "Asia/Shanghai"
      JAVA_OPTS: "-XshowSettings:vm -Dhudson.slaves.NodeProvisioner.initialDelay=0 -Dhudson.slaves.NodeProvisioner.MARGIN=50 -Dhudson.slaves.NodeProvisioner.MARGIN0=0.85 -Duser.timezone=Asia/Shanghai -Dhudson.footerURL=https://www.weiyigeek.top -Dfile.encoding=UTF-8"
      JENKINS_OPTS: "--httpPort=8080 --httpsPort=443 --httpsCertificate=/var/lib/jenkins/pki/weiyigeek.top.pem --httpsPrivateKey=/var/lib/jenkins/pki/weiyigeek.top.key"
      JENKINS_SLAVE_AGENT_PORT: 50000
    volumes:
      - /storage/nfs/docker/jenkins/data:/var/jenkins_home
      - /storage/nfs/docker/jenkins/cert:/var/lib/jenkins/pki
      - /etc/localtime:/etc/localtime
    extra_hosts:
      - "jenkins.weiyigeek.top:127.0.0.1"
    ports:
      - '8080:8080'
      - '443:443'
      - '50000:50000'
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/login"]
      interval: 2m30s
      timeout: 10s
      retries: 3
    restart: always
    dns:
      - 223.6.6.6
EOF

# 目录权限
chown -R 1000:1000 /storage/nfs/docker/jenkins/data /opt/jenkins/cert

# 运行
docker-compose up -d

# 获取初始化密码
docker logs -f --tail 50 jenkins_server
# Jenkins initial setup is required. An admin user has been created and a password generated.
# Please use the following password to proceed to installation:
# 48bd96655888485fb8bece064a6657bf

温馨提示:最后设置 hosts 硬解析后访问类似 https://jenkins.weiyigeek.top/login?from=%2F 地址进行 jenkins 初始化。

温馨提示: 此处笔者采用的通配符证书(weiyigeek.top.pem 、 weiyigeek.top.key)分别使用 --httpsCertificate 和 --httpsPrivateKey 进行指定。

Jenkins-Agent: v2.387.3-alpine 零基础的朋友可参考 使用Docker运行自构建Jenkins的Agent镜像固定工作节点实践 文章。

# 准备自定义的Jenkins Agent镜像的 Dockerfile 文件
tee ~/build/Dockerfile <<'EOF'
#----------------------------------------------------------------------#
# Title: Base in Alpine Images Create Custom Jenkins Kubernetes jnlp Images
# Author: WeiyiGeek
# Index: https://weiyigeek.top
# Email: mastr@weiyigeek.top
# 微信公众号:【全栈工程师修炼指南】
# Release: v1.12
# Image Version: alpine-3.16
# MainFunction:
#   Install ssh-server docker git openssh tzdata curl tar sudo git ca-certificates wget unzip docker zlib nodejs npm jq
#   Install OpenJDK8 OpenJDK11
#   - https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html (丢弃Oracle JDK)
#   Install alpine-pkg-glibc 2.35-r1
#   - https://github.com/sgerrand/alpine-pkg-glibc/releases/
#   - https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
#   Install jnlp Version: 4.14 (两种方式都可以下载agent 或者 remoting)
#   - https://repo.jenkins-ci.org/public/org/jenkins-ci/main/remoting/remoting-4.14.jar
#   - http://youjenkins-domainname/jnlpJars/agent.jar
#   Install Maven Version: 3.9.4
#   - https://apache.osuosl.org/maven/maven-3/
#   Install Gitlab Release Version: 0.15.0
#   - https://gitlab.com/gitlab-org/release-cli/-/releases/v0.15.0/downloads/bin/release-cli-linux-amd64
#   Install kubernetes cli
#   - kubectl Version: 1.23.17
#   Install docker cli
#   - kubectl Version: 20.10.3
# ChangeLog:
# v1.8 - 增加 docker
# v1.9 -  增加 中文环境
# v1.10 - 增加 node.js 环境支持
# v1.11 - 更新依赖软件版本及其agent.jar版本
# v1.12 - 更新 Jenkins、OpenJDK8/11 以及 alpine-pkg-glibc 版本,支持tini启动
#-------------------------------------------------#
FROM alpine:3.18

LABEL AUTHOR="WeiyiGeek" EMAIL="master@weiyigeek.top" DESC="Jenkins-Work-Agent" VERSION="v1.12"

# 构建参数
ARG USERNAME=jenkins \
    AGENT_HOME=/home/jenkins \
    AGENT_WORKDIR=/home/jenkins/agent \
    BASE_DIR=/usr/local  \
    BASE_BIN=/usr/local/bin  \
    BASE_URL=http://192.168.1.107:8080  \
    LOCALE=locale.md \
    JDK_NAME=jdk-8u381-linux-x64   \
    JDK_DIR=/usr/local/jdk1.8.0_381  \
    GLIBC_NAME=glibc-2.35-r1.apk \
    GLIBC_BIN_NAME=glibc-bin-2.35-r1.apk \
    GLIBC_I18N_NAME=glibc-i18n-2.35-r1.apk \
    MAVEN_NAME=apache-maven-3.9.4-bin \
    MAVEN_DIR=/usr/local/apache-maven-3.9.4 \
    GITLAB_CLI=release-cli-0.15.0-linux-amd64 \
    SSH_PUBLIC=id_ed25519 \
    SSH_PRIVATE=id_ed25519.pub

# 构建变量
ENV LANG=en_US.UTF-8\
    LC_ALL=en_US.UTF-8\
    JAVA_HOME=/usr/lib/jvm/java-11-openjdk \
    JRE_HOME=/usr/lib/jvm/java-11-openjdk/jre \
    JAVA8_HOME=/usr/lib/jvm/java-8-openjdk \
    JRE8_HOME=/usr/lib/jvm/java-8-openjdk/jre \
    MAVEN_HOME=/usr/local/maven \
    MAVEN_RPEO=/home/jenkins/.m2 \
    SONAR_SCANNER_HOME=/usr/local/sonar-scanner \
    NODEJS_MODULES=/usr/lib/node_modules

# 用户ROOT切换
USER root

# 执行命令,使用方式极大减少了构建的镜像大小
RUN sed -i 's/dl-cdn.alpinelinux.org/mirror.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \
    && apk update \
    && apk add --no-cache openssh tzdata curl wget tar sudo git git-subtree ca-certificates unzip docker-cli zlib nodejs npm jq openjdk11 openjdk8 tini \
    && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
    && chmod 4755 /bin/busybox \
    && mkdir -vp ${AGENT_WORKDIR}${AGENT_HOME}/.ssh ${AGENT_HOME}/.m2  \
    && addgroup -g 1000 -S ${USERNAME} \
    && adduser ${USERNAME} -D -g ${USERNAME} -G root -u 1000 -s /bin/sh \
    && echo"jenkins   ALL=(root) NOPASSWD:ALL" >> /etc/sudoers \
    && echo"root:WeiyiGeek" | chpasswd \
    && echo"jenkins:WeiyiGeek" | chpasswd \
    && wget -q -O /tmp/${GLIBC_NAME}${BASE_URL}/${GLIBC_NAME} \
    && wget -q -O /tmp/${GLIBC_BIN_NAME}${BASE_URL}/${GLIBC_BIN_NAME} \
    && wget -q -O /tmp/${GLIBC_I18N_NAME}${BASE_URL}/${GLIBC_I18N_NAME} \
    && wget -q -O /etc/apk/keys/sgerrand.rsa.pub ${BASE_URL}/sgerrand.rsa.pub \
    && wget -q -O /tmp/${LOCALE}${BASE_URL}/${LOCALE} \
    && wget -q -O ${BASE_BIN}/agent.jar ${BASE_URL}/agent.jar \
    && curl -fsSL -o ${BASE_BIN}/jenkins-agent.sh ${BASE_URL}/jenkins-agent.sh \
    && curl -fsSL -o /tmp/${MAVEN_NAME}.tar.gz ${BASE_URL}/${MAVEN_NAME}.tar.gz \
    && curl -fsSL -o /usr/local/bin/release-cli ${BASE_URL}/${GITLAB_CLI} \
    && curl -fsSL -o /usr/local/bin/kubectl ${BASE_URL}/kubectl \
    && curl -fsSL -o ${AGENT_HOME}/.ssh/id_ed25519 ${BASE_URL}/id_ed25519 \
    && curl -fsSL -o ${AGENT_HOME}/.ssh/id_ed25519.pub ${BASE_URL}/id_ed25519.pub \
    && apk add /tmp/${GLIBC_NAME} /tmp/${GLIBC_BIN_NAME} /tmp/${GLIBC_I18N_NAME} \
    && tar -zxf /tmp/${MAVEN_NAME}.tar.gz -C ${BASE_DIR} \
    && mv ${MAVEN_DIR}${MAVEN_HOME} \
    && npm config set registry https://registry.npm.taobao.org \
    && sed -i "s/#PermitRootLogin.*/PermitRootLogin yes/g" /etc/ssh/sshd_config \
    && sed -i "s/^#\s*StrictHostKeyChecking ask/StrictHostKeyChecking no/g" /etc/ssh/ssh_config \
    && cp -a /usr/local/maven/bin/mvn /usr/local/maven/bin/mvn8 \
    && sed -i "2a JAVA_HOME=${JAVA8_HOME}" /usr/local/maven/bin/mvn8 \
    && sed -i "3a JRE_HOME=${JRE8_HOME}" /usr/local/maven/bin/mvn8 \
    && ssh-keygen -t dsa -P "" -f /etc/ssh/ssh_host_dsa_key \
    && ssh-keygen -t rsa -P "" -f /etc/ssh/ssh_host_rsa_key \
    && ssh-keygen -t ecdsa -P "" -f /etc/ssh/ssh_host_ecdsa_key \
    && ssh-keygen -t ed25519 -P "" -f /etc/ssh/ssh_host_ed25519_key \
    && gunzip /usr/glibc-compat/share/i18n/charmaps/UTF-8.gz \
    && cat /tmp/${LOCALE} | xargs -i /usr/glibc-compat/bin/localedef -i {} -f UTF-8 {}.UTF-8 \
    && echo"export LANG=zh_CN.UTF-8" > /etc/profile.d/locale.sh \
    && chmod a+x ${BASE_BIN}/* \
    && chown -R jenkins:jenkins ${AGENT_HOME}${BASE_DIR}/ ${AGENT_WORKDIR}/ ${NODEJS_MODULES}/ \
    && chmod 600 ${AGENT_HOME}/.ssh/id_ed25519 \
    && chmod 644 ${AGENT_HOME}/.ssh/id_ed25519.pub \
    && rm -rf /var/cache/apk/* /tmp/* 

USER jenkins

WORKDIR ${AGENT_WORKDIR}

ENV CLASSPATH=.:${JAVA_HOME}/lib/dt.jar:${JAVA_HOME}/lib/tools.jar \
    PATH=${JAVA_HOME}/bin:${JRE_HOME}/bin:${MAVEN_HOME}/bin:$PATH

# 此处使用 tini 进行执行 jenkins-agent.sh 脚本,后续可以使用java相关性能工具,值得学习借鉴
ENTRYPOINT ["/sbin/tini","--","/usr/local/bin/jenkins-agent.sh"]
EOF

使用cd命令进入到~/build(此处是我自定义存放的目录)目录中,使用 docker build 进行构建。

$ cd ~/build
$ docker build -t weiyigeek/jenkins-inbound-agent:v2.387.3-alpine -t harbor.weiyigeek.top/devops/jenkins-inbound-agent:v2.387.3-alpine .

温馨提示: 此处除了使用Docker进行构建也可使用kibana工具,【使用Kaniko直接在K8S集群或Containerd环境构建推送容器镜像】[https://mp.weixin.qq.com/s/wchtH6i0xKrIrqSuYKmWkg]

温馨提示:在alpine中使用glibc-i18n安装支持中文时执行/usr/glibc-compat/bin/localedef出现错误,解决参考[https://github.com/sgerrand/alpine-pkg-glibc/issues/167]

看构建出的 jenkins-inbound-agent 镜像并且上传构建出的镜像到Harbor私有仓库中。

$ docker images | grep "weiyigeek"
weiyigeek/jenkins-inbound-agent                      v2.387.3-alpine        0b38c223091f   5 hours ago     591MB
harbor.weiyigeek.top/devops/jenkins-inbound-agent    v2.387.3-alpine        0b38c223091f   5 hours ago     591MB

$ docker push harbor.weiyigeek.top/devops/jenkins-inbound-agent:v2.387.3-alpine
3605305cf716bf2853a2cd6906c327fd.png
weiyigeek.top-查看上传到Harbor中的jenkins-inbound-agent镜像图

温馨提示: 构建的 Jenkins-Agent 镜像作者已经上传到Docker Hub中,需要的朋友请自行拉取下载weiyigeek/jenkins-inbound-agent:v2.387.3-alpinen

使用 Docker 命令快速启动 Jenkins-Agent 容器实例,注意修改下面相关参数,以及实际的工作目录、maven 缓存目录等、Jenkins 密钥等信息。

$ docker run --user jenkins -d --name docker-jenkins-agent \
-e "JAVA_OPTS=-Xms512m -Xmx2g -Xss1m" \
-e "JENKINS_NAME=docker-jenkins-agent" \
-e "JENKINS_AGENT_NAME=docker-jenkins-agent" \
-e "JENKINS_SECRET=296f0ac949*******779e7efbf1f7bc2b477" \
-e "JENKINS_AGENT_WORKDIR=/home/jenkins/agent" \
-e "JENKINS_DIRECT_CONNECTION=192.168.1.107:50000" \
-e "JENKINS_INSTANCE_IDENTITY=MIIBIj******2x4XktjEAYO3X0PWQIDAQAB" \
-w /home/jenkins \
-v /storage/dev/backup/build/.m2:/home/jenkins/.m2 \
-v /var/run/docker.sock:/var/run/docker.sock weiyigeek/jenkins-inbound-agent:v2.387.3-alpine

Jenkins Generic Webhook Trigger 配置

前面大致介绍了环境准备,接下来进入实战环节。首先,需要在 Jenkins 中安装并配置 Generic Webhook Trigger Plugin 插件或者 GitLab Plugin 插件(不使用提交信息过滤的情况下使用),这两个插件是实现自动化构建的关键组件。

如何安装配置 Generic Webhook Trigger Plugin?

步骤 01. 安装插件🛠️:在 Jenkins 的 "Manage Jenkins(系统管理)" -> "Manage Plugins(插件管理)" 中,点击 " Available plugins (可用插件)" 搜索并安装 "Generic Webhook Trigger Plugin",安装后如下所示

9f2f884a366ff801a7364fd1e5e3a0b1.png
weiyigeek.top-安装Generic Webhook Trigger Plugin图

步骤 02. 返回 Jenkins Dashboard 主页面,点击左侧菜单的 "新建" 按钮创建一个新的 Jenkins Pipeline(流水线) 项目,例如 weiyigeek-devops-cicd

94054dcc9744b197185ed135ae9b85c6.png
weiyigeek.top-创建流水线图

步骤 03.建议为项目配置丢弃旧的构建(缺省:轮询),以节约磁盘空间,例如:保持构建的天数为7天,而且保持构建的最大个数为10个。

1c33ecd26de378e29faf3a79ef4da7d6.png
weiyigeek.top-配置丢弃旧的构建图

步骤 04.在流水线配置中,找到 "构建触发器" 部分,勾选 "Generic Webhook Trigger"。根据你的需求,利用 Post content parameters ,使用 JSONPath 类型,从 Gitlab POST 请求中提取定义变量(后续在配置 Gitlab 阶段也将会罗列出请求的内容),例如从 JSON 负载中通过 $.ref 提取 ref 变量,以新增通过 $.user_name 提取提交的 user 开发者 和 通过 $.commits[-1].message 提取 msg 提交的信息)。

21e30e88bcb4b4633f8e779c016357cf.png
weiyigeek.top-提取所需的变量图

步骤 05.为了保障安全性,可以为 Webhook 配置一个 Token,然后在 GitLab 中设置相同的 Token,这样只有携带正确 Token 的请求才能触发 Jenkins 任务,作者建议将流水线名称设置为 Token,一是为了方便在流水线中自动引入不重复,二是为了在指定Gitlab项目配置触发cicd时可对应上对应要执行的流水线。

其提供两种方式进行认证,一种是直接请求 generic-webhook-trigger 时在 URL 后添加 ?token=自定义TOKEN_HERE,另一种是请求头中添加 token: 自定义 TOKEN_HERE 或者 Authorization: Bearer TOKEN_HERE

例如:作者设置的Token为项目名称:weiyigeek-devops-cicd,为了方便则在 Gitlab 中配置触发流水线的地址为 http://<你的Jenkins地址>p/generic-webhook-trigger/invoke?token=weiyigeek-devops-cicd

011d895661950c6ef38eb90bbdd8379b.png
weiyigeek.top-配置触发流水线的Token图

步骤 06.为实现方案目的,还需在 Optional filter 中配置过滤表达式,例如:设置过滤变量为 $msg 正则表达式'(?is).*(PROD|TEST).*' ,表示在提交的消息中包含 ci-test 或者 ci-prod 才触发构建。又例如:设置过滤变量为 $ref 正则表达式'(?s).*(-RELEASE|-TEST).*' ,表示在提交的标记名称中包含 -RELEASE 或者 -TEST 才触发构建。

e20213e5b1205c1cb21f2b6e3ac51e23.png
weiyigeek.top-设置触发流水线条件图

温馨提示:在编写自行定义提取触发器提交的数据时,为方便调试(Debug)建议开启插件的 Print post content 以及 Print contributed variables 选项,以在流水线中显示POST请求的原始负载和提取出的变量值。

知识扩展:除了通过手动在Jenkins中项目中配置 "Generic Webhook Trigger" 还可在 Jenkins  Pipeline 中,通过流水线 triggers 触发器来动态配置触发器,例如:

  • 脚本化 Jenkinsfile 示例:

node {
 properties([
  pipelineTriggers([
   [$class:'GenericTrigger',
    genericVariables: [
     [key:'ref', value:'$.ref'],
     [
      key:'before',
      value:'$.before',
      expressionType:'JSONPath', //Optional, defaults to JSONPath
      regexpFilter:'', //Optional, defaults to empty string
      defaultValue:''//Optional, defaults to empty string
     ]
    ],
    genericRequestVariables: [
     [key:'requestWithNumber', regexpFilter:'[^0-9]'],
     [key:'requestWithString', regexpFilter:'']
    ],
    genericHeaderVariables: [
     [key:'headerWithNumber', regexpFilter:'[^0-9]'],
     [key:'headerWithString', regexpFilter:'']
    ],

    causeString:'Triggered on $ref',

    token:'abc123',
    tokenCredentialId:'',

    printContributedVariables:true,
    printPostContent:true,

    silentResponse:false,
    
    shouldNotFlatten:false,

    regexpFilterText:'$ref',
    regexpFilterExpression:'refs/heads/' + BRANCH_NAME
   ]
  ])
 ])

 stage("build") {
  sh '''
  echo Variables from shell:
  echo ref $ref
  echo before $before
  echo requestWithNumber $requestWithNumber
  echo requestWithString $requestWithString
  echo headerwithnumber $headerwithnumber
  echo headerwithstring $headerwithstring
  '''
 }
}
59147a14bfc21ef3ab0494d60b9ac116.png
weiyigeek.top-脚本化 Jenkinsfile触发测试命令与结果图
  • 声明式 Jenkinsfile 示例:

pipeline {
  agent any
  triggers {
    GenericTrigger(
     genericVariables: [
      [key: 'ref', value: '$.ref']
     ],

     causeString: 'Triggered on $ref',

     token: 'abc123',
     tokenCredentialId: '',

     printContributedVariables: true,
     printPostContent: true,

     silentResponse: false,
     
     shouldNotFlatten: false,

     regexpFilterText: '$ref',
     regexpFilterExpression: 'refs/heads/' + BRANCH_NAME
    )
  }
  stages {
    stage('Some step') {
      steps {
        sh "echo $ref"
      }
    }
  }
}

至此,流水线通用 Webhook 触发器配置完成,接下来需要在 GitLab 项目中设置 Webhook。

GitLab 配置

描述:在 Jenkins 流水线项目中完成 "Generic Webhook Trigger" 配置后,便可在流水线对应 Gitlab 项目中配置流水线触发地址,以及触发条件请求构建的条件。

操作流程

步骤 01. 进入流水线对应 GitLab 项目,点击 "Settings(设置)" -> 点击 "Webhooks"。

5af99860210bc95a8aa13c2564db5c0c.png
weiyigeek.top-Gitlab Webhooks 配置图

步骤 02.在点击右边的 "Add New webhook (添加新的 webhook)" 按钮,在弹出的表单中填写 Webhook 的触发Jenkins 流水线的 URL 以及触发事件(例如:Push Tag events(标签推送事件)),若你的 Jenkins 服务器使用的是自签证书则需要取消SSL验证(特别注意),最后点击 "Add webhook"即可

6ee8df3ce36b555e95affd8c1e67bba7.png
weiyigeek.top-配置触发条件图

步骤 03.添加完成后,我们在项目 Webhooks 界面点击对应的 webhook 进行事件触发测试,若提示Hook executed successfully: HTTP 200则代表发送成功。

0ba54903bb470ff9f3178e7baf517e06.png
weiyigeek.top-测试触发推送图

步骤 04.再次点击添加的 webhook 滑动到末尾,便可查看到 GitLab 最近事件触发 webhook,点击 "查看详细信息",就可以查看发送给Jenkins的请求了,大家可根据需要提取数据用于在 Jenkins Pipeline 中,

  • 请求的 Headers

Content-Type: application/json
User-Agent: GitLab/17.8.4
Idempotency-Key: d9ac410e-8746-45b0-ba54-b4d6526efabd
X-Gitlab-Event: Tag Push Hook
X-Gitlab-Webhook-UUID: 0f97bb1d-7650-409a-8270-9293f043aaf1
X-Gitlab-Instance: https://git.weiyigeek.top
X-Gitlab-Event-UUID: 30fceeec-2f1d-48dd-8d06-0d6f2c2680ac
  • 请求的 Content

{
  "object_kind": "tag_push",
"event_name": "tag_push",
"before": "ee953270d290320e70e6d1e6a1783a6a33cabd6f",
"after": "170bbff4ac8f68fde4f65db15ccc6ed0a8a41a48",
"ref": "refs/tags/1.4-RELEASE", // Jenkins插件中提取ref变量,由此判断是否触发以及测试或正式环境流水线
"ref_protected": false,
"checkout_sha": "170bbff4ac8f68fde4f65db15ccc6ed0a8a41a48",
"message": null,
"user_id": 1,
"user_name": "全栈工程师修炼指南", // Jenkins 插件中提取 user 变量
"user_username": "weiyigeek",
"user_email": "",
"user_avatar": null,
"project_id": 121,
"project": {
    "id": 121,
    "name": "cicd",
    "description": null,
    "web_url": "https://git.weiyigeek.top/devops/cicd",
    "avatar_url": null,
    "git_ssh_url": "ssh://git@git.weiyigeek.top/devops/cicd.git",
    "git_http_url": "https://git.weiyigeek.top/devops/cicd.git",
    "namespace": "devops",
    "visibility_level": 0,
    "path_with_namespace": "devops/cicd",
    "default_branch": "master",
    "ci_config_path": "",
    "homepage": "https://git.weiyigeek.top/devops/cicd",
    "url": "ssh://git@git.weiyigeek.top/devops/cicd.git",
    "ssh_url": "ssh://git@git.weiyigeek.top/devops/cicd.git",
    "http_url": "https://git.weiyigeek.top/devops/cicd.git"
  },
"commits": [
    {
      "id": "170bbff4ac8f68fde4f65db15ccc6ed0a8a41a48",
      "message": "ci-prod 测试", // Jenkins 插件中提取 msg 变量
      "title": "ci-prod 测试",
      "timestamp": "2025-09-04T16:50:40+08:00",
      "url": "https://git.weiyigeek.top/devops/cicd/-/commit/170bbff4ac8f68fde4f65db15ccc6ed0a8a41a48",
      "author": {
        "name": "weiyigeek",
        "email": "[REDACTED]"
      },
      "added": [

      ],
      "modified": [
        "test.cicd"
      ],
      "removed": [

      ]
    },
"total_commits_count": 3,
"push_options": {
  },
"repository": {
    "name": "cicd",
    "url": "ssh://git@git.weiyigeek.top/devops/cicd.git",
    "description": null,
    "homepage": "https://git.weiyigeek.top/devops/cicd",
    "git_http_url": "https://git.weiyigeek.top/devops/cicd.git",
    "git_ssh_url": "ssh://git@git.weiyigeek.top/devops/cicd.git",
    "visibility_level": 0
  }
}

至此,在 Gitlab 对接 Jenkins 中的 generic-webhook-trigger 插件完毕!

Jenkins Pipeline 流水线

描述:接下来作者就要上硬货了,我抽取了企业中部门 CI 流水线代码进行讲解演示,特别是如何根据 Gitlab 标签事件,触发 Jenkins 流水线对应的 CI/CD 环境,并使用企业微信做为构建的消息通知。

在 Jenkins 项目中将流水线类型设置为 Pipeline script,并将从星球中获取的流水线脚本到此,修改后调试执行,例如:

c79d98ef03341bdfa1a88cddceec5cd2.png

温馨提示: 完整 Java 的 SpringBoot 项目CI/CD 流水线代码请在文末【阅读原文】获取,有不懂的欢迎在星球中私聊。

关键部分

关键:定义企业微信机器人相关全局变量,例如

def QYWX_WEBHOOK_KEY = "d9b42f56-f6e8-4dd6-8672-c288577a5503"  # 注意替换为群聊机器人地址
def QYWX_WEBHOOK = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=${QYWX_WEBHOOK_KEY}"

关键:定义全局函数,用于预设部署环境参数

// # 生产环境
def ENV_PROD() {
def config = [:]
  config.K8S_API_SERVER = "https://apiserver.cluster.prod:6443"
  config.K8S_IP_SERVER = "192.168.2.102"
  config.K8S_CREDENTIALSID = "apiserver.cluster.prod"
  config.CI_NAME = "${env.JOB_NAME}"
  config.CI_NAMESPACE = "prod"
  config.CI_ENVIRONMENT = "prod"
  config.CI_COMMAND = '["java","-jar","/app/app.jar","--spring.profiles.active=prod","--server.port=8080"]'
  config.CI_STORAGE_NAME = "nfs-storage"
  config.INGRESS_DOMAIN = "prod.weiyigeek.top"
  config.HARBOR_URL = "harbor.weiyigeek.top/app"
  config.HARBOR_AUTH = "d0ce1239-c4bf-411d-a4c6-660ab70d9b47"
return config
}
// # 测试环境
def ENV_TEST() {
def config = [:]
  config.K8S_API_SERVER = "https://apiserver.cluster.test:6443"
  config.K8S_IP_SERVER = "10.2.176.212"
  config.K8S_CREDENTIALSID = "apiserver.cluster.test"
  config.CI_NAME = "${env.JOB_NAME}"
  config.CI_NAMESPACE = "test"
  config.CI_ENVIRONMENT = "test"
  config.CI_COMMAND = '["java","-jar","/app/app.jar","--spring.profiles.active=test","--server.port=8080"]'
  config.CI_STORAGE_NAME = "nfs-storage"
  config.INGRESS_DOMAIN = "dev.weiyigeek.top"
  config.HARBOR_URL = "harbor.weiyigeek.top/app"
  config.HARBOR_AUTH = "d0ce1239-c4bf-411d-a4c6-660ab70d9b47"
return config
}

关键点:进行全局触发器配置 Generic Webhook Trigger 插件,特别注意:在声明式流水线中配置了以后,便可不必在项目界面中配置了,它会自动覆盖,非常方便。

pipeline {
  .....
  triggers {
    // Generic Webhook Trigger 插件
    GenericTrigger(
      // 提取数据到变量中,并设置缺省值。
      genericVariables: [
        [key:'ref',value:'$.ref', defaultValue:'master'],
        [key:'msg',value:'$.commits[-1].message', defaultValue:'default'],
        [key:'user',value:'$.user_name', defaultValue:'default']
      ], 
      token: env.JOB_NAME,               // 将项目名称作为 Token 认证
      printContributedVariables:false,  // 生产环境中可关闭打印
      printPostContent:false,           // 生产环境中可关闭打印
      regexpFilterText:'$ref',
      regexpFilterExpression:'(?s).*(RELEASE|TEST).*'
    )
  }
}

关键点:自定义全局选择参数,在 sh 中可通过变量名访问,而在 script pipeline 脚本中通过 params.参数名称 访问

parameters {
  gitParameter branch:'', branchFilter:'origin/(.*)', defaultValue:'origin/master', description:'项目可用标签或分支名称', name:'TAGBRANCHNAME', quickFilterEnabled:false, selectedValue:'NONE', sortMode:'DESCENDING_SMART', tagFilter:'*', type:'PT_BRANCH_TAG'
  string(name:'PREJECT_TAGBRANCH', defaultValue:"master", description:'选择项目标签或分支名称', trim:'True')
  choice(name:'PREJECT_ENVIRONMENT', choices: ['none','test','prod'], description:'选择项目构建部署环境')
  choice(name:'PREJECT_OPERATION', choices: ['none', 'rollback', 'redeploy','deploy'], description:'选择项目部署操作方式')
  choice(name:'PREJECT_NOTIFY', choices: ['ume', 'qywx'], description:'选择项目消息通知方式')
  choice(name:'IS_IMAGEBUILD', choices: ['True','False'], description:'选择是否进行镜像构建操作?')
  choice(name:'IS_RELEASE', choices: ['False','True'], description:'选择是否进行编译成品归档发布?')
  choice(name:'IS_SONARQUBE', choices: ['False','True'], description:'选择是否进行代码质量检测?')
}

关键点:在代码拉取阶段的 script 块中进行判断是由 Gitlab 自动触发还是手动触发。

// 阶段1.项目代码拉取
stage ('代码拉取') {
  steps {
    script {
    ....
    // 根据上面的全局选择参数,判断手动执行还是自动触发执行,若设置的 params.PREJECT_ENVIRONMENT 参数则表示手动触发,反之自动触发
    if ( "${params.PREJECT_ENVIRONMENT}" != "none" ) {
      user = "jenkins"
      ref = params.PREJECT_TAGBRANCH
      if ( "${params.PREJECT_TAGBRANCH}" == "none" ) {
        PREJECT_TAGBRANCH = params.TAGBRANCHNAME
      } else {
        PREJECT_TAGBRANCH = params.PREJECT_TAGBRANCH
      }
      echo "由用户手动触发构建, 标记分支 ${PREJECT_TAGBRANCH} ${ref},环境: ${params.PREJECT_ENVIRONMENT}"
    } else {
      PREJECT_TAGBRANCH = ref
      echo "由 Generic Webhook Trigger 自动触发构建, 分支 ${PREJECT_TAGBRANCH}, 提交人员: ${user}"
    }
    
    // 根据传入的分支或者标签名称拉取仓库代码到Jenkins-agent的工作目录中
    try {
      checkout([$class:'GitSCM', branches: [[name:"${PREJECT_TAGBRANCH}"]], doGenerateSubmoduleConfigurations:false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId:"${env.GITLAB_PUB}", url:"${env.GITLAB_URL}"]]])
    } catch(Exception err) {
      // 显示错误信息但还是会进行下一阶段操作
      echo err.toString()    
      unstable '拉取代码失败'
      error "ErrorMessage: ${err.getMessage()} "// 终止流水线执行
    } 
    
    }
  }
}

关键点:在代码拉取阶段的 script 块中,更加传递的变量判断是测试环境构建,还是正式环境构建。

# 获取java springboot 项目基本信息
project = GET_REAL_PROJECT()

# 根据 Gitlab 传递的变量或者手动设置变量值,判断要触发的环境
if (ref.contains("RELEASE") || ref.contains("release") || "${params.PREJECT_ENVIRONMENT}" == "prod") {
  config = ENV_PROD()
} else if (ref.contains("TEST") || ref.contains("test") || "${params.PREJECT_ENVIRONMENT}" == "test" ) {
  config = ENV_TEST()
}

关键点:在代码拉取阶段的 script 块中,输出项目构建的相关参数信息等,以及通过 企业微信 进行构建信息通知。

echo "任务名称: ${JOB_NAME}, 项目地址: ${env.GITLAB_URL}, 构建版本: ${PREJECT_TAGBRANCH}, 部署环境: ${config.CI_ENVIRONMENT}\n构建操作: ${params.PREJECT_OPERATION}, 镜像构建: ${params.IS_IMAGEBUILD}, 通知类型: ${params.PREJECT_NOTIFY}, 成品发布: ${params.IS_RELEASE}, 代码质量测试: ${params.IS_SONARQUBE}"

echo"项目路径: ${project.path} \n项目信息: ${project.name} ${project.artifactId}-${project.version}.${project.packaging} \n提交信息:${project.commitmsg} \n镜像仓库与名称:${config.HARBOR_URL}/${project.imagename}"

sh """\
  curl ${QYWX_WEBHOOK} \
  -H 'Content-Type: application/json' \
  -d '{
      "msgtype": "markdown",
      "markdown": {
        "content":"【Jenkins】<font color=\\"info\\">【${JOB_NAME}】</font> 应用构建开始通知 \\n
        >项目信息: <font color=\\"warning\\">${project.artifactId}-${project.version}.${project.packaging}</font>
        >提交信息: <font color=\\"warning\\">${project.commitmsg}</font>
        >提交人员: ${user}
        >构建版本: ${PREJECT_TAGBRANCH}
        >构建操作: ${params.PREJECT_OPERATION}
        >镜像构建: ${params.IS_IMAGEBUILD}
        >部署环境: ${config.CI_ENVIRONMENT}
        >成品归档: ${params.IS_RELEASE}
        >质量测试: ${params.IS_SONARQUBE}
        >镜像仓库: ${config.HARBOR_URL}/${project.imagename}:${config.CI_ENVIRONMENT}
        >镜像仓库: ${config.HARBOR_URL}/${project.imagename}:${project.version}-${config.CI_ENVIRONMENT}
        >[查看当前任务流水线](${env.BUILD_URL})
        ><@全栈工程师修炼指南>"
      }
  }'
"""

至此,通过 Gitlab 自动触发 Jenkins 流水线,并根据参数是否选择构建,以及构建环境的选择关键点介绍完毕,若需要完整的流水线请【访问原文】获取

测试验证

描述:首先,在 GitLab 代码平台上提交标签版本号-RELEASE,观察 Webhook 的发送状态和 Jenkins 任务的触发情况,然后在手动执行选择参数进行构建任务,通过 Blue Ocean 插件查看可直观查看到执行流程。

实践流程

步骤 01.将本地代码上传到 Gitlab 并提交标签,触发自动构建。

# 将本地代码上传到 Gitlab 远程仓库
git add .
git commit -m "1.24.61-TEST"
git push 

# 为当前提交设置标签
git tag 1.24.61-TEST

# 推送标签到远程仓库
git push origin 1.24.61-TEST
Total 0 (delta 0), reused 0 (delta 0), pack-reused 0
To ssh://git.weiyigeek.top/devops/cicd.git
 * [new tag]         1.24.61-TEST -> 1.24.61-TEST

步骤 02.上传后便自动触发 Jenkins 构建任务,此时再刷新访问 Jenkins 查看到正在执行构建任务了。

6f7565fbbfb72b9e46d65236a58d832d.png
weiyigeek.top-jenkins执行构建任务图

步骤 03.另外并通过 Blue Ocean 插件查看流水线执行详情,在代码拉取阶段可以看到由 Generic Webhook Trigger 自动触发构建, 分支 refs/tags/1.24.61-TEST, 提交人员: 全栈工程师修炼指南。

ca50252f71b380caac8bc4dcab993fa8.png
weiyigeek.top-代码拉取阶段图

步骤 04.在经过项目编译构建后,进入到镜像构建阶段,可以看到镜像仓库与名称:http://harbor.weiyigeek.top/app/devops-cicd:1.24.61-RELEASE-test

87bc7ef3a946c83a9b30660f729a266b.png
weiyigeek.top-镜像构建阶段图

步骤 05.另外,流水线脚本也是支持手动触发构建的,在 jenkins 点击 “Build with Parameters ”, 进入到构建参数选择页面,然后选择master 分支、测试环境,以及通过企业微信进行消息通知。

0ab0490419adeb0e7c95e7b44b86c66b.png
weiyigeek.top-手动触发流水线图

步骤 06.在触发构建任务时,以及完成后,可以看到通过企业微信发送的构建信息。

285a0926c150a4da10d167436ff32bee.png
weiyigeek.top-构建消息通知图

至此,Jenkins 与 Gitlab 结合实现根据提交的标签进行触发流水线,从而自动化构建,并通过企业微信进行消息通知的测试验证完成,希望对各位看友有所帮助。

相关学习书籍推荐:

END

加入:作者【全栈工程师修炼指南】知识星球

『 全栈工程师修炼指南』星球,主要涉及全栈工程师(Full Stack Development)实践文章,包括但不限于企业SecDevOps和网络安全等保合规、安全渗透测试、编程开发、云原生(Cloud Native)、物联网工业控制(IOT)、人工智能Ai,从业书籍笔记,人生职场认识等方面资料或文章。

Q: 加入作者【全栈工程师修炼指南】星球后有啥好处?

✅ 将获得作者最新工作学习实践文章以及网盘资源。

✅ 将获得作者珍藏多年的全栈学习笔记(需连续两年及以上老星球友,也可单次购买) 

✅ 将获得作者专门答疑学习交流群,解决在工作学习中的问题。

 ✅ 将获得作者远程支持(在作者能力范围内且合规)。

获取:作者工作学习全栈笔记

作者整理了10年的工作学习笔记(涉及网络、安全、运维、开发),需要学习实践笔记的看友,可添加作者微信或者回复【工作学习实践笔记】,当前价格¥299,除了获得从业笔记的同时还可进行问题答疑以及每月远程技术支持,希望大家多多支持,收获定大于付出!

 知识推荐 往期文章

若文章对你有帮助,请将它转发给更多的看友,若有疑问的小伙伴,可在评论区留言你想法哟 💬!


网站公告

今日签到

点亮在社区的每一天
去签到