寒冰剑 發表於 2019-9-25 11:40:00

Jenkins 结合 Docker 为 .NET Core 项目实现低配版的 CI&CD

<p>随着项目的不断增多,最开始单体项目手动执行 <code>docker build</code> 命令,手动发布项目就不再适用了。一两个项目可能还吃得消,10 多个项目每天让你构建一次还是够呛。即便你的项目少,每次花费在发布上面的时间累计起来都够你改几个 BUG 了。</p>
<p>所以我们需要自动化这个流程,让项目的发布和测试不再这么繁琐。在这里我使用了 Jenkins 作为基础的 CI/CD Pipeline 工具,关于 Jenkins 的具体介绍这里就不再赘述。在版本管理、构建项目、单元测试、集成测试、环境部署我分别使用到了 <strong>Gogs</strong>、<strong>Docker</strong>、<strong>Docker Swarm(已与 Docker 整合)</strong> 这几个软件协同工作。</p>
<p>以下步骤我参考了 <strong>Continuous Integration with Jenkins and Docker</strong> 一文,并使用了作者提供的 groovy 文件和 <code>slave.py</code> 文件。</p>
<p>关于 <strong>Docker-CE</strong> 的安装,请参考我的另一篇博文 《Linux 下的 Docker 安装与使用》 。</p>
<h2 id="一jenkins-的部署">一、Jenkins 的部署</h2>
<p>既然都用了 Docker,我是不想在实体机上面安装一堆环境,所以我使用了 Docker 的形式来部署 Jenkins 的 Master 和 Slave,省时省力。Master 就是调度管道任务的主机,也是唯一有 UI 供用户操作的。而 Slave 就是具体的工作节点,用于执行具体的管道任务。</p>
<h3 id="11-构建-master-镜像">1.1 构建 Master 镜像</h3>
<p>第一步,我们在主机上建立一个 master 文件夹,并使用 <code>vi </code> 创建两个 groovy 文件,这两个文件在后面的 Dockerfile 会被使用到,下面是 <code>default-user.groovy</code> 文件的代码:</p>
<pre><code class="language-groovy">import jenkins.model.*
import hudson.security.*

def env = System.getenv()

def jenkins = Jenkins.getInstance()
jenkins.setSecurityRealm(new HudsonPrivateSecurityRealm(false))
jenkins.setAuthorizationStrategy(new GlobalMatrixAuthorizationStrategy())

def user = jenkins.getSecurityRealm().createAccount(env.JENKINS_USER, env.JENKINS_PASS)
user.save()

jenkins.getAuthorizationStrategy().add(Jenkins.ADMINISTER, env.JENKINS_USER)
jenkins.save()
</code></pre>
<p>接着再用 <code>vi</code> 创建一个新的 <code>executors.groovy</code> 文件,并输入以下内容:</p>
<pre><code class="language-groovy">import jenkins.model.*
Jenkins.instance.setNumExecutors(0)
</code></pre>
<p>以上动作完成之后,在 master 文件夹下面应该有两个 groovy 文件。</p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113231453-1601310614.png" alt="" loading="lazy"></p>
<p>两个 master 所需要的 groovy 文件已经编写完成,下面来编写 master 镜像的 Dockerfile 文件,每一步的作用我已经用中文进行了标注。</p>
<pre><code class="language-dockerfile"># 使用官方的 Jenkins 镜像作为基础镜像。
FROM jenkins/jenkins:latest

# 使用内置的 install-plugins.sh 来安装插件。
RUN /usr/local/bin/install-plugins.sh git matrix-auth workflow-aggregator docker-workflow blueocean credentials-binding

# 设置 Jenkins 的管理员账户和密码。
ENV JENKINS_USER admin
ENV JENKINS_PASS admin

# 跳过初始化安装向导。
ENV JAVA_OPTS -Djenkins.install.runSetupWizard=false

# 将刚刚编写的两个 groovy 脚本复制到初始化文件夹内。
COPY executors.groovy /usr/share/jenkins/ref/init.groovy.d/
COPY default-user.groovy /usr/share/jenkins/ref/init.groovy.d/

# 挂载 jenkins_home 目录到 Docker 卷。
VOLUME /var/jenkins_home
</code></pre>
<p>接着我们通过命令构建出 Master 镜像。</p>
<pre><code class="language-bash">docker build -t jenkins-master .
</code></pre>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113242393-1828048772.png" alt="" loading="lazy"></p>
<h3 id="12-构建-slave-镜像">1.2 构建 Slave 镜像</h3>
<p>Slave 镜像的核心是一个 <code>slave.py</code> 的 python 脚本,它主要执行的动作是运行 <code>slave.jar</code> 并和 Master 建立通信,这样你的管道任务就能够交给 Slave 进行执行。这个脚本所做的工作流程如下:</p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925114418804-256835220.png" alt="" loading="lazy"></p>
<p>我们再建立一个 slave 文件夹,并使用 <code>vi</code> 将 python 脚本复制进去。</p>
<p><code>slave.py</code>的内容:</p>
<pre><code class="language-python">from jenkins import Jenkins, JenkinsError, NodeLaunchMethod
import os
import signal
import sys
import urllib
import subprocess
import shutil
import requests
import time

slave_jar = '/var/lib/jenkins/slave.jar'
slave_name = os.environ['SLAVE_NAME'] if os.environ['SLAVE_NAME'] != '' else 'docker-slave-' + os.environ['HOSTNAME']
jnlp_url = os.environ['JENKINS_URL'] + '/computer/' + slave_name + '/slave-agent.jnlp'
slave_jar_url = os.environ['JENKINS_URL'] + '/jnlpJars/slave.jar'
print(slave_jar_url)
process = None

def clean_dir(dir):
    for root, dirs, files in os.walk(dir):
      for f in files:
            os.unlink(os.path.join(root, f))
      for d in dirs:
            shutil.rmtree(os.path.join(root, d))

def slave_create(node_name, working_dir, executors, labels):
    j = Jenkins(os.environ['JENKINS_URL'], os.environ['JENKINS_USER'], os.environ['JENKINS_PASS'])
    j.node_create(node_name, working_dir, num_executors = int(executors), labels = labels, launcher = NodeLaunchMethod.JNLP)

def slave_delete(node_name):
    j = Jenkins(os.environ['JENKINS_URL'], os.environ['JENKINS_USER'], os.environ['JENKINS_PASS'])
    j.node_delete(node_name)

def slave_download(target):
    if os.path.isfile(slave_jar):
      os.remove(slave_jar)

    loader = urllib.URLopener()
    loader.retrieve(os.environ['JENKINS_URL'] + '/jnlpJars/slave.jar', '/var/lib/jenkins/slave.jar')

def slave_run(slave_jar, jnlp_url):
    params = [ 'java', '-jar', slave_jar, '-jnlpUrl', jnlp_url ]
    if os.environ['JENKINS_SLAVE_ADDRESS'] != '':
      params.extend([ '-connectTo', os.environ['JENKINS_SLAVE_ADDRESS' ] ])

    if os.environ['SLAVE_SECRET'] == '':
      params.extend([ '-jnlpCredentials', os.environ['JENKINS_USER'] + ':' + os.environ['JENKINS_PASS'] ])
    else:
      params.extend([ '-secret', os.environ['SLAVE_SECRET'] ])
    return subprocess.Popen(params, stdout=subprocess.PIPE)

def signal_handler(sig, frame):
    if process != None:
      process.send_signal(signal.SIGINT)

signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)

def master_ready(url):
    try:
      r = requests.head(url, verify=False, timeout=None)
      return r.status_code == requests.codes.ok
    except:
      return False

while not master_ready(slave_jar_url):
    print("Master not ready yet, sleeping for 10sec!")
    time.sleep(10)

slave_download(slave_jar)
print 'Downloaded Jenkins slave jar.'

if os.environ['SLAVE_WORING_DIR']:
    os.setcwd(os.environ['SLAVE_WORING_DIR'])

if os.environ['CLEAN_WORKING_DIR'] == 'true':
    clean_dir(os.getcwd())
    print "Cleaned up working directory."

if os.environ['SLAVE_NAME'] == '':
    slave_create(slave_name, os.getcwd(), os.environ['SLAVE_EXECUTORS'], os.environ['SLAVE_LABELS'])
    print 'Created temporary Jenkins slave.'

process = slave_run(slave_jar, jnlp_url)
print 'Started Jenkins slave with name "' + slave_name + '" and labels [' + os.environ['SLAVE_LABELS'] + '].'
process.wait()

print 'Jenkins slave stopped.'
if os.environ['SLAVE_NAME'] == '':
    slave_delete(slave_name)
    print 'Removed temporary Jenkins slave.'
</code></pre>
<p>上述脚本的工作基本与流程图的一致,因为 Jenkins 针对 Python 提供了 SDK ,所以原作者使用 Python 来编写的 “代理” 程序。不过 Jenkins 也有 RESTful API,你也可以使用 .NET Core 编写类似的 “代理” 程序。</p>
<p>接着我们来编写 Slave 镜像的 Dockerfile 文件,因为国内服务器访问 Ubuntu 的源很慢,经常因为超时导致构建失败,这里切换成了阿里云的源,其内容如下:</p>
<pre><code class="language-dockerfile">FROM ubuntu:16.04

# 安装 Docker CLI。
RUN sed -i s@/archive.ubuntu.com/@/mirrors.aliyun.com/@g /etc/apt/sources.list &amp;&amp; apt-get clean
RUN apt-get update --fix-missing &amp;&amp; apt-get install -y apt-transport-https ca-certificates curl openjdk-8-jre python python-pip git

# 使用阿里云的镜像源。
RUN curl -fsSL http://mirrors.aliyun.com/docker-ce/linux/ubuntu/gpg | apt-key add -
RUN echo "deb http://mirrors.aliyun.com/docker-ce/linux/ubuntu xenial stable" &gt; /etc/apt/sources.list.d/docker.list

RUN apt-get update --fix-missing &amp;&amp; apt-get install -y docker-ce --allow-unauthenticated
RUN easy_install jenkins-webapi

# 安装 Docker-Compose 工具。
RUN curl -L https://github.com/docker/compose/releases/download/1.21.2/docker-compose-`uname -s`-`uname -m` &gt; /usr/local/bin/docker-compose &amp;&amp; chmod +x /usr/local/bin/docker-compose
RUN mkdir -p /home/jenkins
RUN mkdir -p /var/lib/jenkins

# 将 slave.py 文件添加到容器。
ADD slave.py /var/lib/jenkins/slave.py

WORKDIR /home/jenkins

# 配置 Jenkins Master 的一些连接参数和 Slave 信息。
ENV JENKINS_URL "http://jenkins"
ENV JENKINS_SLAVE_ADDRESS ""
ENV JENKINS_USER "admin"
ENV JENKINS_PASS "admin"
ENV SLAVE_NAME ""
ENV SLAVE_SECRET ""
ENV SLAVE_EXECUTORS "1"
ENV SLAVE_LABELS "docker"
ENV SLAVE_WORING_DIR ""
ENV CLEAN_WORKING_DIR "true"

CMD [ "python", "-u", "/var/lib/jenkins/slave.py" ]
</code></pre>
<p>继续使用 <code>docker build</code> 构建 Slave 镜像:</p>
<pre><code class="language-shell">docker build -t jenkins-slave .
</code></pre>
<h3 id="13-编写-docker-compose-文件">1.3 编写 Docker Compose 文件</h3>
<p>这里的 Docker Compose 文件,我取名叫 <code>docker-compose.jenkins.yaml</code> ,主要工作是为了启动 Master 和 Slave 容器。</p>
<pre><code class="language-yaml">version: '3.1'
services:
    jenkins:
      container_name: jenkins
      ports:
            - '8080:8080'
            - '50000:50000'
      image: jenkins-master
    jenkins-slave:
      container_name: jenkins-slave
      restart: always
      environment:
            - 'JENKINS_URL=http://jenkins:8080'
      image: jenkins-slave
      volumes:
            - /var/run/docker.sock:/var/run/docker.sock# 将宿主机的 Docker Daemon 挂载到容器内部。
            - /home/jenkins:/home/jenkins # 将数据挂载出来,方便后续进行释放。
      depends_on:
            - jenkins

</code></pre>
<p>执行 Docker Compose 之后,我们通过 <code>宿主机 IP:8080</code> 就可以访问到 Jenkins 内部了,如下图。</p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113251559-1383014544.png" alt="" loading="lazy"></p>
<h2 id="二gogs-的部署">二、Gogs 的部署</h2>
<p>我们内部开发使用的 Git 仓库是使用 Gogs 进行搭建的,Gogs 官方提供了 Docker 镜像,那我们可以直接编写一个 Docker Compose 快速部署 Gogs。</p>
<p><code>docker-compose.gogs.yaml</code> 文件内容如下:</p>
<pre><code class="language-yaml">version: '3.1'
services:
gogs:
    image: gogs/gogs
    container_name: 'gogs'
    expose:
      - '3000:3000'
    expose:
      - 22
    volumes:
      - /var/lib/docker/Persistence/Gogs:/data        # 挂载数据卷。
    restart: always

</code></pre>
<p>执行以下命令后,即可启动 Gogs 程序,访问 <code>宿主机 IP:3000</code> 按照配置说明安装 Gogs 即可,之后你就可以创建远程仓库了。</p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113300746-1556783166.png" alt="" loading="lazy"></p>
<h2 id="三gogs-与-jenkins-的集成">三、Gogs 与 Jenkins 的集成</h2>
<p>虽然大部分都推荐 Jenkins 的 Gogs Webhook 插件,不过这个插件很久不更新了,而且不支持 <strong>版本发布</strong> 事件。针对于该问题虽然官方有 PR #62,但一直没有合并,等到合并的时候都是猴年马月了。这里还是建议使用 <strong>Generic Webhook Trigger</strong> ,用这个插件来触发 Jenkins 的管道任务。</p>
<h3 id="31-创建流水线项目">3.1 创建流水线项目</h3>
<p>首先找到 Jenkins 的插件中心,搜索 Generic Webhook Trigger 插件,并进行安装。</p>
<p><img src="https://i.loli.net/2019/09/25/k2qp5TJSI81WVHe.gif" alt="20190924210101.gif" loading="lazy"></p>
<p>继续新建一个管道任务,取名叫做 TestProject,类型选择 Pipeline 。</p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113554614-737620261.gif" alt="" loading="lazy"></p>
<p>首先配置项目的数据来源,选择 SCM,并且配置 Git 远程仓库的地址,如果是私有仓库则还需要设置用户名和密码。</p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113602988-1823259814.png" alt="" loading="lazy"></p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113610825-228213853.png" alt="" loading="lazy"></p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113618272-1776533647.png" alt="" loading="lazy"></p>
<h3 id="32-jenkins-的-webhook-配置">3.2 Jenkins 的 Webhook 配置</h3>
<p>流水线项目建立完成后,我们就可以开始设置 Generic WebHook Trigger 的一些参数,以便让远程的 Gogs 能够触发构建任务。</p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113630131-1738192339.png" alt="" loading="lazy"></p>
<p>我们为 TestProject 创建一个 Token,这个 Token 是跟流水线任务绑定了,说白了就是流水线任务的一个标识。建议使用随机 Guid 作为 Token,不然其他人都可以随便触发你的流水线任务进行构建了。</p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113638364-1871541696.png" alt="" loading="lazy"></p>
<h3 id="33-gogs-的-webhook-配置">3.3 Gogs 的 Webhook 配置</h3>
<p>接着来到刚刚我们建好的仓库,找到 <strong>仓库设置-&gt;管理 Web 钩子-&gt;添加 Web 钩子-&gt;Gogs</strong> 。</p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113645555-738706285.png" alt="" loading="lazy"></p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113654778-715619347.png" alt="" loading="lazy"></p>
<p>因为触发构建不可能每次提交都触发,一般来说都是创建了某个合并请求,或者发布新版本的时候就会触发流水线任务。因此这里你可以根据自己的情况来选择触发事件,这里我以合并请求为例,你可以在钩子设置页面点击 <strong>测试推送</strong>。这样就可以看到 Gogs 发送给 Jenkins 的 JSON 结构是怎样的,你就能够在 Jenkins 那边有条件的进行处理。</p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113703659-1058885093.png" alt="" loading="lazy"></p>
<p>不过测试推送只能够针对普通的 push 事件进行测试,像 <strong>合并请求</strong> 或者 <strong>版本发布</strong> 这种事件只能自己模拟操作了。在这里我新建了一个用户,Fork 了另一个帐号建立的 TestProject 仓库。</p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113710863-1737740432.png" alt="" loading="lazy"></p>
<p>在 Fork 的仓库里面,我新建了一个 Readme.md 文件,然后点击创建合并,这个时候你看 Gogs 的 WebHook 推送记录就有一条新的数据推送给 Jenkins,同时你也可以在 Jenkins 看到流水线任务被触发了。</p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113718935-1327196420.png" alt="" loading="lazy"></p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113725564-2009048117.png" alt="" loading="lazy"></p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113732433-1470341093.png" alt="" loading="lazy"></p>
<h3 id="34-限定任务触发条件">3.4 限定任务触发条件</h3>
<p>通过上面的步骤,我们已经将 Gogs 和 Jenkins 中的具体任务进行了绑定。不过还有一个比较尴尬的问题是,Gogs 的合并事件不仅仅包括创建合并,它的原始描述是这样说的。</p>
<blockquote>
<p>合并请求事件包括合并被开启、关闭、重新开启、编辑、指派、取消指派、更新标签、清除标签、设置里程碑、取消设置里程碑或代码同步。</p>
</blockquote>
<p>如果我们仅仅是依靠上面的配置,那么上述所有行为都会触发构建操作,这肯定不是我们想要的效果。还好 Generic Webhook 为我们提供了变量获取,以及 Webhook 过滤。</p>
<p>我们从 Gogs 发往 Jenkins 的请求中可以看到,在 JSON 内部包含了一个 <code>action</code> 字段,里面就是本次的操作标识。那么我们就可以想到通过判断 <code>action</code> 字段是否等于 <code>opened </code> 来触发流水线任务。</p>
<p>首先,我们增加 2 个 <strong>Post content parameters</strong> 参数,分别获取到 Gogs 传递过来的 <code>action</code> 和 PR 的 Id,这里我解释一下几个文本框的意思。</p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113742460-1492062582.png" alt="" loading="lazy"></p>
<p>除了这两个 Post 参数以外,在请求头中,Gogs 还携带了具体事件,我们将其一起作为过滤条件。<strong>需要注意的是,针对于请求头的参数,在转换成变量时,插件会将字符转为小写,并会使用 "_" 代替 "-"。</strong></p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113748838-1601168943.png" alt="" loading="lazy"></p>
<p>最后我们编写一个 <strong>Optional filter</strong> ,它的 Expression 参数是正则表达式,下面的 Text 即是源字符串。实现很简单,当 Text 里面的内容满足正则表达式的时候,就会触发流水线任务。</p>
<p>所以我们的 Text 字符串就是由上面三个变量的值组成,然后和我们预期的值进行匹配即可。</p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113756938-1393910079.png" alt="" loading="lazy"></p>
<blockquote>
<p>当然,你还想整一些更加炫酷的功能,可以使用 Jenkins 提供的 Http Request 之类的插件。因为 Gogs 提供了 API 接口,你就可以在构建完成之后,回写给 Gogs,用于提示构建结果。</p>
<p>这样的话,这种功能就有点像 Github 上面的机器人帐号了。</p>
</blockquote>
<h2 id="四完整的项目示例">四、完整的项目示例</h2>
<p>在上一节我们通过 Jenkins 的插件完成了远程仓库推送通知,当我们合并代码时,Jenkins 会自动触发执行我们的管道任务。接下来我将建立一个 .NET Core 项目,该项目拥有一个 Controller,接收到请求之后输出 “Hello World”。随后为该项目建立一个 xUnit 的测试项目,用于执行单元测试。</p>
<p>整个项目的结构如下图:</p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113806953-1248156331.png" alt="" loading="lazy"></p>
<p>我们需要编写一个 <code>UnitTest.Dockerfile</code> 镜像,用于执行 xUnit 单元测试。</p>
<pre><code class="language-dockerfile">FROM mcr.microsoft.com/dotnet/core/sdk:2.2

# 还原 NuGet 包。
WORKDIR /home/app
COPY ./ ./
RUN dotnet restore

ENTRYPOINT ["dotnet", "test" , "--verbosity=normal"]

</code></pre>
<p>之后为部署操作编写一个 <code>Deploy.Dockerfile</code> ,这个 Dockerfile 首先还原了 NuGet 包,然后通过 <code>dotnet publish</code> 命令发布了我们的网站。</p>
<pre><code class="language-dockerfile">FROM mcr.microsoft.com/dotnet/core/sdk:2.2 as build-image

# 还原 NuGet 包。
WORKDIR /home/app
COPY ./ ./
RUN dotnet restore

# 发布镜像。
COPY ./ ./
RUN dotnet publish ./TestProject.WebApi/TestProject.WebApi.csproj -o /publish/

FROM mcr.microsoft.com/dotnet/core/aspnet:2.2
WORKDIR /publish
COPY --from=build-image /publish .

ENTRYPOINT ["dotnet", "TestProject.WebApi.dll"]

</code></pre>
<p>两个 Dockerfile 编写完成之后,将其存放在项目的根目录,以便 Slave 进行构建。</p>
<p>Dockerfile 编写好了,那么我们还要分别为两个镜像编写 Docker Compose 文件,用于执行单元测试和部署行为,用于部署的文件名称叫做 <code>docker-compose.Deploy.yaml</code>,内容如下:</p>
<pre><code class="language-yaml">version: '3.1'

services:
backend:
    container_name: dev-test-backend
    image: dev-test:B${BUILD_NUMBER}
    ports:
      - '5000:5000'
    restart: always

</code></pre>
<p>然后我们需要编写运行单元测试的 Docker Compose 文件,名字叫做 <code>docker-compose.UnitTest.yaml</code>,内容如下:</p>
<pre><code class="language-yaml">version: '3.1'

services:
backend:
    container_name: dev-test-unit-test
    image: dev-test:TEST${BUILD_NUMBER}

</code></pre>
<h2 id="五编写-jenkinsfile">五、编写 Jenkinsfile</h2>
<pre><code class="language-jenkinsfile">node('docker') {

    stage '签出代码'
      checkout scm
    stage '单元测试'
      sh "docker build -t dev-test:TEST${BUILD_NUMBER} -f UnitTest.Dockerfile ."
      sh "docker-compose -f docker-compose.UnitTest.yaml up --force-recreate --abort-on-container-exit"
      sh "docker-compose -f docker-compose.UnitTest.yaml down -v"
    stage '部署项目'
      sh "docker build -t dev-test:B${BUILD_NUMBER} -f Deploy.Dockerfile ."
      sh 'docker-compose -f docker-compose.Deploy.yaml up -d'
}

</code></pre>
<h2 id="六最后的效果">六、最后的效果</h2>
<p>上述操作完成之后,将这些文件放在项目根目录。</p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113815906-2060625646.png" alt="" loading="lazy"></p>
<p>回到 Jenkins,你可以手动执行一下任务,然后项目就被成功执行了。</p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113823850-116890824.png" alt="" loading="lazy"></p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113831387-1140199274.png" alt="" loading="lazy"></p>
<p><img src="https://img2018.cnblogs.com/blog/1203160/201909/1203160-20190925113838387-77312898.png" alt="" loading="lazy"></p>
<p>至此,我们的 “低配版” CI、CD 环境就搭建成功了。</p><br><br>
来源:https://www.cnblogs.com/myzony/p/11583702.html
頁: [1]
查看完整版本: Jenkins 结合 Docker 为 .NET Core 项目实现低配版的 CI&CD