Circles of hell with GitHub Actions(为 Java 项目构建 CI/CD 管道)

Circles of hell with GitHub Actions(为 Java 项目构建 CI/CD 管道)

我经常需要构建一个用于用 Java 构建项目的管道。 有时它是开源的,有时不是。 我最近决定尝试将我的一些存储库从 Travis-CI 和 TeamCity 迁移到 GitHub Actions,这就是结果。

我们将自动化什么?

首先,我们需要一个自动化的项目,让我们在 Spring boot / Java 11 / Maven 中制作一个小应用程序。 就本文而言,我们对应用程序逻辑根本不感兴趣;应用程序周围的基础设施对我们来说很重要,因此一个简单的 REST API 控制器就足够了。

您可以在此处查看来源: github.com/antkorwin/github-actions 构建管道的所有阶段都反映在该项目的拉取请求中。

JIRA 和规划

值得一提的是,我们通常使用 JIRA 作为问题跟踪器,因此让我们为该项目创建一个单独的面板并在其中添加第一个问题:

Circles of hell with GitHub Actions(为 Java 项目构建 CI/CD 管道)

稍后我们将回到 JIRA 和 GitHub 组合可以提供哪些有趣的东西。

我们自动化项目的组装

我们的测试项目是通过maven构建的,所以构建它非常简单,我们只需要mvn clean包。

要使用 Github Actions 执行此操作,我们需要在存储库中创建一个文件来描述我们的工作流程,这可以使用常规 yml 文件来完成,我不能说我喜欢“yml 编程”,但我们能做什么 -我们在 .github/ 目录工作流程/文件 build.yml 中执行此操作,我们将在其中描述构建 master 分支时的操作:

name: Build

on:
  pull_request:
    branches:
      - '*'
  push:
    branches:
      - 'master'

jobs:
  build:
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@v1
      - name: set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 1.11
      - name: Maven Package
        run: mvn -B clean package -DskipTests

on — 这是对我们的脚本将启动的事件的描述。

上:拉请求/推 — 表示每次向主服务器推送并创建拉取请求时都需要启动此工作流程。

以下是任务的描述(工作)和执行步骤(步骤)对于每个任务。

连续运行 - 在这里我们可以选择目标操作系统,令人惊讶的是,您甚至可以选择 Mac 操作系统,但在私有存储库上,这是相当昂贵的(与 Linux 相比)。

使用 允许您重用其他操作,例如,使用 actions/setup-java 操作我们安装 Java 11 的环境。

通过 我们可以指定启动操作的参数,本质上这些是将传递给操作的参数。

剩下的就是使用 Maven 运行项目构建: run: mvn -B clean package-B 说我们需要一种非交互模式,这样 Maven 突然就不想问我们什么了

Circles of hell with GitHub Actions(为 Java 项目构建 CI/CD 管道)

伟大的! 现在,每次您提交给 master 时,项目构建都会开始。

自动化测试启动

组装是好的,但实际上,一个项目可以安全地组装,但不能工作。 因此,下一步是自动化测试运行。 此外,在进行 PR 审查时查看通过测试的结果非常方便 - 您确信测试已通过,并且没有人忘记在进行合并之前运行其分支。

我们将在创建拉取请求时运行测试并合并到主库中,同时我们将添加关于代码覆盖率的报告的创建。

name: Build

on:
  pull_request:
    branches:
      - '*'
  push:
    branches:
      - 'master'

jobs:
  build:
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@v1
      - name: set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 1.11
      - name: Maven Verify
        run: mvn -B clean verify
      - name: Test Coverage
        uses: codecov/codecov-action@v1
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

为了涵盖测试,我将 codecov 与 jacoco 插件结合使用。 codecov 有自己的操作,但它需要一个令牌才能处理我们的拉取请求:

${{ secrets.CODECOV_TOKEN }} ——我们会不止一次地看到这种结构,secrets 是在 GitHub 中存储秘密的机制,我们可以在那里写入密码/令牌/主机/url 以及其他不应该包含在存储库代码库中的数据。

您可以在 GitHub 上的存储库设置中向 Secret 添加变量:

Circles of hell with GitHub Actions(为 Java 项目构建 CI/CD 管道)

您可以在以下位置获取令牌 编解码器.io 通过 GitHub 授权后,要添加公共项目,您只需点击如下链接: GitHub 用户名/[仓库名称]. 还可以添加私有存储库;为此,您需要在 Github 中向应用程序授予 codecov 权限。

Circles of hell with GitHub Actions(为 Java 项目构建 CI/CD 管道)

将jacoco插件添加到POM文件中:

<plugin>
	<groupId>org.jacoco</groupId>
	<artifactId>jacoco-maven-plugin</artifactId>
	<version>0.8.4</version>
	<executions>
		<execution>
			<goals>
				<goal>prepare-agent</goal>
			</goals>
		</execution>
		<!-- attached to Maven test phase -->
		<execution>
			<id>report</id>
			<phase>test</phase>
			<goals>
				<goal>report</goal>
			</goals>
		</execution>
	</executions>
</plugin>
<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-surefire-plugin</artifactId>
	<version>2.22.2</version>
	<configuration>
		<reportFormat>plain</reportFormat>
		<includes>
			<include>**/*Test*.java</include>
			<include>**/*IT*.java</include>
		</includes>
	</configuration>
</plugin>

现在,codecov 机器人将输入我们的每个拉取请求并添加覆盖率变化图:

Circles of hell with GitHub Actions(为 Java 项目构建 CI/CD 管道)

让我们添加一个静态分析器

在我的大多数开源项目中,我使用声纳云进行静态代码分析,连接到 travis-ci 非常容易。 因此,迁移到 GitHub Actions 来执行相同操作是一个合乎逻辑的步骤。 动作市场是一个很酷的东西,但这一次它让我有点失望,因为出于习惯我找到了我需要的动作并将其添加到工作流程中。 但事实证明,sonar 不支持通过在 Maven 或 gradle 上分析项目的操作来工作。 当然,这是写在文档中的,但是谁会读它?!

通过操作是不可能的,所以我们将通过 mvn 插件来完成:

name: SonarCloud

on:
  push:
    branches:
      - master
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  sonarcloud:
    runs-on: ubuntu-16.04
    steps:
      - uses: actions/checkout@v1
      - name: Set up JDK
        uses: actions/setup-java@v1
        with:
          java-version: 1.11
      - name: Analyze with SonarCloud
#       set environment variables:
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
#       run sonar maven plugin:
        run: mvn -B verify sonar:sonar -Dsonar.projectKey=antkorwin_github-actions -Dsonar.organization=antkorwin-github -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=$SONAR_TOKEN -Dsonar.coverage.jacoco.xmlReportPaths=./target/site/jacoco/jacoco.xml

声纳_TOKEN - 可以在以下位置获得 声纳云 并且需要秘密注册。 GITHUB_TOKEN - 这是 GitHub 生成的内置令牌,借助该令牌,sonarcloud[bot] 将能够登录 Git,以便在拉取请求中给我们留下消息。

Dsonar.projectKey — 声纳中的项目名称,您可以在项目设置中看到它。

Dsonar组织 — 来自 GitHub 的组织名称。

我们发出拉取请求并等待 sonarcloud[bot] 出现在评论中:

Circles of hell with GitHub Actions(为 Java 项目构建 CI/CD 管道)

发布管理

构建已经配置完毕,测试已经运行,我们可以发布版本了。 让我们看看 GitHub Actions 如何使发布管理变得更加容易。

在工作中,我有一些项目的代码库位于 bitbucket 中(一切就像那个故事“我白天写入 bitbucket,晚上提交到 GitHub”)。 不幸的是,bitbucket 没有内置的发布管理工具。 这是一个问题,因为对于每个版本,您都必须手动创建一个 Confluence 页面,并将该版本中包含的所有功能放在那里,搜索思维宫殿、jira 中的任务、存储库中的提交。 有很多机会犯错误,您可能会忘记某些内容或输入上次已发布的内容,有时根本不清楚将拉取请求分类为什么 - 是功能还是错误修复,还是编辑测试,或者一些基础设施的东西。

GitHub 操作可以如何帮助我们? 有一个很棒的操作 - 发布起草器,它允许您设置发行说明文件模板来设置拉取请求的类别并自动将它们分组在发行说明文件中:

Circles of hell with GitHub Actions(为 Java 项目构建 CI/CD 管道)

设置报告的示例模板 (.github/release-drafter.yml):

name-template: 'v$NEXT_PATCH_VERSION'
tag-template: 'v$NEXT_PATCH_VERSION'
categories:
  - title: ' New Features'
    labels:
      - 'type:features'
# в эту категорию собираем все PR с меткой type:features

  - title: ' Bugs Fixes'
    labels:
      - 'type:fix'
# аналогично для метки type:fix и т.д.

  - title: ' Documentation'
    labels:
      - 'type:documentation'

  - title: ' Configuration'
    labels:
      - 'type:config'

change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
template: |
  ## Changes
  $CHANGES

添加脚本以生成草稿版本 (.github/workflows/release-draft.yml):

name: "Create draft release"

on:
  push:
    branches:
      - master

jobs:
  update_draft_release:
    runs-on: ubuntu-18.04
    steps:
      - uses: release-drafter/release-drafter@v5
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

从现在开始,所有拉取请求都将自动收集在发行说明中 - 神奇!

这里可能会出现一个问题:如果开发人员忘记在 PR 中添加标签怎么办? 然后就不清楚该将其放入哪个类别,并且您将不得不再次手动处理它,针对每个 PR 分别进行处理。 为了解决这个问题,我们可以使用另一个操作 - 标签验证器 - 它检查拉取请求上是否存在标签。 如果没有必需的标签,那么检查将失败,我们将在拉取请求中看到有关此的消息。

name: "Verify type labels"

on:
  pull_request:
    types: [opened, labeled, unlabeled, synchronize]

jobs:
  triage:
    runs-on: ubuntu-18.04
    steps:
      - uses: zwaldowski/match-label-action@v2
        with:
          allowed: 'type:fix, type:features, type:documentation, type:tests, type:config'

现在,任何拉取请求都必须使用以下标签之一进行标记:type:fix、type:features、type:documentation、type:tests、type:config。

Circles of hell with GitHub Actions(为 Java 项目构建 CI/CD 管道)

拉取请求的自动注释

既然我们谈到了拉取请求的有效工作这样的主题,那么值得讨论一下标签器这样的操作,它根据已更改的文件在 PR 中添加标签。 例如,我们可以将任何包含目录更改的拉取请求标记为 [build] .github/workflow.

连接起来非常简单:

name: "Auto-assign themes to PR"

on:
  - pull_request

jobs:
  triage:
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/labeler@v2
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}

我们还需要一个描述项目目录和拉取请求主题之间对应关系的文件:

theme:build:
  - ".github/**"
  - "pom.xml"
  - ".travis.yml"
  - ".gitignore"
  - "Dockerfile"

theme:code:
  - "src/main/*"

theme:tests:
  - "src/test/*"

theme:documentation:
  - "docs/**"

theme:TRASH:
  - ".idea/**"
  - "target/**"

我没有成功地将自动在拉取请求中放置标签的操作与检查所需标签是否存在的操作配对;match-label 不想看到机器人添加的标签。 编写自己的结合这两个阶段的动作似乎更容易。 但即使是这种形式,使用起来也相当方便;您需要在创建拉取请求时从列表中选择一个标签。

是时候部署了

Circles of hell with GitHub Actions(为 Java 项目构建 CI/CD 管道)

我通过 GitHub Actions 尝试了几种部署选项(通过 ssh、通过 scp 和使用 docker-hub),我可以说,很可能你会找到一种将二进制文件上传到服务器的方法,无论你的管道有多么弯曲是。

我喜欢将整个基础设施保留在一个地方的选项,所以让我们看看如何部署到 GitHub Packages(这是二进制内容、npm、jar、docker 的存储库)。

用于构建 docker 镜像并将其发布到 GitHub Packages 的脚本:

name: Deploy docker image

on:
  push:
    branches:
      - 'master'

jobs:

  build_docker_image:
    runs-on: ubuntu-18.04
    steps:

#     Build JAR:
      - uses: actions/checkout@v1
      - name: set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 1.11
      - name: Maven Package
        run: mvn -B clean compile package -DskipTests

#     Set global environment variables:
      - name: set global env
        id: global_env
        run: |
          echo "::set-output name=IMAGE_NAME::${GITHUB_REPOSITORY#*/}"
          echo "::set-output name=DOCKERHUB_IMAGE_NAME::docker.pkg.github.com/${GITHUB_REPOSITORY}/${GITHUB_REPOSITORY#*/}"

#     Build Docker image:
      - name: Build and tag image
        run: |
          docker build -t "${{ steps.global_env.outputs.DOCKERHUB_IMAGE_NAME }}:latest" -t "${{ steps.global_env.outputs.DOCKERHUB_IMAGE_NAME }}:${GITHUB_SHA::8}" .

      - name: Docker login
        run: docker login docker.pkg.github.com -u $GITHUB_ACTOR -p ${{secrets.GITHUB_TOKEN}}

#     Publish image to github package repository:
      - name: Publish image
        env:
          IMAGE_NAME: $GITHUB_REPOSITORY
        run: docker push "docker.pkg.github.com/$GITHUB_REPOSITORY/${{ steps.global_env.outputs.IMAGE_NAME }}"

首先,我们需要构建应用程序的 JAR 文件,然后计算 GitHub docker 注册表的路径和镜像的名称。 这里有一些我们还没有遇到过的技巧:

  • 像这样的结构: echo “::set-output name=NAME::VALUE” 允许您在当前步骤中设置变量的值,以便可以在所有其他步骤中读取它。
  • 通过本步骤的标识符可以获取上一步设置的变量值:${{steps.global_env.outputs.DOCKERHUB_IMAGE_NAME }}
  • 标准 GITHUB_REPOSITORY 变量存储存储库的名称及其所有者(“owner/repo-name”)。 为了从这一行中删除除存储库名称之外的所有内容,我们将使用 bash 语法: ${GITHUB_REPOSITORY#*/}

接下来我们需要构建 docker 镜像:

docker build -t "docker.pkg.github.com/antkorwin/github-actions/github-actions:latest"

登录注册表:

docker login docker.pkg.github.com -u $GITHUB_ACTOR -p ${{secrets.GITHUB_TOKEN}}

并将图像发布到 GitHub Packages Repository:

docker push "docker.pkg.github.com/antkorwin/github-actions/github-actions"

为了指示图像的版本,我们使用提交的 SHA 哈希值的第一个数字 - GITHUB_SHA 这里也有细微差别,如果您不仅在合并到 master 时进行此类构建,而且还根据拉取请求创建如果发生事件,那么 SHA 可能与我们在 git 历史记录中看到的哈希值不匹配,因为 actions/checkout 操作会生成自己唯一的哈希值,以避免 PR 中的死锁操作。

Circles of hell with GitHub Actions(为 Java 项目构建 CI/CD 管道)

如果一切顺利,然后打开存储库中的软件包部分(https://github.com/antkorwin/github-actions/packages),您将看到一个新的 docker 镜像:

Circles of hell with GitHub Actions(为 Java 项目构建 CI/CD 管道)

在那里您还可以看到 docker 镜像的版本列表。

剩下的就是配置我们的服务器以使用此注册表并重新启动服务。 我可能会在下次讨论如何通过 systemd 来做到这一点。

监控

让我们看一个简单的选项,了解如何使用 GitHub Actions 对我们的应用程序进行运行状况检查。 我们的启动应用程序有一个执行器,因此我们甚至不需要编写 API 来检查其状态;我们已经为懒人做好了一切。 你只需要拉主机: SERVER-URL:PORT/actuator/health

$ curl -v 127.0.0.1:8080/actuator/health

> GET /actuator/health HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.61.1
> Accept: */*

< HTTP/1.1 200
< Content-Type: application/vnd.spring-boot.actuator.v3+json
< Transfer-Encoding: chunked
< Date: Thu, 04 Jun 2020 12:33:37 GMT

{"status":"UP"}

我们需要做的就是编写一个任务来使用 cron 检查服务器,如果突然没有回答我们,那么我们将通过电报发送通知。

首先,让我们弄清楚如何运行 cron 工作流程:

on:
  schedule:
    - cron:  '*/5 * * * *'

很简单,我什至不敢相信在 Github 中你可以创建根本不适合 webhook 的事件。 详细信息在文档中: help.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule

让我们通过curl手动检查服务器状态:

jobs:
  ping:
    runs-on: ubuntu-18.04
    steps:

      - name: curl actuator
        id: ping
        run: |
          echo "::set-output name=status::$(curl ${{secrets.SERVER_HOST}}/api/actuator/health)"

      - name: health check
        run: |
          if [[ ${{ steps.ping.outputs.status }} != *"UP"* ]]; then
            echo "health check is failed"
            exit 1
          fi
          echo "It's OK"

首先,我们将服务器响应请求的内容保存到变量中,下一步我们检查状态是否为“UP”,如果不是,则退出并出现错误。 如果你需要用手“压倒”一个动作,那么 退出1 - 合适的武器。

  - name: send alert in telegram
    if: ${{ failure() }}
    uses: appleboy/telegram-action@master
    with:
      to: ${{ secrets.TELEGRAM_TO }}
      token: ${{ secrets.TELEGRAM_TOKEN }}
      message: |
        Health check of the:
        ${{secrets.SERVER_HOST}}/api/actuator/health
        failed with the result:
        ${{ steps.ping.outputs.status }}

仅当上一步操作失败时,我们才会发送电报。 要发送消息,我们使用 appleboy/telegram-action;您可以在文档中阅读有关如何获取机器人令牌和聊天 ID 的信息: github.com/appleboy/telegram-action

Circles of hell with GitHub Actions(为 Java 项目构建 CI/CD 管道)

不要忘记在 Github 上写入机密:服务器的 URL 和电报机器人的令牌。

奖励曲目 - JIRA 适合懒人

我承诺我们会回到 JIRA,我们已经回来了。 我数百次在站立会议中观察到这样的情况:开发人员制作了一个功能,合并了一个分支,但忘记将问题拖入 JIRA。 当然,如果所有这些都在一个地方完成,那就更容易了,但实际上我们在IDE中编写代码,将分支合并到bitbucket或GitHub中,然后将任务拖到Jira中,为此我们需要打开新窗口,有时会重新登录等等。 当你完全记得下一步需要做什么时,就没有必要再次打开面板了。 因此,在早上的站立会议上,您需要花时间更新任务板。

GitHub 也会帮助我们完成这个例行任务;对于初学者来说,当我们提交拉取请求时,我们可以自动将问题拖到 code_review 列中。 您需要做的就是遵循分支命名约定:

[имя проекта]-[номер таска]-название

例如,如果项目密钥“GitHub Actions”是 GA,则 GA-8-jira-bot 可能是执行 GA-8 任务的一个分支。

与 JIRA 的集成是通过 Atlassian 的操作进行的,它们并不完美,我必须说其中一些对我来说根本不起作用。 但我们只会讨论那些确实有效并且被积极使用的。

首先,您需要使用以下操作登录 JIRA:atlassian/gajira-login

jobs:
  build:
    runs-on: ubuntu-latest
    name: Jira Workflow
    steps:
      - name: Login
        uses: atlassian/gajira-login@master
        env:
          JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
          JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
          JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}

为此,您需要在 JIRA 中获取令牌,此处描述了如何执行此操作: confluence.atlassian.com/cloud/api-tokens-938839638.html

我们从分支名称中提取任务标识符:

  - name: Find Issue
    id: find_issue
    shell: bash
    run: |
      echo "::set-output name=ISSUE_ID::$(echo ${GITHUB_HEAD_REF} | egrep -o 'GA-[0-9]{1,4}')"
      echo brach name: $GITHUB_HEAD_REF
      echo extracted issue: ${GITHUB_HEAD_REF} | egrep -o 'GA-[0-9]{1,4}'

  - name: Check Issue
    shell: bash
    run: |
      if [[ "${{steps.find_issue.outputs.ISSUE_ID}}" == "" ]]; then
        echo "Please name your branch according to the JIRA issue: [project_key]-[task_number]-branch_name"
        exit 1
      fi
      echo succcessfully found JIRA issue: ${{steps.find_issue.outputs.ISSUE_ID}}

如果您在 GitHub 市场中搜索,您可以找到此任务的操作,但我必须使用分支名称使用 grep 编写相同的内容,因为 Atlassian 的此操作不想以任何方式在我的项目上工作,找出哪里出了问题——比用手做同样的事情要花更长的时间。

剩下的就是在创建拉取请求时将任务移至“代码审查”列:

  - name: Transition issue
    if: ${{ success() }}
    uses: atlassian/gajira-transition@master
    with:
      issue: ${{ steps.find_issue.outputs.ISSUE_ID }}
      transition: "Code review"

GitHub 上有一个专门的操作,只需要上一步中获得的问题 ID 和我们上面在 JIRA 中进行的授权即可。

Circles of hell with GitHub Actions(为 Java 项目构建 CI/CD 管道)

以同样的方式,您可以在合并到 master 时拖动任务,以及 GitHub 工作流中的其他事件。 一般来说,这完全取决于您的想象力和自动化日常流程的愿望。

发现

如果你看一下经典的 DEVOPS 图,我们已经涵盖了所有阶段,除了操作之外,我想如果你尝试的话,你可以在市场上找到一些与帮助台系统集成的行动,所以我们假设管道转向是彻底的,可以根据其使用得出结论。

Circles of hell with GitHub Actions(为 Java 项目构建 CI/CD 管道)

优点:

  • 市场上有适合所有场合的现成动作,这非常酷。 在大多数情况下,您还可以查看源代码以了解如何解决类似问题或直接在 GitHub 存储库中向作者发布功能请求。
  • 选择汇编的目标平台:Linux、mac os、windows 是一个非常有趣的功能。
  • Github Packages 是一件很棒的事情,可以很方便地将整个基础设施保存在一个地方,您不必浏览不同的窗口,一切都在单击一两次鼠标的半径范围内,并且与 GitHub Actions 完美集成。 免费版本中的 Docker 注册表支持也是一个很好的优势。
  • GitHub 在构建日志中隐藏秘密,因此使用它来存储密码和令牌并不那么可怕。 在我所有的实验中,我从未能够在控制台中以纯粹的形式看到这个秘密。
  • 免费用于开源项目

缺点:

  • YML,好吧,我不喜欢他。 在使用这样的流程时,我最常见的提交消息是“修复 yml 格式”,然后您忘记在某处放置选项卡,或者将其写在错误的行上。 一般来说,坐在屏幕前拿着量角器和尺子并不是最愉快的体验。
  • 调试、通过提交调试流程、运行重建以及输出到控制台并不总是很方便,但它更多的是“你做得太过分了”类别;当你可以调试任何东西时,你习惯于使用方便的 IDEA 工作。
  • 如果你将它包装在 Docker 中,你可以在任何东西上编写你的操作,但本机仅支持 javascript,当然这是一个品味问题,但我更喜欢其他东西而不是 js。

让我提醒您,包含所有脚本的存储库位于此处: github.com/antkorwin/github-actions

下周我将与 报告 在 Heisenbug 2020 Piter 会议上。 我不仅会告诉您如何在准备测试数据时避免错误,还会分享我在 Java 应用程序中处理数据集的秘密!

来源: habr.com