前言

产品需求评审后,各自拆分任务,从 master 分支切出一个 release 分支,根据各自任务情况切出 updatefeature 的开发分支;

开发调试或提测时,将代码 push 到远程分支,提 merge request(以下简称 mr)到 test 分支,GitLab CI 将项目代码自动构建并部署到测试环境;

测试完毕后提 mrrelease 分支,待本次需求的开发分支都 code review 并合并后,从 release 分支提 mrpre 分支,GitLab CI 将项目代码自动构建并部署到预生产环境,然后进行回归测试,有问题再从 release 分支切出开发分支进行修改,重复之前的流程。

预生产环境没问题后,从 release 分支提 mrmaster 分支,,然后打 tag 上线,GitLab CI 将项目代码自动构建并部署到生产环境,然后进行回归测试,有问题再发版。

至此一次需求的完整开发流程就告一段落了,其中构建/部署等一些重复工作都是 GitLab CI 帮我们完成,对此一直很好奇,接下来我们就来尝试搭建一个使用 GitLab CI 的项目。

搭建新项目

现有项目中使用 GitLab CI 可以直接跳过这步,从这里开始

可以按下面的步骤一步一步搭建,也可以直接克隆这个仓库:gitlab-ci-example

初始化项目

新建项目文件夹

mkdir gitlab-ci-example
cd gitlab-ci-example

初始化 git 和 npm

git init
npm init -y

新建项目文件

mkdir src build

新建 .gitignore 文件

gitlab-ci-example/.gitignore

dist
node_modules

新建 .editorconfig 文件

gitlab-ci-example/.editorconfig

# editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

新建 index.html 文件

gitlab-ci-example/src/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <h1>Learn Gitlab CI</h1>
    </div>
  </body>
</html>

新建 main.js 文件

gitlab-ci-example/src/main.js

function appendElementToAPP({ tag = "div", content = "" }) {
  const appEle = document.getElementById("app");
  const newEle = document.createElement(tag);
  newEle.innerHTML = content;
  appEle.append(newEle);
}

appendElementToAPP({
  tag: "div",
  content: `append content by js on ${new Date().toUTCString()}`,
});

新建 webpack.dev.js 文件

gitlab-ci-example/build/webpack.dev.js

"use strict";
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

const resolve = (dir) => path.resolve(__dirname, "../", dir);

module.exports = {
  mode: "development",
  entry: {
    app: "./src/main.js",
  },
  output: {
    path: resolve("dist"),
    filename: "[name].[hash].js",
  },
  resolve: {
    extensions: [".js"],
  },
  devServer: {
    port: 8090,
    contentBase: resolve("dist"),
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: resolve("dist/index.html"),
      template: "src/index.html",
    }),
  ],
};

新建 webpack.prod.js 文件

gitlab-ci-example/build/webpack.prod.js

"use strict";
const path = require("path");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");

const resolve = (dir) => path.resolve(__dirname, "../", dir);

module.exports = {
  mode: "production",
  entry: {
    app: "./src/main.js",
  },
  output: {
    path: resolve("dist"),
    filename: "[name].[hash].js",
  },
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      filename: resolve("dist/index.html"),
      template: "src/index.html",
    }),
  ],
};

新建 append-element.js 文件

gitlab-ci-example/build/append-element.js.sh

const path = require("path");
const fs = require("fs");
const cheerio = require("cheerio");

const htmlFilePath = path.resolve(__dirname, "../dist/index.html");

fs.readFile(htmlFilePath, (err, data) => {
  if (err) {
    return;
  }
  const $ = cheerio.load(data);
  $("#app").append(
    `<div style="color: red;">append content by build on ${new Date().toUTCString()}</div>`
  );
  fs.writeFileSync(htmlFilePath, $.html());
});

新建 deploy-test.sh 文件

gitlab-ci-example/build/deploy-test.sh

cp -rf dist/* /www/test/gitlab-ci-example

修改 package.json 文件

gitlab-ci-example/package.json

{
  "name": "gitlab-ci-example",
  "version": "0.0.1",
  "description": "",
  "main": "index.js",
  "scripts": {
    "deploy-test": "build/deploy-test.sh",
    "dev": "webpack-dev-server --config build/webpack.dev.js",
    "build": "webpack --config build/webpack.prod.js && node build/append-element.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

安装项目依赖

npm i -D cheerio webpack webpack-cli webpack-dev-server clean-webpack-plugin html-webpack-plugin

运行项目

npm run dev

在浏览器中打开链接:http://localhost:8090/ ,你应该能看到:

打包项目

npm run build

在浏览器中打开 dist 目录下的 index.html 文件,你应该能看到:

至此项目的基本功能搭建完成,接下来开始在项目中使用 GitLab CI


项目中使用 GitLab CI

使用 GitLab CI之前,你得先准备一下:

  • 一台云服务器
  • 一个 GitLab 仓库

设置 GitLab Runner

在仓库主页,点击侧边栏 - Settings - CI / CD,跳转 CI / CD Settings 页面,展开 Runners 选项,按步骤手动设置 GitLab Runner

安装 GitLab Runner

根据系统架构,下载并安装对应的软件包,查看详情

# 下载(适用于amd64的软件包)
curl -LJO https://gitlab-runner-downloads.s3.amazonaws.com/latest/deb/gitlab-runner_amd64.deb

# 如果下载太慢,建议在本地下载好之后,通过scp命令复制到远程,类似这样
# scp ~/gitlab-runner_amd64.deb yourUserName@yourIPAddress:/home/yourUserName

# 安装
sudo dpkg -i gitlab-runner_amd64.deb
# 输出
Selecting previously unselected package gitlab-runner.
(Reading database ... 67015 files and directories currently installed.)
Preparing to unpack gitlab-runner_amd64.deb ...
Unpacking gitlab-runner (13.0.1) ...
Setting up gitlab-runner (13.0.1) ...
GitLab Runner: detected user gitlab-runner
Runtime platform                                    arch=amd64 os=linux pid=28968 revision=21cb397c version=13.0.1
gitlab-runner: Service is not installed.
Runtime platform                                    arch=amd64 os=linux pid=28975 revision=21cb397c version=13.0.1
gitlab-ci-multi-runner: Service is not installed.
Runtime platform                                    arch=amd64 os=linux pid=28993 revision=21cb397c version=13.0.1
Runtime platform                                    arch=amd64 os=linux pid=29039 revision=21cb397c version=13.0.1

# 如果你收到类似上面的报错,运行下面的命令,如果能输出信息表示正常
sudo gitlab-runner status
# 输出
Runtime platform                                    arch=amd64 os=linux pid=29971 revision=21cb397c version=13.0.1
gitlab-runner: Service is running!

对于上面的报错信息,可以看看这个 gitlab issue

注册 GitLab Runnner

开始注册,下面是 Linux 的例子,其他系统请看这里

# 注册
sudo gitlab-runner register
# 输出
Runtime platform                                    arch=amd64 os=linux pid=31237 revision=21cb397c version=13.0.1
Running in system-mode.

# 指定 GitLab 实例 URL
Please enter the gitlab-ci coordinator URL (e.g. https://gitlab.com/):
https://gitlab.com/

# 输入注册令牌(从项目-设置-CI/CD 设置-Runners 那里拷贝)
Please enter the gitlab-ci token for this runner:
JhXh7o********yDXATd

# 输入描述
Please enter the gitlab-ci description for this runner:
[hostname]: runner-001

# 输入关联标签
Please enter the gitlab-ci tags for this runner (comma separated):
runner-001-tag
# 输出
Registering runner... succeeded                     runner=JhXh7oEx

# 选择执行环境,这里选择的是 shell
Please enter the executor: virtualbox, docker-ssh+machine, kubernetes, parallels, shell, ssh, docker+machine, custom, docker, docker-ssh:
shell
# 输出
Runner registered successfully. Feel free to start it, but if it\'s running already the config should be automatically reloaded!

下载安装并注册完 Runner 后,返回 CI / CD Settings 页面,现在应该能看到项目关联的 Runner

配置 GitLab CI

设置完 GitLab Runner 后,我们就可以开始配置 GitLab CI 了,新建 .gitlab-ci.yml 文件

gitlab-ci-example/.gitlab-ci.yml

# 工作名称
job-test:
  # 阶段
  stage: test
  # 触发条件:test 分支更新时
  only:
    - test
  # 指定工作给具有特定标签的 Runners
  tags:
    - runner-001-tag
  # 脚本
  script:
    - npm install
    - npm run build
    - npm run deploy-test

默认情况下 GitLab Runner 不会运行没有 tags 的工作,所以这里我们指定注册 GitLab Runner 时候设置的标签:runner-001-tag查看更多 GitLab CI/CD 配置选项

如果你不想设置 tags,可以修改 GitLab Runner 的配置,勾选 Run untagged jobs,表示允许 GitLab Runner 运行没有设置 tags 的任务。

保存 .gitlab-ci.yml 文件后,将改动 push 到远程仓库

触发 GitLab CI

配置文件有了之后,我们需要将其触发,从包含上面改动的分支,切出一个 test 分支,提交到远程,用于触发 GitLab CI(新提交和合并 test 分支都会触发 CI ),当然通过图形化界面创建 test 分支也是可以的

git checkout test
git push -u origin test

在仓库主页,点击侧边栏 - CI / CD - Pipelines,就能看到当前仓库所有的 CI 记录,类似下面这样:


遇到的问题

1. mkdir: cannot create directory ‘/home/gitlab-runner/builds/3-1Hb5zy’: Permission denied

 Running with gitlab-runner 13.0.1 (21cb397c)
   on runner-001 3-1Hb5zy
Preparing the "shell" executor 00:00
 Using Shell executor...
Preparing environment 00:00
 Running on xx-ubuntu...
Getting source from Git repository 00:00
 mkdir: cannot create directory ‘/home/gitlab-runner/builds/3-1Hb5zy’: Permission denied
Uploading artifacts for failed job 00:00
 mkdir: cannot create directory ‘/home/gitlab-runner/builds/3-1Hb5zy’: Permission denied
 ERROR: Job failed: exit status 1

原因:

将代码 push 到远程之后,构建出现了上面的报错,GitLab Runner 构建时使用的是 gitlab-runner 用户,创建目录的时候提示权限不足,尝试查看目录信息:

# 查看文件和目录信息
ls -alF /home/gitlab-runner
# drwxr-xr-x 4 root          root          4096 Jun  2 17:45 builds/

当前目录的权限和权限组都是 rootgitlab-runner 用户不在 root 权限组下,所以没权限操作。

仔细想想 🤔 发现不对劲,GitLab Runner 构建时使用的是 gitlab-runner 用户,但是为什么 builds 目录在 root 权限组下?回想一下在此之前做过哪些和 root 用户相关的操作,经过确认和查阅资料后发现,原来是在这次构建之前,手动安装服务(gitlab-runner install)的时候指定使用 root 用户(--user root)导致的:

# 安装服务,指定工作目录,指定运行任务的用户为 root 用户
sudo gitlab-runner install --working-directory /home/gitlab-runner --user root

如何解决:

删除 builds 目录、卸载重装 gitlab-runner 服务,将服务关联的用户指回 gitlab-runner 用户

# 停止服务
sudo gitlab-runner stop
# 卸载服务
sudo gitlab-runner uninstall
# 重新安装服务,指定工作目录和用户
sudo gitlab-runner install --working-directory /home/gitlab-runner --user gitlab-runner
# 完整配置
# sudo gitlab-runner install --working-directory /home/gitlab-runner --config /etc/gitlab-runner/config.toml --service gitlab-runner --syslog --user gitlab-runner
# 校验
sudo gitlab-runner verify
# 启动服务
sudo gitlab-runner start
# 查看状态
sudo gitlab-runner status
# 再次查看文件和目录信息
ls -alF /home/gitlab-runner
# drwxrwxr-x 3 gitlab-runner gitlab-runner 4096 Jun  3 16:21 builds/

现在 builds 目录的权限归回 gitlab-runner 用户所有了,在 gitlab 仓库的 PipelinesJobs 页面找到这次工作关联的 retry 按钮,点击按钮尝试重新运行构建

2. bash: line 92: npm: command not found

 Running with gitlab-runner 13.0.1 (21cb397c)
   on runner-001 3-1Hb5zy
Preparing the "shell" executor  00:00
 Using Shell executor...
Preparing environment 00:00
 Running on VM-0-5-ubuntu...
Getting source from Git repository  00:03
 Fetching changes with git depth set to 50...
 Reinitialized existing Git repository in /home/gitlab-runner/builds/3-1Hb5zy/0/Lsnsh/gitlab-ci-example/.git/
 Checking out 4e716630 as test...
 Skipping Git submodules setup
Restoring cache 00:00
Downloading artifacts 00:00
Running before_script and script  00:00
 $ npm install
 bash: line 92: npm: command not found
Running after_script  00:00
Uploading artifacts for failed job  00:00
 ERROR: Job failed: exit status 1

原因:

重装服务后 retry 构建后,出现了上面的报错,原因是因为 gitlab-runner 用户所处的环境没有安装 node 导致的(默认情况下在 root 或者其他用户上安装的 nodegitlab-runner 用户所处环境是访问不到的)

如何解决:

登录服务器,切换到 gitlab-runner 用户,安装 nvm,再安装 node

# 切换到 root 用户
sudo su
# 登录 gitlab-runner 用户
su -l gitlab-runner
# 安装 nvm(https://github.com/nvm-sh/nvm),如果访问脚本443,尝试用其他方式安装 nvm 或者直接安装 node
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash
# 查看 nvm
nvm ls
# 安装 node(截止目前最新LTS版本为:12.18.0,自行选择版本安装)
nvm install 12.16.2
# 查看 node 和 npm 版本
node -v
npm -v

现在 gitlab-runner 用户所处环境已经安装 node 了,高兴的尝试 retry 后,发现依然是 bash: line 92: npm: command not found,一度以为是错觉、是服务没有检测到 node 的存在,尝试重装、重启 gitlab-runner 服务后 retry 依然是 failed

冷静一番后,查阅大量类似案例,初步判断可能是环境变量没加载,也就是 nvm 没有加载导致的,尝试在构建过程中手动加载 ~/.bashrc 文件:

before_script:
  - source ~/.bashrc

重新 retry 后依然还是 failed,最后还是在 ~/.profile~/.bashrc 两个配置文件头部的一些注释信息里,找到了一些新的灵感:

~/.profile

# ~/.profile: executed by the command interpreter for login shells.
# ...

直译过来:~/.profile: 由命令解释器针对登录 shell 执行。

~/.bashrc

# ~/.bashrc: executed by bash(1) for non-login shells.
# ...

直译过来:~/.bashrc:由 bash(1) 对非登录 shell 执行。

以上信息中提到了登录与非登录两种状态,配置文件在对应状态下才会执行,通过添加调试信息发现,在 gitlab-runner 执行任务构建时,不会加载 ~/.bashrc 文件,只会加载 ~/.profile 文件;而通过 ssh 登录服务器时,两个文件都会加载,是不是有些疑惑 🤔,这是因为 ~/.profile 文件在开头会根据环境(bash)决定是否要先加载 ~/.bashrc 文件,具体代码如下:

# ~/.profile: executed by the command interpreter for login shells.
# This file is not read by bash(1), if ~/.bash_profile or ~/.bash_login
# exists.
# see /usr/share/doc/bash/examples/startup-files for examples.
# the files are located in the bash-doc package.

# the default umask is set in /etc/profile; for setting the umask
# for ssh logins, install and configure the libpam-umask package.
#umask 022

# if running bash
if [ -n "$BASH_VERSION" ]; then
    # include .bashrc if it exists
    if [ -f "$HOME/.bashrc" ]; then
	. "$HOME/.bashrc"
    fi
fi

要解决 npm 命令找不到这个问题,需要在 ~/.profile 配置文件添加上加载 nvm 的代码:

# 编辑配置文件
vi ~/.profile
# 配置 nvm 加载,将下面的代码添加到配置文件中(https://github.com/nvm-sh/nvm#installing-and-updating)
export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm
# 保存配置文件后,重新加载配置文件
source ~/.profile

3. sh: 1: build/deploy-test.sh: Permission denied

$ npm run deploy-test
> gitlab-ci-example@1.0.0 deploy-test /home/ubuntu/builds/3-1Hb5zy/0/Lsnsh/gitlab-ci-example
> build/deploy-test.sh
sh: 1: build/deploy-test.sh: Permission denied
npm ERR! code ELIFECYCLE
npm ERR! errno 126
npm ERR! gitlab-ci-example@1.0.0 deploy-test: `build/deploy-test.sh`
npm ERR! Exit status 126
npm ERR!
npm ERR! Failed at the gitlab-ci-example@1.0.0 deploy-test script.
# ...

原因:

# 在项目构建目录下(类似:/home/ubuntu/builds/3-1Hb5zy/0/Lsnsh/gitlab-ci-example),查看部署脚本的权限信息
ls -alF ./build/deploy-test.sh
# -rw-rw-r-- 1 ubuntu ubuntu   42 Jun  2 19:40 deploy-test.sh

通过以上命令发现和搜索相关问题,发现 deploy-test.sh 文件不具备可执行权限,所以无法执行

如何解决:

  1. 直接更改 deploy-test.sh 文件权限
# 表明 deploy-test.sh 文件是可执行的
git update-index --chmod=+x ./build/deploy-test.sh
# 改动会直接进入暂存区,编辑器的 git state 可能表明还有新的更改,忽略后直接提交本次更改后,git state 的状态会更新
git commit -m 'Make build.sh executable'
# 提交到远程,master, test 或者其他分支
git push
  1. 通过 sh 命名执行 deploy-test.sh 文件
# package.json
- "deploy-test": "build/deploy-test.sh",
+ "deploy-test": "sh build/deploy-test.sh",

4. /www/test/gitlab-ci-example: No such file or directory

$ npm run deploy-test
> gitlab-ci-example@1.0.0 deploy-test /home/gitlab-runner/builds/3-1Hb5zy/0/Lsnsh/gitlab-ci-example
> build/deploy-test.sh
/www/test/gitlab-ci-example: No such file or directory
Please create this directory and then assign the directory permissions to the gitlab-runner user.
You can execute the following command as root:
mkdir /www/test/gitlab-ci-example
chown gitlab-runner /www/test/gitlab-ci-example

原因:

build/deploy-test.sh 脚本会将构建好的代码,拷贝到 /www/test/gitlab-ci-example 目录下,因此在构建之前需要先创建好这个目录

如何解决:

参考 build/deploy-test.sh 脚本中打印出的提示信息,新建并分配目录权限即可:

# 新建目录(使用 root 用户或者其他 gitlab-runner 用户以为的用户)
mkdir /www/test/gitlab-ci-example
# 分配 gitlab-runner 用户文件夹权限
chown gitlab-runner /www/test/gitlab-ci-example

总结

至此,CI 终于可以跑通了,部署后页面的内容是这样的,点击查看

通过 .gitlab-ci.yml 配置文件,你可以在构建的各个阶段做处理,比如你可以在 before_scriptafter_script 阶段调用钉钉机器人接口,及时将部署状态同步到个人/群:

before_script:
  # 钉钉通知 钉钉群
  - curl -X POST 'https://oapi.dingtalk.com/robot/send?access_token=xxx&xxxx'

通知类似下面这样:

更多关于 .gitlab-ci.yml 文件的配置信息,请看官方文档

好利用 CI / CD 这件工具,相信会大大提升团队协作和开发效率。万事开头难,起初肯定会有抵触心理,迈过这道坎之后,还有下一道坎等着你[手动狗头]

示例项目的仓库链接如下,欢迎 star 🌟:

github 仓库(template):https://github.com/Lsnsh/gitlab-ci-example

gitlab 仓库:https://gitlab.com/Lsnsh/gitlab-ci-example


命令汇总

# 查看 gitlab-runner 相关进程
ps aux|grep gitlab-runner

# 注册 gitlab-runner
sudo gitlab-runner register

# 重装 gitlab-runner 服务
# 停止服务
sudo gitlab-runner stop
# 卸载服务
sudo gitlab-runner uninstall
# 重新安装服务,指定工作目录和用户
sudo gitlab-runner install --working-directory /home/gitlab-runner --user gitlab-runner
# 完整配置
# sudo gitlab-runner install --working-directory /home/gitlab-runner --config /etc/gitlab-runner/config.toml --service gitlab-runner --syslog --user gitlab-runner
# 校验
sudo gitlab-runner verify
# 启动服务
sudo gitlab-runner start
# 查看状态
sudo gitlab-runner status

# 拷贝文件到远程主机
# scp ~/gitlab-runner_amd64.deb yourUserName@yourIPAddress:/home/yourUserName
# eg: (将文件 ~/gitlab-runner_amd64.deb 拷贝到远程主机,公网 IP 为 110.120.130 的 root 用户目录下)
scp ~/gitlab-runner_amd64.deb root@110.120.130:/home/root

# 查看文件和目录信息
# eg: (查看 /home/gitlab-runner 目录)
ls -alF /home/gitlab-runner

# 切换到 root 用户
sudo su
# root 用户登录其他用户
# eg: (登录 gitlab-runner 用户)
su -l gitlab-runner

# 表明文件是可执行的
# git update-index --chmod=+x 文件路径
# 表明文件是不可执行的
# git update-index --chmod=-x 文件路径
# eg: (表明 ./build/deploy-test.sh 文件是可执行的)
git update-index --chmod=+x ./build/deploy-test.sh

# 给用户分配文件或文件夹权限
# chown 用户名 文件或文件夹路径
# eg: (分配 gitlab-runner 用户 /www/test/gitlab-ci-example 文件夹权限)
chown gitlab-runner /www/test/gitlab-ci-example

参考链接