65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 GitHub & Gitlab 进行 Docker 化 PHP 应用的 CI 流水线

starIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

1.00/5 (5投票s)

2022 年 5 月 1 日

MIT

10分钟阅读

viewsIcon

7491

如何在 Github Actions 和 Gitlab Pipelines 上设置 CI 流水线来运行代码质量工具和测试

所有代码示例都可以在我的 GitHub 上的 Docker PHP 教程仓库 中公开获取。
您可以在 part-7-ci-pipeline-docker-php-gitlab-github 分支上找到本教程。

Docker PHP 教程已发布的部分

如果您想继续学习,请订阅 RSS 源通过电子邮件,以便在下一部分发布时获得自动通知。 :)

引言

CI 是 **C**ontinuous **I**ntegration(持续集成)的缩写,对我来说,它主要意味着 **在隔离的环境中运行代码库的代码质量工具和测试**(最好是自动化的)。这
在团队协作中尤其重要,因为 **CI 系统在功能或 bug 修复合并到主分支之前充当最终的守门员**。

我最初接触 CI 系统是在涉足开源领域时。以前,我为自己的项目使用了 Travis CI,后来在某个时候将其替换为 Github Actions。在 ABOUT YOU,我们最初使用的是自托管的 Jenkins 服务器,然后迁移到了 Gitlab CI 作为完全托管的解决方案(尽管我们使用 自定义 runner)。

推荐阅读

本教程建立在之前部分的基础上。我会尽力在必要时交叉引用相应的文章,但我仍然建议提前阅读有关

以及作为附加知识

方法

在本教程中,我将解释 **如何使我们现有的 Docker 设置与 Github Actions 和 Gitlab CI/CD Pipelines 协同工作**。由于我非常推崇“渐进增强”的方法,我们将确保 **所有必要的步骤都可以通过 make 在本地完成**。这还有一个额外的好处,就是保持一个单一的真相来源(Makefile),这在我们设置两个不同提供商(Github 和 Gitlab)的 CI 系统时会很有用。

通用流程将与本地开发非常相似

  • 构建 Docker 设置
  • 启动 Docker 设置
  • 运行 QA 工具
  • 运行测试

您可以在 CI 设置 部分看到最终结果,包括具体的 yml 文件和仓库链接,请参阅

在代码层面,我们将 **将 CI 视为一个环境**,通过环境变量 ENV 进行配置。到目前为止,我们只使用了 ENV=local,现在我们将扩展它以使用 ENV=ci。必要的更改将在具体 CI 设置说明之后的章节中进行解释

自己动手

为了感受一下发生了什么,您可以先 执行本地 CI 运行

这应该会给您一个与 执行示例 中呈现的类似输出。

git checkout part-7-ci-pipeline-docker-php-gitlab-github

# Initialize make
make make-init

# Execute the local CI run
bash .local-ci.sh

CI 设置

通用 CI 注意事项

为 CI 初始化 make

作为第一步,我们需要“配置”代码库以在 ci 环境下运行。这是通过 make-init 目标完成的,稍后将在 Makefile 更改 部分更详细地解释,通过

make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=12345678"
$ make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=12345678"
Created a local .make/.env file

ENV=ci 确保我们

TAG=latest 目前只是一个简化,因为我们还没有对镜像做任何处理。在接下来的教程中,我们将把它们推送到容器注册表以供将来用于生产部署,然后将 TAG 设置为更有意义的值(如构建号)。

EXECUTE_IN_CONTAINER=true 强制所有使用 RUN_IN_*_CONTAINER 设置make 命令在容器中运行。这一点很重要,因为 **Gitlab runner 本身将在一个 docker 容器中运行**。但是,这会导致任何受影响的目标 **省略 $(DOCKER_COMPOSER) exec 前缀**。

GPG_PASSWORD=12345678 是敏感 gpg 密钥的密码,如 添加密码保护的敏感 gpg 密钥 中所述。

wait-for-service.sh

我将在本文后面 添加 mysql 的健康检查 部分解释“容器已启动但底层服务未就绪”的问题以及我们如何解决它。我们有意不让 docker compose 来处理等待,因为我们可以“更好地利用等待时间”,而是用一个简单的 bash 脚本来实现它,该脚本位于 .docker/scripts/wait-for-service.sh

#!/bin/bash

name=$1
max=$2
interval=$3

[ -z "$1" ] && echo "Usage example: bash wait-for-service.sh mysql 5 1"
[ -z "$2" ] && max=30
[ -z "$3" ] && interval=1

echo "Waiting for service '$name' to become healthy, 
      checking every $interval second(s) for max. $max times"

while true; do 
  ((i++))
  echo "[$i/$max] ..."; 
  status=$(docker inspect --format "{{json .State.Health.Status }}" 
        "$(docker ps --filter name="$name" -q)")
  if echo "$status" | grep -q '"healthy"'; then 
   echo "SUCCESS";
   break
  fi
  if [ $i == $max ]; then 
    echo "FAIL"; 
    exit 1
  fi 
  sleep $interval; 
done

该脚本通过 检查 docker inspect 命令的 .State.Health.Status 信息 来等待 docker $service 变为“健康”。

注意:脚本使用 $(docker ps --filter name="$name" -q) 来确定容器的 ID,即它将“匹配”所有正在运行的容器与 $name - 如果匹配的容器超过一个,这就会失败!也就是说,您必须确保 $name 足够具体,能够唯一地识别一个容器。

脚本最多会尝试 $max 次,每次间隔 $interval 秒。请参阅关于“如何在脚本中编写重试逻辑,最多重试 5 次?”这个问题的 这些 答案 来实现重试逻辑。要检查 mysql 服务 5 次,每次间隔 1 秒,可以通过以下方式调用它:

bash wait-for-service.sh mysql 5 1

输出

$ bash wait-for-service.sh mysql 5 1
Waiting for service 'mysql' to become healthy, checking every 1 second(s) for 
max. 5 times
[1/5] ...
[2/5] ...
[3/5] ...
[4/5] ...
[5/5] ...
FAIL

# OR

$ bash wait-for-service.sh mysql 5 1
Waiting for service 'mysql' to become healthy, checking every 1 second(s) for 
max. 5 times
[1/5] ...
[2/5] ...
SUCCESS

“容器依赖”的问题并不新鲜,并且已经有一些现有的解决方案,例如

但不幸的是,所有这些解决方案都是通过检查 host:port 组合的可用性来工作的,而在 mysql 的情况下,这并没有帮助,因为容器已经启动,端口可以访问,但容器中的 mysql 服务却没有。

“本地” CI 运行设置

正如在 方法 部分所述,我们希望能够在本地执行所有必要的步骤,并且我创建了一个相应的脚本 .local-ci.sh

#!/bin/bash
# fail on any error 
# @see https://stackoverflow.com/a/3474556/413531
set -e

make docker-down ENV=ci || true

start_total=$(date +%s)

# STORE GPG KEY
cp secret-protected.gpg.example secret.gpg

# DEBUG
docker version
docker compose version
cat /etc/*-release || true

# SETUP DOCKER
make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=12345678"
start_docker_build=$(date +%s)
make docker-build
end_docker_build=$(date +%s)
mkdir -p .build && chmod 777 .build

# START DOCKER
start_docker_up=$(date +%s)
make docker-up
end_docker_up=$(date +%s)
make gpg-init
make secret-decrypt-with-password

# QA
start_qa=$(date +%s)
make qa || FAILED=true
end_qa=$(date +%s)

# WAIT FOR CONTAINERS
start_wait_for_containers=$(date +%s)
bash .docker/scripts/wait-for-service.sh mysql 30 1
end_wait_for_containers=$(date +%s)

# TEST
start_test=$(date +%s)
make test || FAILED=true
end_test=$(date +%s)

end_total=$(date +%s)

# RUNTIMES
echo "Build docker:        " `expr $end_docker_build - $start_docker_build`
echo "Start docker:        " `expr $end_docker_up - $start_docker_up  `
echo "QA:                  " `expr $end_qa - $start_qa`
echo "Wait for containers: " `expr $end_wait_for_containers - $start_wait_for_containers`
echo "Tests:               " `expr $end_test - $start_test`
echo "---------------------"
echo "Total:               " `expr $end_total - $start_total`

# CLEANUP
# reset the default make variables
make make-init
make docker-down ENV=ci || true

# EVALUATE RESULTS
if [ "$FAILED" == "true" ]; then echo "FAILED"; exit 1; fi

echo "SUCCESS"

运行详情

  • 作为准备步骤,我们首先确保没有过时的 ci 容器正在运行(这仅在本地需要,因为远程 CI 系统上的 runner 将“从头开始”启动)
    make docker-down ENV=ci || true
  • 我们花费一些时间来测量某些部分需要多长时间,通过
    start_total=$(date +%s)

    存储当前时间戳

  • 我们需要敏感 gpg 密钥才能解密敏感信息,并简单地复制 密码保护的示例密钥(在实际的 CI 系统中,密钥将被配置为注入到运行中的一个秘密值)
    # STORE GPG KEY
    cp secret-protected.gpg.example secret.gpg
  • 我喜欢打印一些调试信息,以便了解我们所处的具体情况(说实话,这主要在设置 CI 系统或对其进行修改时相关)
    # DEBUG
    docker version
    docker compose version
    cat /etc/*-release || true
  • 对于 Docker 设置,我们从 ci 初始化环境 开始
    # SETUP DOCKER
    make make-init ENVS="ENV=ci 
    TAG=latest EXECUTE_IN_CONTAINER=true GPG_PASSWORD=12345678"

    然后构建 Docker 设置

    make docker-build

    最后添加一个 .build/ 目录以 收集构建伪影

    mkdir -p .build && chmod 777 .build
  • 然后,启动 Docker 设置
    # START DOCKER
    make docker-up

    并初始化 gpg,以便 解密敏感信息

    make gpg-init
    make secret-decrypt-with-password

    我们不需要将 GPG_PASSWORD 传递给 secret-decrypt-with-password,因为我们已经在上一步通过 make-init 设置了默认值。

  • 一旦 application 容器运行起来,就可以通过调用 qa make 目标 来运行 QA 工具。
    # QA
    make qa || FAILED=true

    || FAILED=true 部分确保在检查失败时脚本不会终止。相反,失败的事实被“记录”在 FAILED 变量中,以便我们最后进行评估。我们不希望脚本在这里停止,因为我们希望后续步骤也能执行(例如,测试)。

  • 为了缓解 mysql 未准备就绪”问题,我们现在将应用 wait-for-service.sh 脚本
    # WAIT FOR CONTAINERS
    bash .docker/scripts/wait-for-service.sh mysql 30 1
  • 一旦 mysql 准备就绪,我们就可以通过 test make 目标 执行测试,并对 QA 工具应用相同的 || FAILED=true 变通方法。
    # TEST
    make test || FAILED=true
  • 最后,打印所有计时器
    # RUNTIMES
    echo "Build docker:        " `expr $end_docker_build - $start_docker_build`
    echo "Start docker:        " `expr $end_docker_up - $start_docker_up  `
    echo "QA:                  " `expr $end_qa - $start_qa`
    echo "Wait for containers: " `expr $end_wait_for_containers - 
                                  $start_wait_for_containers`
    echo "Tests:               " `expr $end_test - $start_test`
    echo "---------------------"
    echo "Total:               " `expr $end_total - $start_total`
  • 我们清理资源(这仅在本地运行时需要,因为 CI 系统的 runner 会被关闭)
    # CLEANUP
    make make-init
    make docker-down ENV=ci || true
  • 最后,评估在运行 QA 工具或测试时是否发生了任何错误

    # EVALUATE RESULTS
    if [ "$FAILED" == "true" ]; then echo "FAILED"; exit 1; fi
    
    echo "SUCCESS"

执行示例

通过以下方式执行脚本

bash .local-ci.sh

产生以下(缩短的)输出

$ bash .local-ci.sh
Container dofroscra_ci-redis-1  Stopping
# Stopping all other `ci` containers ...
# ...

Client:
 Cloud integration: v1.0.22
 Version:           20.10.13
# Print more debugging info ...
# ...

Created a local .make/.env file
ENV=ci TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra 
           APP_USER_NAME=application APP_GROUP_NAME=application docker 
           compose -p dofroscra_ci --env-file ./.docker/.env 
           -f ./.docker/docker-compose/docker-compose-php-base.yml build php-base
#1 [internal] load build definition from Dockerfile
# Output from building the docker containers 
# ...

ENV=ci TAG=latest DOCKER_REGISTRY=docker.io DOCKER_NAMESPACE=dofroscra 
       APP_USER_NAME=application APP_GROUP_NAME=application docker compose 
       -p dofroscra_ci --env-file ./.docker/.env 
       -f ./.docker/docker-compose/docker-compose.local.ci.yml 
       -f ./.docker/docker-compose/docker-compose.ci.yml up -d
Network dofroscra_ci_network  Creating
# Starting all `ci` containers ...
# ...

"C:/Program Files/Git/mingw64/bin/make" -s gpg-import GPG_KEY_FILES="secret.gpg"
gpg: directory '/home/application/.gnupg' created
gpg: keybox '/home/application/.gnupg/pubring.kbx' created
gpg: /home/application/.gnupg/trustdb.gpg: trustdb created
gpg: key D7A860BBB91B60C7: public key "Alice Doe protected 
     <alice.protected@example.com>" imported
# Output of importing the secret and public gpg keys
# ...

"C:/Program Files/Git/mingw64/bin/make" -s git-secret ARGS="reveal -f -p 12345678"
git-secret: done. 1 of 1 files are revealed.
"C:/Program Files/Git/mingw64/bin/make" -j 8 -k --no-print-directory 
                     --output-sync=target qa-exec NO_PROGRESS=true
phplint                             done   took 4s
phpcs                               done   took 4s
phpstan                             done   took 8s
composer-require-checker            done   took 8s
Waiting for service 'mysql' to become healthy, checking every 1 second(s) 
for max. 30 times
[1/30] ...
SUCCESS
PHPUnit 9.5.19 #StandWithUkraine

........                                                            8 / 8 (100%)

Time: 00:03.077, Memory: 28.00 MB

OK (8 tests, 15 assertions)
Build docker:         12
Start docker:         2
QA:                   9
Wait for containers:  3
Tests:                5
---------------------
Total:                46
Created a local .make/.env file

Container dofroscra_ci-application-1  Stopping
Container dofroscra_ci-mysql-1  Stopping
# Stopping all other `ci` containers ...
# ...

SUCCESS

Github Actions 设置

如果您完全不熟悉 Github Actions,我建议从 GitHub Actions 官方快速入门指南理解 GitHub Actions 文章开始。简而言之:

  • Github Actions 基于所谓的 **Workflows(工作流)**
    • Workflows 是 yaml 文件,位于仓库中特殊的 .github/workflows 目录下
  • 一个 Workflow 可以包含多个 **Jobs(作业)**
  • 每个 Job 由一系列 **Steps(步骤)** 组成
  • 每个 Step 都需要一个 run: 元素,它代表由新 shell 执行的命令

工作流文件

Github Actions 会基于 .github/workflows 目录中的文件自动触发。我添加了文件 .github/workflows/ci.yml,内容如下:

name: CI build and test

on:
  # automatically run for pull request and for pushes to branch 
    "part-7-ci-pipeline-docker-php-gitlab-github"
  # @see https://stackoverflow.com/a/58142412/413531
  push:
    branches:
      - part-7-ci-pipeline-docker-php-gitlab-github
  pull_request: {}
  # enable to trigger the action manually
  # @see https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/
  # CAUTION: there is a known bug that makes the "button to trigger the run" 
  # not show up
  # @see https://github.community/t/workflow-dispatch-workflow-not-showing-in-actions-tab/130088/29
  workflow_dispatch: {}

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v1

      - name: start timer
        run: |
          echo "START_TOTAL=$(date +%s)" > $GITHUB_ENV

      - name: STORE GPG KEY
        run: |
          # Note: make sure to wrap the secret in double quotes (")
          echo "${{ secrets.GPG_KEY }}" > ./secret.gpg

      - name: SETUP TOOLS
        run : |
          DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
          # install docker compose
          # @see https://docs.dockerd.com.cn/compose/cli-command/#install-on-linux
          # @see https://github.com/docker/compose/issues/8630#issuecomment-1073166114
          mkdir -p $DOCKER_CONFIG/cli-plugins 
          curl -sSL https://github.com/docker/compose/releases/download/v2.2.3/docker-compose-linux-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose
          chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose

      - name: DEBUG
        run: |
          docker compose version
          docker --version
          cat /etc/*-release

      - name: SETUP DOCKER
        run: |
          make make-init ENVS="ENV=ci TAG=latest 
          EXECUTE_IN_CONTAINER=true GPG_PASSWORD=${{ secrets.GPG_PASSWORD }}"
          make docker-build
          mkdir .build && chmod 777 .build

      - name: START DOCKER
        run: |
          make docker-up
          make gpg-init
          make secret-decrypt-with-password

      - name: QA
        run: |
          # Run the tests and qa tools but only store the error 
          # instead of failing immediately
          # @see https://stackoverflow.com/a/59200738/413531
          make qa || echo "FAILED=qa" >> $GITHUB_ENV

      - name: WAIT FOR CONTAINERS
        run: |
          # We need to wait until mysql is available.
          bash .docker/scripts/wait-for-service.sh mysql 30 1 

      - name: TEST
        run: |
          make test || echo "FAILED=test $FAILED" >> $GITHUB_ENV

      - name: RUNTIMES
        run: |
          echo `expr $(date +%s) - $START_TOTAL`

      - name: EVALUATE
        run: |
          # Check if $FAILED is NOT empty
          if [ ! -z "$FAILED" ]; then echo "Failed at $FAILED" && exit 1; fi

      - name: upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: build-artifacts
          path: ./.build

步骤与 本地运行的运行详情 中解释的基本相同。一些额外的说明:

  • 我希望 Action 仅在我 推送到分支 part-7-ci-pipeline-docker-php-gitlab-github 或创建拉取请求(通过 pull_request)时自动触发。此外,我希望能够 手动触发任何分支上的 Action(通过 workflow_dispatch)。
    on:
    push:
      branches:
        - part-7-ci-pipeline-docker-php-gitlab-github
    pull_request: {}
    workflow_dispatch: {}

    对于实际项目,我会让 Action 只在长期分支(如 maindevelop)上自动运行。手动触发对于只想测试当前工作而不将其提交评审很有帮助。 **注意:** 有一个 已知问题,它会“隐藏”手动触发 Action 的“Trigger workflow”按钮

  • 每个 run: 指令都会启动一个新的 shell,因此我们必须将计时器存储在“全局” 环境变量 $GITHUB_ENV 中。
      - name: start timer
      run: |
        echo "START_TOTAL=$(date +%s)" > $GITHUB_ENV 

    这将是我们唯一使用的计时器,因为作业使用了多个自动计时的步骤——所以我们不需要手动记录时间戳:

  • gpg 密钥被配置为 加密的秘密,名为 GPG_KEY,并存储在 ./secret.gpg 中。其值是 secret-protected.gpg.example 文件的内容。

      - name: STORE GPG KEY
        run: |
          echo "${{ secrets.GPG_KEY }}" > ./secret.gpg

    秘密在 Github 仓库的 **Settings** > **Secrets** > **Actions** 下配置,网址为

    https://github.com/$user/$repository/settings/secrets/actions
    
    e.g.
    
    https://github.com/paslandau/docker-php-tutorial/settings/secrets/actions

  • ubuntu-latest 镜像不包含 docker compose 插件,因此我们需要 手动安装它
      - name: SETUP TOOLS
      run : |
        DOCKER_CONFIG=${DOCKER_CONFIG:-$HOME/.docker}
        mkdir -p $DOCKER_CONFIG/cli-plugins 
        curl -sSL https://github.com/docker/compose/releases/download/v2.2.3/
        docker-compose-linux-$(uname -m) -o $DOCKER_CONFIG/cli-plugins/docker-compose
        chmod +x $DOCKER_CONFIG/cli-plugins/docker-compose
  • 对于 make 初始化,我们需要第二个名为 GPG_PASSWORD 的秘密——在本例中,它被配置为 12345678,请参阅 添加密码保护的秘密 gpg 密钥
      - name: SETUP DOCKER
        run: |
          make make-init ENVS="ENV=ci TAG=latest EXECUTE_IN_CONTAINER=true 
          GPG_PASSWORD=${{ secrets.GPG_PASSWORD }}"
  • 由于 runner 将在运行后关闭,我们需要将构建伪影移动到永久位置,使用 actions/upload-artifact@v3 action
      - name: upload build artifacts
        uses: actions/upload-artifact@v3
        with:
          name: build-artifacts
          path: ./.build

    您可以在运行概览 UI 中 下载伪影

Gitlab Pipelines 设置

如果您完全不熟悉 Gitlab Pipelines,我建议从 GitLab CI/CD 官方入门指南 开始。简而言之:

  • Gitlab Pipelines 的核心概念是 **Pipeline(流水线)**
    • 它在仓库根目录下的 .gitlab-ci.yml yaml 文件中定义
  • 一个 Pipeline 可以包含多个 **Stages(阶段)**
  • 每个 Stage 由一系列 **Jobs(作业)** 组成
  • 每个 Job 包含一个 **script** 部分
  • script 部分由一系列 shell 命令组成

由于技术限制,本文档限制在 40000 个字符。请访问 CI Pipelines for dockerized PHP Apps with Github & Gitlab [Tutorial Part 7] 阅读完整内容。

© . All rights reserved.