Hugegraph集成CI+CD

实际开发中, 当项目模块众多, 依赖复杂的时候, 源码的编译部署依靠人工运维效率显然是很低的, 于是早些年就出现了DevOps的概念.

那么, 接下来就以Hugegraph讲一下如何在实际多模块项目里如何快速的集成CI+CD的环境, 然后内部开发如何单独维护一套版本, 在方便维护的前提下, 尽可能易于跟官方合并, 让社区的用户都能有更好的开发体验~

0x00. 主人公

业界已有的CICD +私有仓库的方案不在少数, 但侧重快速实用, 适用面最广的可能还是之前在Docker+K8s实现CICD系统里提到的Jenkins ,而且我们这次使用的JFrogArtifact作为私有包仓库也在对Jenkins 有单独的支持, 后续更易于整合, 关于这次的两个主人公就介绍完了(其他看官方文档), 下面说一下我们这次要实现的主线脉络: (引用两张图)

jfrogJenkins00

上图就清晰简单的说明了我们要实现的效果, 后续就大致沿着这个脉络去实现. 这里图中右侧的几个环境还不够直观, 那可以看看下面的对照:

cicdPipeline00

其中测试环境我们这次就和开发合在一起了, 准生产和生产也合并一下, 所以其实就 “开发 + 生产” 两个环境即可. 开始动手. 顺便在后文会着重讲一下”版本管理 + 官方同步“的问题, 也许那些会更刚需一些….

0x01. 实战

关于JenkinsJFrog 的安装启动官方已经封的很简单了, 文档也非常详细, 建议直接参考上面给的官方链接, 由于Docker 单独使用会增加不确定性和运维成本, 所以并不建议上手采用Docker模式运行 (Jenkins建议用RPM包方式安装配置, 利弊自查).现在假设我们已经搭好了”Jfrog+Jenkins”的环境, 重点说说如何使用和整合.

1. JFrog使用

JFrog有非常多的功能, 但我们都不用管:) 直接来看它包管理仓库的使用, 如图我们建立了几个不同的仓库, 开发测试snapshot , 正式发布用release 区分一下, 然后点Set me Up 可以获取相应的配置参数, 点Deploy 可以手动上传本地包.

jfrog00

首先这里需要搞清楚mvn的几个概念, 你在某个项目的pom.xml 里写了一个项目依赖, mvn做了哪几步事:

  1. 首先在本地的~/.m2/repository 里去找本地是否有这个包, 如果有就直接使用.
  2. 如果本地没有, 那再读取一个配置文件(setting.xml)去找远端的仓库地址, 然后再从远端仓库里去找. (默认的仓库地址应该是通过的, 下载速度比较慢)
  3. 如果你配置了一个本地的mvn仓库地址, 比如(Nexus或JFog的). 那有两种方式让项目读取本地仓库包:
    • 一是修改maven上面全局的配置文件, 让所有项目都先去从你本地mvn仓库找. (推荐)
    • 二是在当前项目的pom.xml 里通过配置<repositories> 项, 手动指定本项目使用的仓库地址.

综上大部分时候我们肯定是采用修改全局配置的方式, 这样引入依赖和发布包都方便许多, 也不需要到处改pom.xml 文件, 那这里需要注意的是, 在win上, 你可能发现并没有看到~/.m2/settings.xml 这个文件存在, 主要可能是因为你IDEA使用的是自带的maven ,并且没有勾上Override User setting file 选项, 当然得清楚, 本质上这个文件是从你maven的安装目录比如maven3.x/conf/settings.xml 来的, 所以直接修改它是一样可以的, 文件里有一大堆注释, 我们先重点关注servers就行.

然后 , 需要注意的是Jfrog 里的仓库也有几个分类:

  • Local-repo : 本地仓库, 内部使用, 仓库内容不会向外部同步
  • Remote-repo : 远程仓库, 最常用. 不能往里面上传私有组件. (但是可以通过包含本地仓库, 达到一个复用关系)
  • Virtual-repo : 虚拟仓库, 它管理本地和远程库.

然后简单点, 从JFrog 界面点了Set Me Up 之后, 再点击Generate Maven Settings –> Generate Settings 就能看到可以直接覆盖默认settings.xml的文件了, 例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<?xml version="1.0" encoding="UTF-8"?>
<settings xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd" xmlns="http://maven.apache.org/SETTINGS/1.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<servers>
<!--注意这里的用户名密码, 要改为自己实际的,
否则如果没有匿名访问权限, 获取或部署都可能401失败
至于如何使用加密后的密码和这个函数..我暂时没看到,就先不管它了-->
<server>
<username>${security.getCurrentUsername()}</username>
<password>${security.getEscapedEncryptedPassword()!"*** Insert encrypted password here ***"}</password>
<id>central</id>
</server>
<server>
<username>${security.getCurrentUsername()}</username>
<password>${security.getEscapedEncryptedPassword()!"*** Insert encrypted password here ***"}</password>
<id>snapshots</id>
</server>
</servers>
<profiles>
<profile>
<repositories>
<repository>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>central</id>
<name>repo-release</name>
<url>http://serverIP:8081/artifactory/repo-release</url>
</repository>
<repository>
<snapshots />
<id>snapshots</id>
<name>repo-snapshot</name>
<url>http://serverIP:8081/artifactory/repo-snapshot</url>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>central</id>
<name>repo-release</name>
<url>http://serverIP:8081/artifactory/repo-release</url>
</pluginRepository>
<pluginRepository>
<snapshots />
<id>snapshots</id>
<name>repo-snapshot</name>
<url>http://serverIP:8081/artifactory/repo-snapshot</url>
</pluginRepository>
</pluginRepositories>
<id>artifactory</id>
</profile>
</profiles>
<activeProfiles>
<activeProfile>artifactory</activeProfile>
</activeProfiles>
</settings>

然后配置好之后, 理论上你就可以试试在项目里直接引入一个上传的测试jar包, 看看是否IDEA能自动解析导入了. (如果不能注意观察自动导包的URL是否正确, 然后建议重启一下IDEA, 避免大量缓存出现一些问题)

补充: IDEA测试的时候发现有些包明明已经导入成功了, 调用也没问题, 也没有本地/项目依赖冲突或者版本冲突, 但是就是提示红线(而且SNAPSHOT版就正常)…具体原因还不确定, 但是不影响使用. 如果发现根本原因我再来更新.

4.20更新: 如果SNAPSHOT包更新了, 没有自动发现, 最快的办法就是先把带日期的全路径复制到pom文件中, 等加载好再换回.

2. 部署

除了导入包, 还有个重要的功能就是部署包到Artifactory上, 那么这里需要在项目的pom.xml单独设置一下, 同样在Set Me Up 的默认页面就有相关配置参数, 比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
<distributionManagement>
<repository>
<id>central</id>
<name>serverIP-releases</name>
<!--注意这里的url,如果你仓库区分了release和snapshot, 那这里url改为release,而不是复制来的-->
<url>http://serverIP:8081/artifactory/repo-release-local</url>
</repository>
<snapshotRepository>
<id>snapshots</id>
<name>serverIP-snapshots</name>
<url>http://serverIP:8081/artifactory/repo-snapshot-local</url>
</snapshotRepository>
</distributionManagement>

最后在命令中执行mvn package deploy -DskipTests ,表示先打包, 然后上传到仓库, 并跳过测试. (生产环境严禁跳过测试), SNAPSHOT 是平常常用的测试版本, 需要修改项目pom中的:

1
2
3
4
<!--如果正式版本是0.9.0-->
<version>0.9.0</version>
<!--那么开发用snapshot版本就写0.9.0-SNAPSHOT,到时候mvn会自动帮你把SNAPSHOT包下最新的包引用进项目,而不用写带时间戳的那个版本号 -->
<version>0.9.0-SNAPSHOT</version>

这里要注意, 如果没有配置好maven的settings文件, 上传会提示401无权限访问. 每个需要上传包的项目都可以这样写达到同样效果, 后面在[maven模块优化](#0x02. Maven优化) 的顶层pom 里只需要写一个, 其他模块就能都复用了. (而不需要每个模块复制配置…)

2. Jenkins使用

进入Jenkin控制面板后, 首先关闭一下确定用不到的插件, 然后根据下面的两个选择, 安装各自所需核心插件比如BlueoceanArtifactory 插件,

然后你有两个选择:

  1. 使用旧的Jenkins-pipeline-stage方式, UI不好看, 但是易于和插件结合, 并且参考资料多不少. (pipeline1.0 ,不需要Jenkinsfile)
  2. 采用新的”BlueOcean + Jenkinsfile”模式, 可视化操作, UI直观简洁, 复用很方便, 是以后Pipeline模式的代表. (pipeline2.0, Jenkinsfile有一定学习成本)

为了方便大家复用, 我这里优先选择Blueocean + Jenkinsfile的方式, 大家之后参考做法, 就像复用Dockerfile一样可以自己很快构建, 而无需研究Jenkins的使用..

待补充Jenkinsfile文件…

1
2


3. 二者整合

整合参考官方文档, 这里有些小坑, 等我跑顺再写吧…

待补充…


0x02. Maven优化

1. 核心

因为Huge官方的模块很多, 但是内部维护单独的版本和依赖打包会很不方便, 所以这里选择把Huge的模块归总到一个项目里, 然后用子模块的方式去管理, 并且希望修改顶层版本号的时候, 其他子模块能自动变更版本. 保持统一: (官方版本单独fork不冲突)

1
2
3
4
5
6
7
#假设我们的目录结构是这样的
-jin #(顶层)
-module1
-m11
-m12
-module2
...

然后我们遵循以下几个步骤, 来一步步完善我们的“父-子” 模块管理, 对整个配置和原理也就更加清晰:

  1. 首先, 在顶层(root)的pom.xml 里添加version管理插件, 添加子模块, 并创建对应子模块(IDEA一键创建)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <groupId>num</groupId>
    <artifactId>jin</artifactId>
    <!--下面这行申明是一定需要的,好比告诉maven我是root-->
    <packaging>pom</packaging>
    <version>1.0</version>

    <modules>
    <module>module1</module>
    <module>module2</module>
    </modules>


    <build>
    <plugins>
    <plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>versions-maven-plugin</artifactId>
    <version>2.7</version>
    <configuration>
    <!--默认会自动生成一个旧版的备份文件,熟悉后可去,但就无法回滚了 -->
    <generateBackupPoms>true</generateBackupPoms>
    </configuration>
    </plugin>
    </plugins>
    </build>
  2. 然后在每个子模块pom.xml 中引入父模块声明, 这样就把父子模块关联了起来 (module2同理, IDEA完成后右侧有继承标志)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    <parent>
    <groupId>num</groupId>
    <artifactId>jin</artifactId>
    <version>1.0</version>
    </parent>

    <groupId>num</groupId>
    <artifactId>module1</artifactId>
    <!--↓如非必要↓, 子模块不单独设置自己的版本号,默认打包的时候会使用父模块的版本
    <version>1.0</version>
    -->
  3. 在顶层pom中大量使用版本变量, 使子模块高效复用 (然后子模块引用依赖的时候无需写版本, 只用写groupId+artifactId)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
        <properties>
    <!--所有依赖的外部jar包版本号统一设置在此-->
    <guava.version>27.0</guava.version>
    <!--当然,还可以设置一些全局属性-->
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>1.8</java.version>
    </properties>


    <!--这里分两种场景:一是子模块间内部相互引用, 二是引用外部包-->
    <dependencyManagement>
    <dependencies>
    <!--项目内的模块,版本号全部使用${project.version}指定,无需单独定义,直接取parent的version-->
    <dependency>
    <groupId>num</groupId>
    <artifactId>module1</artifactId>
    <version>${project.version}</version>
    </dependency>
    <dependency>
    <groupId>num</groupId>
    <artifactId>module2</artifactId>
    <version>${project.version}</version>
    </dependency>

    <!--外部模块,版本号全部使用占位符指定(需要单独声明)-->
    <dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>${guava.version}</version>
    </dependency>
    </dependencies>
    </dependencyManagement>
  4. 在顶层pom引入本地的Maven仓库配置, 这样子模块就不需要重复填写了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
     <!--给的是Artifactory的配置, Nexus也是类似的-->
    <distributionManagement>
    <repository>
    <id>central</id>
    <name>serverIP-releases</name>
    <url>http://serverIP:8081/artifactory/repo-release-local</url>
    </repository>
    <snapshotRepository>
    <id>snapshots</id>
    <name>serverIP-snapshots</name>
    <url>http://serverIP:8081/artifactory/repo-snapshot-local</url>
    </snapshotRepository>
    </distributionManagement>
  5. 最后执行一键修改版本的命令(许多参数默认值都不用改, 不过注意有些情况下子模块自己的版本不会被修改, 此时建议去掉.)

    1
    2
    # 设置新的版本号未1.2.0-SNAPSHOT (其他详细参考可参考官网,一般不需要单独修改)
    mvn versions:set -DnewVersion=1.1
  6. 尝试打包一下, 看看有没有报错

    1
    mvn clean install -DskipTests

2. 补充

a. 单个模块继承多个parent(重要)

当然, 实际使用里可能遇到一些比较麻烦的问题, 比如一个module 同时继承了两个parent ,但是pom里是不支持多继承的, 那怎么办呢? 参考Spring-boot官方. 比如Huge里原本继承了一个oss-parent ,那我们可以删掉原本的parent, 然后在最顶层pom里加入如下: (version对应映射单独配置.)

1
2
3
4
5
6
7
<dependency>
<groupId>org.sonatype.oss</groupId>
<artifactId>oss-parent</artifactId>
<version>${oss.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

当然, 还有一种可能的解决方案是在顶层的pom里把子类依赖的parent 搬过来, 但是这样做就算可以, 最多只能共用一个parent, 所以就不推荐使用了. (可行性?)

b. 优化.gitignore文件

首先建议去github上找一个大家使用的多的模板, 然后在定制化修改一下, 比如把pom.xml.versionsBackup 这个文件加进去.

0x03. 与官方库同步

这是很关键的一步, 并且有一些坑, 不注意会很容易出错, 我们单独构建了一套管理方案之后, 很多时候还需要从官方版本上cherry-pick 一些改动 , 但是此时你会发现因为项目结构不一样, 你并不能直接这样做了. 但是还有个方案, 就是让官方的目录结构跟我们一样, 然后单独创建一个新的分支 (比如github) , 再去合并.

首先, 根目录下不需要历史记录, 也不需要任何文件, 使用--orphan 参数代表无任何提交记录.

1
2
3
4
5
6
7
8
9
10
# 1.创建新的空记录分支
git checkout --orphan github

# 2.清空所有历史数据
git rm -rf . #如果还有文件夹残留,则手动删除,保证只剩一个.git文件夹

# 3.一次性下载全部所需模块(也方便你后续出错重建:)
git clone https://github.com/hugegraph/hugegraph.git && git clone https://github.com/hugegraph/hugegraph-client.git && \
git clone https://github.com/hugegraph/hugegraph-loader.git && git clone https://github.com/hugegraph/hugegraph-studio.git && \
git clone https://github.com/hugegraph/hugegraph-common.git && git clone https://github.com/hugegraph/hugegraph-tools.git

然后记得在根目录下放置一个.gitignore 文件, 然后0x02的根目录的pom.xml 来作为父管理几个子模块. 下面我以自己的尝试过程为例, 告诉大家这里存在哪些可能的坑, 当然也可以直接跳到[正确的做法](#2. 正确的做法)

1. 错误的做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1.尝试第一次提交
git add . && git commit -m "init from official github on 19.06.21"

# 2.很开心然后想切换分支回去. 发现Fatal了.
git checkout master
# 或者这里你用一个测试分支 test-master, 发现提示checkout会覆盖以下未被标记的文件
error: The following untracked working tree files would be overwritten by checkout:
hugegraph/.gitignore
hugegraph/.travis.yml
....
hugegraph/hugegraph-api/pom.xml
hugegraph/hugegraph-api/src/main/java/com/baidu/hugegraph/api/API.java
#....大概有70+文件提示.
Aborting

然后这时候你随便一搜“git checkout 无法切换”之类的 ,强制切换(-f)或者全部丢弃之类的, 就会发现文件都丢了, 而且就算全部丢弃, 下次这个github分支有任何的新拉取, 再切换还是一样的提示. 所以还是把根本原因搞清楚 ,再操作. 那先看看图 —– untracked files 是什么 :

gitFileStatus00

从图里你可以发现, 目录下所有文件只有两种状态:

  1. 未被标记 : 不存在历史记录中, 也没存入本地暂存区, 简单说就是还没纳入git管理.
  2. 已被标记 : 存在历史记录或本地暂存区中, 文件之后所有的变动都会被git跟踪检查到(tracked)
1
2
3
4
5
6
7
8
9
10
11
# 很简单的举例,在git目录下新建一个文件
touch newFile
# 查看git状态
git status
#输出如下
Untracked files:
(use "git add <file>..." to include in what will be committed)

newFile

nothing added to commit but untracked files present (use "git add" to track)

这样可能就比较好理解了, 我们切换分支的时候, 还有一些文件根本没被git成功”纳入”. 此时切换分支, 这些文件要么丢弃, 要么被覆盖了, 但这显然不是我们的初衷, 我们是希望这些文件都保留下来. 但是这里诡异的是, 我们是新分支, 而且使用git status 查看也都提示nothing to commit, working directory clean, 并没有显示有未跟踪文件呢. 别急, 一步步来排查

  1. 使用git status --ignored 排除是不是被放入忽略文件了. (√)

  2. 手动添加提示没被跟踪的文件, 发现问题

    1
    2
    3
    git add hugegraph/.gitignore
    # 提示hugegraph是一个子模块
    fatal: Pathspec 'hugegraph/.gitignore' is in submodule 'hugegraph'

这里又涉及到另一个git子模块管理的概念, 当然这个并非我们想要的, 那为什么模块们都自己变成submodule 了呢? 这要回到最开始我们添加这些文件的时候的git add . 命令, 这个命令平常随便用没问题, 在这个嵌套.git文件夹的时候, 就非常蛋疼了, 它会自动把.git的子目录当为子模块, 从而自动忽视所有子模块的内容, 只是把一个空的文件夹添加到了git仓库中, 让我们误以为成功了…. 而且这个是强制自动执行的, 那么如何解决呢, 大致3个思路,

2. 正确的做法

  • 使用git submodule 子模块管理 (不合适, 我们这只是单纯组织管理下文件, 引入不必要的复杂度.)

  • 删除所有模块的.git 文件夹 (不合适, 这适用于0x02, 但这里我们要经常和官方同步, 当然不能删掉.)

  • 在根目录的.gitignore 中添加.git 忽略 (似乎符合?试试)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    # 0.添加忽略.git文件夹,请先把.gitignore,shell之类的放入
    echo ".git" >> .gitignore

    # 1.1 先尝试添加一个模块 (失败)
    git add hugegraph #糟糕, 仍然提示添加子模块, 实际又只加了个文件夹.为什么.gitignore没有生效呢?

    # 1.2 尝试单独添加里面的一个文件 (成功)
    git add hugegraph/.gitignore

    # 1.3 那会不会是git add的检测机制的问题呢? 尝试用 hugegraph/ ,直接使用 hugegraph会被当成一个整体添加. 而不是单纯的文件夹
    git add hugegraph/ # 大功告成! 再git status看一下发现文件都加了进去, 并且没有.git文件夹. 符合我们的需要

    # 1.批量一次性添加,测试用通配符'*'似乎不OK(也不能写成一条add命令,原因暂时未知..)
    git add hugegraph/ && git add hugegraph-client/ && git add hugegraph-common/ && \
    git add hugegraph-loader/ && git add hugegraph-tools/ && git add hugegraph-studio/ && \
    git add .

    # 2. 提交为初次记录
    git commit -m "init from official github on 19.06.21"

    # 补充:如果添加studio的时候提示有CRLF存在...批量转换一下. (Win下常见..)
    dos2unix hugegraph-studio/studio-ui/assets/vendors/bootstrap-select/css/bootstrap-select.min.css

    你再切换到其他分支就会发现丝滑顺畅了~ 之后需要和官方哪个模块进行同步合并, 就切换到github分支, 然后进目录里单独git pull 一下就行. 当然合并操作推荐使用IDEA 自带的强大对比功能, 而不是手动的复制粘贴. (不过这里存在个问题..见下)

3. 棘手的问题

不过这里还有个比较麻烦的事, 就是实测IDEA(2018)对带有.git文件夹的模块合并的时候会提示Bad version xxx , 实测重命名或去掉.git 文件后, 对比合并就很正常了…. 但是这样每次更新都得手动把.git 文件夹名改一下, 然后提交为修改… 虽然可以写个脚本做, 但还是很难受…(git子模块不知道会不会解决这问题). 姑且先这样吧, 下面是我自己简单写的脚本, 如果有改进或其他的思路, 欢迎及时告知~

  1. 先批量重命名 : (直接执行, 只需要第一次使用)

    1
    2
    for module in 'hugegraph' 'hugegraph-loader' 'hugegraph-client' 'hugegraph-studio' 'hugegraph-common' 'hugegraph-tools'; \
    do mv -v ${module}/.git ${module}/git_bak;done;
  1. 批量更新: (放根目录下, 命名update.sh , 每次需要更新就执行一次)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #!/usr/bin/env bash

    modules=(hugegraph hugegraph-loader hugegraph-client hugegraph-studio hugegraph-common hugegraph-tools)

    # 重命名.git文件夹,并不设置为隐藏,否则其他分支还可以看到..容易误操作
    for module in "${modules[@]}"; do
    cd $module || exit # exit for safety
    mv -v git_bak .git || exit
    git pull
    mv -v .git git_bak || exit
    cd .. || exit
    done
    git add . && git commit -m "useless operation, rebase it together" || echo "Update failed, please check"
    echo 'Update all modules from github OK'

这样处理完, 再切换回你自己单独的分支, 就可以愉快的合并代码了…. 整个过程是有点艰辛, 但是我想大家在我的基础上参考改进, 应该会好一些…

补充: 这里拉取代码的时候选择的是官方库, 如果你想给官方提交PR贡献, 那么开始拉取的时候应该选择你自己Fork 之后的链接为好, 但是这同样有些比较蛋疼的问题, 所以我暂时先没这样, 还是单独维护另一个版本先…:(

0x04. 总结 & 反思:

总结:

总结待完成再补….

反思:

  1. 还记得Linus邮件常Read the fuxking manual (RTFM). 但是很多时候我还是不经意的忽略了一些Unix返回警告(Warning)信息, 这次这个git add . 导致的问题本来也有输出信息, 但是被一带而过的忽视了, 才导致这个问题变复杂了许多.
  2. 遇到不熟悉领域的问题, 还是小心参考网上很多教程, 啪啪啪几下命令下去, 可能你上了天堂, 也可能下了地狱. 耐心一点分析一下背后的原理, 搞清楚这步到底是在做什么, 我的问题是不是需要这样, 很多时候也许你的问题跟别人说的并非一回事. 错误的依葫芦画瓢, 最后可能导致你的问题变得不可弥补…
  3. 严谨一点, 再严谨一点…. add diradd dir/ 在有些时候, 就有很大的区别…. 切勿惯性拍脑袋, 强迫自己做事一步步尝试, 而不要一下就觉得对或不对

参考资料:
  1. 持续交付:当前普遍存在的三个问题与解决方案
  2. jfrog artifactory jenkins pipeline 集成
  3. git-submodule