Jenkins 学习笔记


持续集成

1. 概述

持续集成(Continuous integration,简称 CI)指的是,频繁地(一天多次)将代码集成到主干

持续集成的目的,就是让产品可以快速迭代,同时还能保持高质量。它的核心措施是,代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成

通过持续集成,团队可以快速的从一个功能到另一个功能,简而言之,敏捷软件开发很大一部分都要归功于持续集成

Jenkins 学习笔记

根据持续集成的设计,代码从提交到生产,整个过程有以下几步:

  • 提交

    流程的第一步,是开发者向代码仓库提交代码,所有后面的步骤都始于本地代码的一次提交

  • 测试(第一轮)

    代码仓库对提交操作配置了钩子,只要提交代码或者合并进主干,就会跑自动化测试

  • 构建

    通过第一轮测试,代码就可以合并进主干,就算可以交付了

  • 测试(第二轮)

    构建完成,就要进行第二轮测试。如果第一轮已经涵盖了所有测试内容,第二轮可以省略,当然,这时构建步骤也要移到第一轮测试前面

  • 部署

    过了第二轮测试,当前代码就是一个可以直接部署的版本。将这个版本的所有文件打包存档,发到生产服务器

  • 回滚

    一旦当前版本发生问题,就要回滚到上一个版本的构建结果。最简单的做法就是修改一下符号链接,指向上一个版本的目录

2. 组成要素

Jenkins 学习笔记

1、一个自动构建过程,从检出代码、编译构建、运行测试、结果记录、测试统计等都是自动完成的,无需人工干预

2、一个代码存储库,即需要版本控制软件来保障代码的可维护性,同时作为构建过程的素材库,一般使用 SVN 或 Git

3、一个持续集成服务器, Jenkins 就是一个配置简单和使用方便的持续集成服务器

3. 持续集成的好处

1、降低风险,由于持续集成不断去构建,编译和测试,可以很早期发现问题,所以修复的代价就少;

2、对系统健康持续检查,减少发布风险带来的问题;

3、减少重复性工作;

4、持续部署,提供可部署单元包;

5、持续交付可供使用的版本;

6、增强团队信心

4. 持续集成流程说明

Jenkins 学习笔记

1)开发人员每天进行代码提交,提交到 Git 仓库

2)然后,Jenkins 作为持续集成工具,使用 Git 工具或者 Git 仓库拉取代码到集成服务器,再配合 JDK、Maven 等软件完成代码编译、代码测试与审查、测试、打包等工作,在这个过程中有一步出错,都要重新执行一次流程

3)最后,Jenkins 把生成的包分发到测试服务器或生产服务器


Gitlab 代码托管服务器

GitLab 是一个用于仓库管理系统的开源项目,使用 Git 作为代码管理工具,并在此基础上搭建起来的 web 服务

GitLab 和 GitHub 一样属于第三方基于 Git 开发的作品,免费且开源。不同的是,GitLab 可以部署到自己的服务器上,数据库等一切信息都掌握在自己手上,适合团队内部协作开发

以 centos 为例,安装步骤如下:

  1. 安装相关依赖

    yum -y install policycoreutils openssh-server openssh-clients postfix

  2. 启动 ssh 服务 & 设置为开机启动

    systemctl enable sshd && sudo systemctl start sshd

  3. 设置 postfix 开机自启,并启动,postfix 支持 gitlab 发信功能

    systemctl enable postfix && systemctl start postfix

  4. 开放 ssh 以及 http 服务,然后重新加载防火墙列表

    firewall-cmd --add-service=ssh --permanent firewall-cmd --add-service=http --permanent firewall-cmd --reload

    如果关闭防火墙就不需要做以上配置

  5. 下载 gitlab 包,并且安装在线下载安装包

    wget https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el6/gitlab-ce-12.4.2-ce.0.el6.x 86_64.rpm](https://mirrors.tuna.tsinghua.edu.cn/gitlab-ce/yum/el6/gitlab-ce-12.4.2-ce.0.el6.x86_64.rpm

  6. 修改 gitlab 配置

    vi /etc/gitlab/gitlab.rb

    修改 gitlab 访问地址和端口,默认为 80,我们改为 82

    external_url ‘http://192.168.66.100:82’
    nginx[‘listen_port’] = 82
    
  7. 重载配置及启动

    gitlab gitlab-ctl reconfigure gitlab-ctl restart

  8. 把端口添加到防火墙

    firewall-cmd --zone=public --add-port=82/tcp --permanent firewall-cmd --reload

Jenkins

Jenkins 是一款流行的开源持续集成(Continuous Integration)工具,广泛用于项目开发,具有自动化构建、测试和部署等功能

1. Jenkins 安装

  1. 获取 Jenkins 安装包,下载页面:https://jenkins.io/zh/download/

    进行安装:rpm -ivh jenkins-2.190.3-1.1.noarch.rpm

  2. 修改 Jenkins 配置

    vi /etc/syscofig/jenkins

    修改内容如下:

    JENKINS_USER="root"
    JENKINS_PORT="8888"
    
  3. 启动 Jenkins

    systemctl start jenkins

  4. 打开浏览器访问 http://localhost:8888

  5. 获取并输入 admin 账户密码

    cat /var/lib/jenkins/secrets/initialAdminPassword

2. Jenkins 插件管理

Jenkins 本身不提供很多功能,我们可以通过使用插件来满足我们的使用。例如从Gitlab拉取代码,使用Maven构建项目等功能需要依靠插件完成

Jenkins 国外官方插件地址下载速度非常慢,可以修改为国内插件地址:Jenkins - Manage Jenkins - Manage Plugins,点击 Available

这样做是为了把 Jenkins 官方的插件列表下载到本地,接着修改地址文件,替换为国内插件地址

cd /var/lib/jenkins/updates

sed -i 's/http:\/\/updates.jenkinsci.org\/download/https:\/\/mirrors.tuna.tsinghua.edu.cn\/jenkins/g' default.json && sed -i
's/http:\/\/www.google.com/https:\/\/www.baidu.com/g' default.json

最后,Manage Plugins 点击 Advanced,把 Update Site 改为国内插件下载地址 https://mirrors.tuna.tsinghua.edu.cn/jenkins/updates/update-center.json

Sumbit 后,在浏览器输入:http://localhost:8888/restart,重启 Jenkins,下载中文汉化插件

Jenkins - Manage Jenkins - Manage Plugins,点击 Available,搜索 "Chinese",勾选并安装。重启 Jenkins 后,就看到 Jenkins 汉化了

3. Jenkins 用户权限管理

我们可以利用 Role-based Authorization Strategy 插件来管理 Jenkins 用户权限,安装插件,点击 Manage Jenkins,选择 Configure Global Security,授权策略切换为 Role-Based Strategy,保存

Jenkins 学习笔记

在系统管理页面进入 Manage and Assign Roles,点击 Manage Roles,可创建角色

Jenkins 学习笔记

  • Global roles(全局角色):管理员等高级用户可以创建基于全局的角色
  • Project roles(项目角色):针对某个或者某些项目的角色
  • Slave roles(奴隶角色):节点相关的权限

在系统管理页面进入 Manage Users,创建用户。接下来是为用户分配角色,系统管理页面进入 Manage and Assign Roles,点击 Assign Roles,为用户分配角色

4. Jenkins 凭证管理

凭据可以用来存储需要密文保护的数据库密码、Gitlab 密码信息、Docker 私有仓库密码等,以便 Jenkins 可以和这些第三方的应用进行交互

要在 Jenkins 使用凭证管理功能,需要安装 Credentials Binding 插件。安装插件后,会多出一个凭证菜单,在这里管理所有凭证

Jenkins 学习笔记

可以添加的凭证有五种:

  • Username with password:用户名和密码
  • SSH Username with private key:使用 SSH 用户和密钥
  • Secret file:需要保密的文本文件,使用时 Jenkins 会将文件复制到一个临时目录中,再将文件路径设置到一个变量中,等构建结束后,所复制的 Secret file 就会被删除
  • Secret text:需要保存的一个加密的文本串,如钉钉机器人或 Github 的 api token
  • Certificate:通过上传证书文件的方式

5. 集成 Maven

  1. Jenkins 关联 JDK 和 MAVEN

    Jenkins - Global Tool Configuration - JDK,新增 JDK,配置指定 JDK 的 JAVA_HOME

    Jenkins - Global Tool Configuration - Maven,新增 Maven,配置指定 MAVEN 的 MAVEN_HOME

  2. 添加 Jenkins 全局变量

    Manage Jenkins - Configure System - Global Properties,添加三个全局变量 JAVA_HOME、M2_HOME、PATH+EXTRA

我们也可以在拉取代码时完成构建,选择 构建 - 增加构建步骤 - Execute Shell,输入:mvn clean package

保存配置后,选择项目,点击构建 Build Now 开始构建项目

Jenkins 学习笔记

查看 linux 的 /var/lib/jenkins/workspace/**目录,会生成一个 target 目录,里面有相应的包生成


Jenkins 项目构建类型

Jenkins 中自动构建项目的类型有很多,常用的有以下三种:

  • *风格软件项目(FreeStyle Project)
  • Maven 项目(Maven Project)
  • 流水线项目(Pipeline Project)

每种类型的构建都可以完成一样的构建过程与结果,只是在操作方式、灵活度等方面有所区别,在实际开发中,可以根据自己的需求和习惯来选择

1. *风格项目构建

一个*风格项目来完成项目的集成过程:拉取代码 - 编译 - 打包 - 部署

  1. 创建项目

  2. 配置源码管理,从 gitlab 拉取代码

  3. 编译打包

    构建 - 添加构建步骤 - Executor Shell

    echo "开始编译和打包"
    mvn clean package
    echo "编译和打包结束"
    
  4. 部署,把项目部署到远程的 Tomcat

    Jenkins 部署项目到 Tomcat 服务器,需要用到 Tomcat 的用户,所以修改 tomcat 以下配置,添加用户及权限

    vi /opt/tomcat/conf/tomcat-users.xml
    

    内容如下:

    <tomcat-users>
       <role rolename="tomcat"/>
       <role rolename="role1"/>
       <role rolename="manager-script"/>
       <role rolename="manager-gui"/>
       <role rolename="manager-status"/>
       <role rolename="admin-gui"/>
       <role rolename="admin-script"/>
       <user username="tomcat" password="tomcat" roles="manager-gui,managerscript,tomcat,admin-gui,admin-script"/>
    </tomcat-users>
    

    为了能够刚才配置的用户登录到 Tomcat,还需要修改以下配置

    vi /opt/tomcat/webapps/manager/META-INF/context.xml
    

    把下面内容注释

    <!--
    <Valve className="org.apache.catalina.valves.RemoteAddrValve"
    allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1" />
    -->
    

    重启 Tomcat

    /opt/tomcat/bin/shutdown.sh 停止
    /opt/tomcat/bin/startup.sh 启动
    

    访问:http://localhost:8080/manager/html ,输入 tomcat 和 tomcat,看到以下页面代表成功

    Jenkins 学习笔记

    Jenkins 本身无法实现远程部署到 Tomcat 的功能,需要安装 Deploy to container 插件实现

    添加 Tomcat 用户凭证,添加构建后操作,选择 Deploy war/ear to a container,部署到容器(远程 tomcat)

改动代码后的持续集成

  1. 源码修改并提交到 gitlab
  2. 在 Jenkins 中项目重新构建
  3. 访问 Tomcat

2. Maven 项目构建

使用 Maven 项目构建需要安装 Maven Integration 插件,拉取代码和远程部署的过程和*风格项目一样,只是构建部分不同。之前是通过 shell 来指定编译后的行为,现在则是在 Build 操作界面输入指定的 pom.xml 文件路径,输入 maven 指令

Jenkins 学习笔记

3. Pipeline 流水线项目构建

3.1 Pipeline 简介

Pipeline,简单来说,就是一套运行在 Jenkins 上的工作流框架,将原来独立运行于单个或者多个节点的任务连接起来,实现单个任务难以完成的复杂流程编排和可视化的工作

Pipeline 脚本是由 Groovy 语言实现的,支持两种语法:Declarative(声明式)和 Scripted Pipeline(脚本式)语法

Pipeline 也有两种创建方法:

  • 可以直接在 Jenkins 的 Web UI 界面中输入脚本
  • 也可以通过创建一个 Jenkinsfile 脚本文件放入项目源码库中(推荐在 Jenkins 中直接从源代码控制 SCM 中直接载入 Jenkinsfile Pipeline 这种方法)

要使用 Pipeline,需安装 Pipeline 插件,Manage Jenkins - Manage Plugins - 可选插件 – 安装 Pipeline,安装插件后,创建项目的时候多了流水线类型

Jenkins 学习笔记

3.2 Pipeline 语法快速入门
  1. Declarative 声明式 Pipeline

    流水线 - 选择 Declarative Pipeline - 选择 HelloWorld 模板,生成内容如下:

    pipeline {
        agent any
        stages {
            stage('Hello') {
                steps {
                    echo 'Hello World'
                }
            }
    	}
    }
    
    • stages:代表整个流水线的所有执行阶段,通常 stages 只有一个,里面包含多个 stage
    • stage:代表流水线中的某个阶段,可能出现多个,一般分为拉取代码,编译构建,部署等阶段
    • steps:代表一个阶段内需要执行的逻辑,steps 里面是 shell 脚本,git 拉取代码,ssh 远程发布等任意内容

    编写一个简单的声明式 Pipeline:

    pipeline {
       agent any
       stages {
           stage('拉取代码') {
               steps {
                   echo '拉取代码'
               }
           } 
           stage('编译构建') {
               steps {
                   echo '编译构建'
               }
           } 
           stage('项目部署') {
               steps {
                   echo '项目部署'
               }
           }
       }
    }
    

    点击构建,可以看到整个构建过程

    我们可以在流水线语法里选择片段生成器,快速生成 Pipeline 代码:

    • 生成一个 pull stage

      选择 checkout from version controller,拉取代码,选择类型为 git,填写好 git 项目地址,填写拉取分支名字,生成流水线脚本,脚本里就包含了凭证信息

    • 生成一个构建 stage

      选择 sh:shell script,输入 mvc clean package,点击生成脚本

    • 生成一个部署 stage

      选择 deploy,填写 WAR files:targer/*.war,选择 tomcat 远程,然后填写 tomcat 的地址就可远程部署,可以同时部署多台 tomcat

  2. Scripted 脚本式 Pipeline

    流水线 - 选择 Scripted Pipeline,编写一个简单的脚本式 Pipeline:

    node {
        def mvnHome
        stage('拉取代码') { // for display purposes
            echo '拉取代码'
        } 
    	stage('编译构建') {
            echo '编译构建'
        } 
    	stage('项目部署') {
        	echo '项目部署'
    	}
    }
    
    • Node:节点,一个 Node 就是一个 Jenkins 节点,Master 或者 Agent,是执行 Step 的具体运行环境
    • Stage:阶段,一个 Pipeline 可以划分为若干个 Stage,每个 Stage 代表一组操作,比如:Build、Test、Deploy,Stage 是一个逻辑分组的概念
    • Step:步骤,Step 是最基本的操作单元,可以是打印一句话,也可以是构建一个 Docker 镜像,由各类 Jenkins 插件提供,比如命令:sh 'make',就相当于我们平时 shell 终端中执行 make 命令一样

    完整代码如下:

    pipeline{
        agentanystages{
            stage('拉取代码'){
                steps{
                    checkout([
                        $class: 'GitSCM',
                        branches: [ [name: '*/master']],
                        doGenerateSubmoduleConfigurations: false,
                        extensions: [ ],
                        submoduleCfg: [ ],
                        userRemoteConfigs: [
                            [
                                credentialsId: '68f2087f-a034-4d39-a9ff-1f776dd3dfa8',
                                url: 'git@192.168.66.100: itheima_group/web_demo.git'
                            ]
                        ]
                    ])
                }
            }stage('编译构建'){
                steps{
                    shlabel: '',
                    script: 'mvncleanpackage'
                }
            }stage('项目部署'){
                steps{
                    deployadapters: [
                        tomcat8(credentialsId: 'afc43e5e-4a4e-4de6-984fb1d5a254e434',
                        path: '',
                        url: 'http: //192.168.66.102: 8080')
                    ],
                    contextPath: null,
                    war: 'target/*.war'
                }
            }
        }
    }
    
  3. Pipeline Script from SCM

    之前我们都是直接在 Jenkins 的 UI 界面编写 Pipeline 代码,这样不方便脚本维护,建议把 Pipeline 脚本放在项目中,一起进行版本控制

    1. 在项目根目录建立 Jenkinsfile 文件,编写脚本内容,把文件上传到 Gitlab

    2. 在项目中引用该文件

      Jenkins 学习笔记

    3. 点击构建,就开始拉取,拉取后拿到 Jenkins 后操作


Jenkins 构建触发器

Jenkins 内置了四种构建触发器:

  • 远程触发构建
  • 其他工程构建后触发(Build after other projects are build)
  • 定时构建(Build periodically)
  • 轮询SCM(Poll SCM)

1. 远程触发构建

在 Jenkins 工程下点击配置,然后构建触发器,其他系统发送 URL 请求,就可以让 Jenkins 开始构建(触发构建)

Jenkins 学习笔记

触发构建url:http://192.168.66.101:8888/job/web_demo_pipeline/build?token=6666

2. 其他工程构建后触发

该触发器的需求是:当前项目需要前一个项目构建完成后才能触发

  1. 创建 pre_job 流水线工程,该工程构建完成后触发当前项目

  2. 配置需要触发的工程

    Jenkins 学习笔记

3. 定时构建

选择 Build periodically,输入定时字符串表达式,即可定时构建

下面是一些定时表达式的例子:

每30分钟构建一次:H代表形参 H/30 * * * * 10:02 10:32

每2个小时构建一次: H H/2 * * *

每天的8点,12点,22点,一天构建3次: (多个时间点中间用逗号隔开) 0 8,12,22 * * *

每天中午12点定时构建一次 H 12 * * *

每天下午18点定时构建一次 H 18 * * *

在每个小时的前半个小时内的每10分钟 H(0-29)/10 * * * *

每两小时一次,每个工作日上午9点到下午5点(也许是上午10:38,下午12:38,下午2:38,下午4:38) H H(9-16)/2 * * 1-5

4. 轮询 SCM

轮询 SCM,是指定时扫描本地代码仓库的代码是否有变更,如果代码有变更就触发项目构建

Jenkins 学习笔记

Jenkins 会定时扫描本地整个项目的代码,增大系统的开销,不建议使用轮询 SCM

5. Git hook 自动触发构建

利用 Gitlab 的 webhook 实现代码 push 到仓库,立即触发项目自动构建,需要安装两个插件:Gitlab Hook 和 GitLab

Jenkins 学习笔记

Jenkins 学习笔记

需要把生成的 webhook URL 配置到 Gitlab 中:

  1. 使用 root 账户登录到后台,点击 Admin Area - Settings - Network,勾选 Allow requests to the local network from web hooks and services 让网络钩子允许请求本地网络
  2. 点击项目 - Settings - Integrations,在项目添加 webhook

在 Jenkins 中,Manage Jenkins - Configure System,取消勾选 Enable authentication for '/project' end-point GitLab connections


Jenkins 参数化构建

有时候在项目构建的过程中,我们需要根据用户的输入动态传入参数,从而影响整个构建结果,比如:我们希望根据用户传入的参数,部署不同的分支,这时我们可以使用参数化构建

Jenkins 学习笔记

在 Jenkins 添加字符串类型参数

Jenkins 学习笔记

改动 pipeline 流水线代码

Jenkins 学习笔记

点击 Build with Parameters,就用指定参数开始了构建

Jenkins 学习笔记


Jenkins 配置邮箱服务器

安装 Email Extension 插件 template,Jenkins 设置邮箱相关参数:Manage Jenkins - Configure System,

Jenkins 学习笔记

设置 Jenkins 默认邮箱信息

Jenkins 学习笔记

在项目根目录编写 email.html,并把文件推送到 Gitlab,内容如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>${ENV, var="JOB_NAME"}-第${BUILD_NUMBER}次构建日志</title>
    </head>
    <body leftmargin="8" marginwidth="0" topmargin="8" marginheight="4"
          offset="0">
        <table width="95%" cellpadding="0" cellspacing="0"
               style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica, sansserif">
            <tr>
                <td>(本邮件是程序自动下发的,请勿回复!)</td>
            </tr>
            <tr>
                <td><h2>
                    <font color="#0000FF">构建结果 - ${BUILD_STATUS}</font>
                    </h2></td>
            </tr>
            <tr>
                <td><br />
                    <b><font color="#0B610B">构建信息</font></b>
                    <hr size="2" width="100%" align="center" /></td>
            </tr>
            <tr>
                <td>
                    <ul>
                        <li>项目名称&nbsp;:&nbsp;${PROJECT_NAME}</li>
                        <li>构建编号&nbsp;:&nbsp;第${BUILD_NUMBER}次构建</li>
                        <li>触发原因:&nbsp;${CAUSE}</li>
                        <li>构建日志:&nbsp;<a
                                          href="${BUILD_URL}console">${BUILD_URL}console</a></li>
                        <li>构建&nbsp;&nbsp;Url&nbsp;:&nbsp;<a
                                                             href="${BUILD_URL}">${BUILD_URL}</a></li>
                        <li>工作目录&nbsp;:&nbsp;<a
                                                href="${PROJECT_URL}ws">${PROJECT_URL}ws</a></li>
                        <li>项目&nbsp;&nbsp;Url&nbsp;:&nbsp;<a
                                                             href="${PROJECT_URL}">${PROJECT_URL}</a></li>
                    </ul>
                </td>
            </tr>
            <tr>
                <td><b><font color="#0B610B">Changes Since Last
                    Successful Build:</font></b>
                    <hr size="2" width="100%" align="center" /></td>
            </tr>
            <tr>
                <td>
                    <ul>
                        <li>历史变更记录 : <a
                                        href="${PROJECT_URL}changes">${PROJECT_URL}changes</a></li>
                    </ul> ${CHANGES_SINCE_LAST_SUCCESS,reverse=true, format="Changes for
                    Build #%n:<br />%c<br />",showPaths=true,changesFormat="<pre>[%a]<br
/>%m</pre>",pathFormat="&nbsp;&nbsp;&nbsp;&nbsp;%p"}
                </td>
            </tr>
            <tr>
                <td><b>Failed Test Results</b>
                    <hr size="2" width="100%" align="center" /></td>
            </tr>
            <tr>
                <td><pre
                         style="font-size: 11pt; font-family: Tahoma, Arial, Helvetica,
                    sans-serif">$FAILED_TESTS</pre>
                    <br /></td>
            </tr>
            <tr>
                <td><b><font color="#0B610B">构建日志 (最后 100行):</font></b>
                    <hr size="2" width="100%" align="center" /></td>
            </tr>
            <tr>
                <td><textarea cols="80" rows="30" readonly="readonly"
                              style="font-family: Courier New">${BUILD_LOG,
                    maxLines=100}</textarea>
                </td>
            </tr>
        </table>
    </body>
</html>

编写 Jenkinsfile 添加构建后发送邮件的 Pipeline 代码,这个 post 可以到声明式脚本生成器里选择 post,选择对应的 conditions,比如选择永远都执行等等,他和 stage 是分开的

pipeline {
    agent any
    stages {
    stage('拉取代码') {
        steps {
            checkout([$class: 'GitSCM',
            branches: [[name: '*/master']],
            doGenerateSubmoduleConfigurations: false, 
            extensions: [], 
            submoduleCfg: [],
            userRemoteConfigs: [[credentialsId: '68f2087f-a034-4d39-a9ff-1f776dd3dfa8',
                                 url: 'git@192.168.66.100:itheima_group/web_demo.git']]])
        }
	} 
    stage('编译构建') {
        steps {
        	sh label: '', script: 'mvn clean package'
    	}
	} 
    stage('项目部署') {
    	steps {
    		deploy adapters: [tomcat8(credentialsId: 'afc43e5e-4a4e-4de6-984fb1d5a254e434',
            path: '', 
    		url: 'http://192.168.66.102:8080')],
            contextPath: null,
			war: 'target/*.war'
		}
	}
}
    post {  # 主要看这就行
    	always {
        	emailext(
            	subject: '构建通知:${PROJECT_NAME} - Build # ${BUILD_NUMBER} -${BUILD_STATUS}!',
                body: '${FILE,path="email.html"}',
                to: 'xxx@qq.com'
              )
          }
    }
}

邮件相关全局参数参考列表:系统设置 - Extended E-mail Notification - Content Token Reference,点击旁边的 ? 号

Jenkins 学习笔记


上一篇:Package jenkins is not available, but is referred to by another package. This may mean that the pack


下一篇:新手教程:教你如何利用PS把臭水沟里的水变清澈透亮