如何使用 Gitlab 搭配 Gitlab CI/CD 進行自動化測試

如何對在 Gitlab 上面的專案進行 CI/CD 可以怎麼做呢? Gitlab 可以搭配 Gitlab CI/CD 來執行 CI/CD , Gitlab CI/CD 會依照所設定的設定檔將指令交由 Gitlab Runner 來執行進而完成整個 Pipeline ,在 Gitlab CI 的世界有幾個名詞。

  • Pipeline: 一個 CI/CD 的流程,串接多個 Job 與多個 Stage 組成,任何一個 Job 失敗都視為這個 Pipeline 失敗(除非該 Job 有設定為 allow fail )。
  • Stage: 階段,一個 Stage 可以由多個 Job 組成,同個 Stage 中的 Job 都必須通過才會執行下個 Stage 。
  • Job: 工作,依照 .gitlab-ci.yaml 所設定之設定交由 Gitlab Runner 來執行,並且提交這個 Job 的執行狀態(綠燈、橘燈、紅燈)。

在 repository 根目錄直接新增 .gitlab-ci.yaml 檔案,這邊有個範例給大家參考。

stages: // 定義這個 Pipeline 有的 Stage
  - build // Stage 名稱
  - test

Build: // Job 名稱
  stage: build // stage name 需要與上方的 stages 名稱一致
  image: composer // 這個 Job 需要使用的 docker image ,以範例來說使用的是 compose:latest
  cache:
    key: "$CI_COMMIT_REF_SLUG"
    paths:
      - vendor/
  script: // 需要執行的指令
    - composer install --ignore-platform-reqs
  artifacts: // 透過 artifacts 將 vendor 這個資料夾 pass 給下個 Stage 的 Job 使用
    expire_in: '10 mins'
    paths:
      - vendor/

Test:
  stage: test
  image: php:7.4-cli
  script:
    - ./vendor/bin/phpunit // 執行 PHPUnit 測試

通常在 Continuous Integration(CI) 階段我們會設計一些流程來進行專案的驗證,目的在於透過自動化導入檢測並且減少人為介入進而提升專案穩定度。

  • 專案安裝
  • 專案檢測(靜態程式碼分析、 coding style 檢查、 docker image best practice 檢測 …)
  • 專案測試
    • 單元測試 Unit Test
    • 整合測試 Acceptance Test
    • End-to-End Testing
  • 專案打包(在容器化世代通常會需要打包 docker image 供其他容器管理器進行使用)

當然在 CI 階段可以做的事情不只這些,就看專案的特性來進行調整即可,撰寫 .gitlab-ci.yaml 完成後執行 git push 推送到專案上就會生效了,即可在 Gitlab > Repo > CI/CD > Pipelines 看到執行結果。

https://gitlab.com/coosos810609/phpunit-demo-with-ci/pipelines/134780795

每個 Stage 中各自 Job 的執行狀態也可以直接點擊查看,畫面中可以看到會將設定在 .gitlab-ci.yaml 中的指令一一執行,並且將執行結果呈現在畫面中。

https://gitlab.com/coosos810609/phpunit-demo-with-ci/-/jobs/506685408

如果執行的過程中有任何的錯誤都可以透過這邊進行 Debug ,下篇再與大家分享如何優化所撰寫的 .gitlab-ci.yaml 檔案。

如何使用 gitlab-runner 進行 Pipeline Debug

安裝 gitlab-runner 的部分可以參考此篇文章,這邊就直接針對如何透過 gitlab-runner 指令來執行 .gitlab-ci.yml 特定的 pipeline 進行測試。

> cat .gitlab-ci.yml
...
Test elasticsearch on 6.x:
  stage: integration
  services:
   - name: elasticsearch:6.8.0
     alias: elasticsearch
  image: *image_ref56
  cache: *cache_ref56
  script:
      - make integration
  tags:
    - docker
...

從上面的 yaml 檔可以知道有一個 “Test elasticsearch on 6.x” 的 Pipeline ,而透過 gitlab-runner 指令可以直接模擬 Gitlab-CI 來執行這個 pipeline 。

> gitlab-runner exec docker --cache-shared --docker-privileged --docker-cache-dir /cache --builds-dir /builds --docker-privileged --docker-volumes /home/xxx/var/gitlab-runner/builds:/builds --docker-volumes /home/xxx/cache:/cache --docker-volumes /home/xxx/.ssh:/root/.ssh --docker-volumes /var/run/docker.sock:/var/run/docker.sock 'Test elasticsearch on 6.x'

就會看到與 Gitlab-CI 上一樣的執行結果了,上面的指定目錄再麻煩各位依照自己的環境進行調整。

此時想說有沒有可能在不進行 git push 的狀況下,也能透過 gitlab-runner 模擬 Gitlab-CI 的執行結果呢?

然而在我修改程式碼之後馬上執行了上面的指令,卻執行尚未修改前的程式,這是怎麼回事? gitlab-runner 有提供一個 --debug 的參數可以使用,或許可以發現些線索。

> gitlab-runner --debug exec docker --cache-shared --docker-privileged --docker-cache-dir /cache --builds-dir /builds --docker-privileged --docker-volumes /home/xxx/var/gitlab-runner/builds:/builds --docker-volumes /home/xxx/cache:/cache --docker-volumes /home/xxx/.ssh:/root/.ssh --docker-volumes /var/run/docker.sock:/var/run/docker.sock 'Test elasticsearch on 6.x'

...
Executing on /runner--project-0-concurrent-0-predefined-1 the #!/usr/bin/env bash

set -eo pipefail
set +o noclobber
: | eval $'export FF_CMD_DISABLE_DELAYED_ERROR_LEVEL_EXPANSION=$\'false\'\nexport FF_USE_LEGACY_BUILDS_DIR_FOR_DOCKER=$\'false\'\nexport FF_USE_LEGACY_VOLUMES_MOUNTING_ORDER=$\'false\'\nexport CI_RUNNER_SHORT_TOKEN=\'\'\nexport CI_BUILDS_DIR=$\'/builds\'\nexport CI_PROJECT_DIR=$\'/builds/0/project-0\'\nexport CI_CONCURRENT_ID=0\nexport CI_CONCURRENT_PROJECT_ID=0\nexport CI_SERVER=$\'yes\'\nexport CI=$\'true\'\nexport GITLAB_CI=$\'true\'\nexport CI_SERVER_NAME=$\'GitLab CI\'\nexport CI_SERVER_VERSION=\'\'\nexport CI_SERVER_REVISION=\'\'\nexport CI_PROJECT_ID=0\nexport CI_JOB_ID=1\nexport CI_JOB_NAME=$\'Test elasticsearch on 5.x\'\nexport CI_JOB_STAGE=$\'integration\'\nexport CI_JOB_TOKEN=\'\'\nexport CI_REPOSITORY_URL=$\'/home/xxx/repo/project-a\'\nexport CI_COMMIT_SHA=$\'74cd6c29f235b8be115fd54f97a163025ac49dfe\'\nexport CI_COMMIT_BEFORE_SHA=$\'88c146a08a8ed3cc6bc41b023ff064ea7d2c3b28\'\nexport CI_COMMIT_REF_NAME=$\'feature/boost-from-query\'\nexport COMPOSER_HOME=$\'/cache/composer\'\nexport CI_DISPOSABLE_ENVIRONMENT=$\'true\'\nexport CI_RUNNER_VERSION=$\'12.6.0~beta.2048.g59f07d78\'\nexport CI_RUNNER_REVISION=$\'59f07d78\'\nexport CI_RUNNER_EXECUTABLE_ARCH=$\'linux/amd64\'\nexport GIT_LFS_SKIP_SMUDGE=1\n$\'rm\' "-r" "-f" "/builds/0/project-0"\necho $\'\\x1b[32;1mFetching changes...\\x1b[0;m\'\n$\'mkdir\' "-p" "/builds/0/project-0.tmp/git-template"\n$\'git\' "config" "-f" "/builds/0/project-0.tmp/git-template/config" "fetch.recurseSubmodules" "false"\n$\'git\' "init" "/builds/0/project-0" "--template" "/builds/0/project-0.tmp/git-template"\n$\'cd\' "/builds/0/project-0"\n$\'rm\' "-f" ".git/index.lock"\n$\'rm\' "-f" ".git/shallow.lock"\n$\'rm\' "-f" ".git/HEAD.lock"\n$\'rm\' "-f" ".git/hooks/post-checkout"\nif $\'git\' "remote" "add" "origin" "/home/xxx/repo/project-a" >/dev/null 2>/dev/null; then\n  echo $\'\\x1b[32;1mCreated fresh repository.\\x1b[0;m\'\nelse\n  $\'git\' "remote" "set-url" "origin" "/home/xxx/repo/project-a"\nfi\n$\'git\' "fetch" "origin" "--prune"\necho $\'\\x1b[32;1mChecking out 74cd6c29 as feature/boost-from-query...\\x1b[0;m\'\n$\'git\' "checkout" "-f" "-q" "74cd6c29f235b8be115fd54f97a163025ac49dfe"\n$\'git\' "clean" "-ffdx"\nif $\'git\' "lfs" "version" >/dev/null 2>/dev/null; then\n  $\'git\' "lfs" "pull"\n  echo\nfi\necho $\'\\x1b[32;1mSkipping Git submodules setup\\x1b[0;m\'\n'
exit 0
...

後段可以看到一串指令將它整理一下。

Executing on /runner--project-0-concurrent-0-predefined-1 the #!/usr/bin/env bash

set -eo pipefail
set +o noclobber
: | eval $'
export FF_CMD_DISABLE_DELAYED_ERROR_LEVEL_EXPANSION=$\'false\'
export FF_USE_LEGACY_BUILDS_DIR_FOR_DOCKER=$\'false\'
export FF_USE_LEGACY_VOLUMES_MOUNTING_ORDER=$\'false\'
export CI_RUNNER_SHORT_TOKEN=\'\'
export CI_BUILDS_DIR=$\'/builds\'
export CI_PROJECT_DIR=$\'/builds/0/project-0\'
export CI_CONCURRENT_ID=0
export CI_CONCURRENT_PROJECT_ID=0
export CI_SERVER=$\'yes\'
export CI=$\'true\'
export GITLAB_CI=$\'true\'
export CI_SERVER_NAME=$\'GitLab CI\'
export CI_SERVER_VERSION=\'\'
export CI_SERVER_REVISION=\'\'
export CI_PROJECT_ID=0
export CI_JOB_ID=1
export CI_JOB_NAME=$\'Test elasticsearch on 5.x\'
export CI_JOB_STAGE=$\'integration\'
export CI_JOB_TOKEN=\'\'
export CI_REPOSITORY_URL=$\'/home/ubuntu/repo/project-a\'
export CI_COMMIT_SHA=$\'74cd6c29f235b8be115fd54f97a163025ac49dfe\'
export CI_COMMIT_BEFORE_SHA=$\'88c146a08a8ed3cc6bc41b023ff064ea7d2c3b28\'
export CI_COMMIT_REF_NAME=$\'feature/boost-from-query\'
export COMPOSER_HOME=$\'/cache/composer\'
export CI_DISPOSABLE_ENVIRONMENT=$\'true\'
export CI_RUNNER_VERSION=$\'12.6.0~beta.2048.g59f07d78\'
export CI_RUNNER_REVISION=$\'59f07d78\'
export CI_RUNNER_EXECUTABLE_ARCH=$\'linux/amd64\'
export GIT_LFS_SKIP_SMUDGE=1\n$\'rm\' "-r" "-f" "/builds/0/project-0"\necho $\'\\x1b[32;1mFetching changes...\\x1b[0;m\'\n$\'mkdir\' "-p" "/builds/0/project-0.tmp/git-template"\n$\'git\' "config" "-f" "/builds/0/project-0.tmp/git-template/config" "fetch.recurseSubmodules" "false"\n$\'git\' "init" "/builds/0/project-0" "--template" "/builds/0/project-0.tmp/git-template"\n$\'cd\' "/builds/0/project-0"\n$\'rm\' "-f" ".git/index.lock"\n$\'rm\' "-f" ".git/shallow.lock"\n$\'rm\' "-f" ".git/HEAD.lock"\n$\'rm\' "-f" ".git/hooks/post-checkout"\nif $\'git\' "remote" "add" "origin" "/home/xxx/repo/project-a" >/dev/null 2>/dev/null; then\n  echo $\'\\x1b[32;1mCreated fresh repository.\\x1b[0;m\'\nelse\n  $\'git\' "remote" "set-url" "origin" "/home/ubuntu/repo/project-a"\nfi\n$\'git\' "fetch" "origin" "--prune"\necho $\'\\x1b[32;1mChecking out 74cd6c29 as feature/boost-from-query...\\x1b[0;m\'\n$\'git\' "checkout" "-f" "-q" "74cd6c29f235b8be115fd54f97a163025ac49dfe"\n$\'git\' "clean" "-ffdx"\nif $\'git\' "lfs" "version" >/dev/null 2>/dev/null; then\n  $\'git\' "lfs" "pull"\n  echo\nfi\necho $\'\\x1b[32;1mSkipping Git submodules setup\\x1b[0;m\'\n'
exit 0

其中 GIT_LFS_SKIP_SMUDGE 環境參數後面執行的指令這邊吸引了我,將他整理一下…

> rm -rf /builds/0/project-0
> echo git-template
> $git config -f /builds/0/project-0.tmp/git-template/config fetch.recurseSubmodules false
> $git init /builds/0/project-0 --template /builds/0/project-0.tmp/git-template
> cd /builds/0/project-0
> rm -f .git/index.lock
> rm -f .git/shallow.lock
> rm -f .git/HEAD.lock
> rm -f .git/hooks/post-checkout
> if 
    $git remote add origin /home/xxx/repo/project-a >/dev/null 2>/dev/null;
then
    echo $\\x1b[32;1mCreated fresh repository.\\x1b[0;m
else
    $git remote set-url origin /home/xxx/repo/project-a
fi
> $git fetch origin --prune
> echo \\x1b[32;1mChecking out 74cd6c29 as feature/boost-from-query...\\x1b[0
> $git checkout -f -q 74cd6c29f235b8be115fd54f97a163025ac49dfe
> $git clean -ffdx
> if
    $git lfs version >/dev/null 2>/dev/null;
then
    $git lfs pull
fi
> echo \\x1b[32;1mSkipping Git submodules setup\\x1b[0;m

其中可以看到在設定 git remote 路徑的時候,他是抓取實體專案路徑,也就是說,如果 code change 沒有下 git commit 就無法讓 runner 抓取到,這下簡單了,只要 git commit 就好… 測試後確認成功。

如何在自己的環境安裝 gitlab-runner 指令

有使用 Gitlab 的朋友對於 Gitlab-CI 應該都不陌生,透過 CI 可以幫助開發者進行測試或是部署,相當的方便,但往往在執行 CI 的時候出錯要 debug 都要一直 push code 很麻煩, Gitlab 有將他的 runner 開源並提供方法讓大家在自己的環境安裝 gitlab-runner ,下面就來跟大家說怎麼安裝。

如果你只是想要使用 gitlab-runner 的指令,可以直接透過這份文件進行安裝即可,但如果你會有需要使用不同版本的 gitlab-runner 的需求,就可以參考這份文件進行操作,如果此環境你沒有 sudo 權限或是想要看懶人版可以直接往下看 xD

gitlab-runner 是使用 Golang 寫的,所以需要先進行 Golang 相關配置與安裝。

更新你的 .profile, .bash_profile, .zshrc 看習慣放哪裡,我自己都會放在 .zshrc

> vim ~/.zshrc
...
export GOPATH=$HOME/Go
export PATH=$PATH:$GOPATH/bin:$HOME/local/go/bin
...
> exec zsh
> mkdir ~/Go
> mkdir ~/local

下載與安裝 Golang 。

> cd
> wget https://storage.googleapis.com/golang/go1.10.8.linux-amd64.tar.gz
> tar -C local -xzf go1.10.8.linux-amd64.tar.gz

完成之後你就會在你的 ~/local/go 這邊看到 go 的原始碼,也可以開始使用 go 了,接著來確認是否有成功安裝

> go version
go version go1.10.8 linux/amd64

看到上面訊息代表你的 go 語言有成功的安裝,接下來安裝 gitlab-runner

> go get gitlab.com/gitlab-org/gitlab-runner

執行完成後你可以看到 ~/Go/bin 裡面有個 gitlab-runner 的執行檔,就可以直接使用了,或是要使用 $GOPATH/src/gitlab.com/gitlab-org/gitlab-runner/.gopath/bin/gitlab-runner 也可以,至於為什麼要知道後面這個路徑呢?後面會提到,測試一下 gitlab-runner 是否安裝成功。

╭─hashmanlin@ubuntu ~/Go/bin
╰─➤  gitlab-runner -v
Version:      development version
Git revision: HEAD
Git branch:   HEAD
GO version:   go1.10.8
Built:        unknown
OS/Arch:      linux/amd64

這邊可以看到安裝成功了,但會發現 Version 是 development version ,如果希望指定版本怎麼辦?舉例想要使用 11.11.4 的版本。

╭─hashmanlin@ubuntu ~/Go/src/gitlab.com/gitlab-org/gitlab-runner  ‹master›
╰─➤  git checkout v11.11.4
Note: switching to 'v11.11.4'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at e828d3bc9 Update CHANGELOG for v11.11.4
╭─hashmanlin@ubuntu ~/Go/src/gitlab.com/gitlab-org/gitlab-runner  ‹e828d3bc9›
╰─➤  make deps
mkdir -p /home/hashmanlin/Go/src/gitlab.com/gitlab-org/gitlab-runner/.gopath/bin
touch /home/hashmanlin/Go/src/gitlab.com/gitlab-org/gitlab-runner/.gopath/.ok
go get github.com/golang/dep/cmd/dep
go get github.com/mitchellh/gox
go get github.com/vektra/mockery/.../
╭─hashmanlin@ubuntu ~/Go/src/gitlab.com/gitlab-org/gitlab-runner  ‹e828d3bc9›
╰─➤  make install
go install --ldflags="-X gitlab.com/gitlab-org/gitlab-runner/common.NAME=gitlab-runner -X gitlab.com/gitlab-org/gitlab-runner/common.VERSION=11.11.4 -X gitlab.com/gitlab-org/gitlab-runner/common.REVISION=e828d3bc -X gitlab.com/gitlab-org/gitlab-runner/common.BUILT=2019-12-02T12:42:34+0000 -X gitlab.com/gitlab-org/gitlab-runner/common.BRANCH= -s -w" gitlab.com/gitlab-org/gitlab-runner
╭─hashmanlin@ubuntu ~/Go/src/gitlab.com/gitlab-org/gitlab-runner  ‹e828d3bc9›
╰─➤  ./.gopath/bin/gitlab-runner -v
Version:      11.11.4
Git revision: e828d3bc
Git branch:
GO version:   go1.10.8
Built:        2019-12-02T12:42:34+0000
OS/Arch:      linux/amd64

至於為什麼 $GOPATH/bin/gitlab-runner 沒有被置換成 11.11.4 的版本呢?看了一下 gitlab-runnerMakefile 會把 $GOPATH 改為 .gopath 這個路徑,如果希望 $GOPATH/bin/gitlab-runner 也跟著置換可以考慮用 ln 的方式來處理。

╭─hashmanlin@ubuntu ~/Go/bin
╰─➤  rm gitlab-runner
╭─hashmanlin@ubuntu ~/Go/bin
╰─➤  rm gitlab-runner
╭─hashmanlin@ubuntu ~/Go/bin
╰─➤  ln /home/hashmanlin/Go/src/gitlab.com/gitlab-org/gitlab-runner/.gopath/bin/gitlab-runner gitlab-runner
╭─hashmanlin@ubuntu ~/Go/bin
╰─➤  cd
╭─hashmanlin@ubuntu ~
╰─➤  gitlab-runner -v
Version:      11.11.4
Git revision: e828d3bc
Git branch:
GO version:   go1.10.8
Built:        2019-12-02T12:42:34+0000
OS/Arch:      linux/amd64

這樣就可以執行特定版本的 gitlab-runner