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 管道)

您可以在以下位置取得令牌 Codecov.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 應用程式中處理資料集的秘密!

來源: www.habr.com