Github ActionsとCircleCIの組み合わせが最高という話【後編】


前回の記事

何をGithub Actionsで処理して、何をCircleCIで処理するか
どうやって実現するかを前回書きました

今回は実際にコードを見ながらどう実現するかを解説していきます。

おさらい

まずおさらいです。

Github Actions
ビルド
Lintチェック

CircleCI
テスト
Slackへの通知
自動マージ
デプロイ

※僕はAndroidエンジニアのためビルド等は基本Androidコマンドを叩いています。ご了承ください。
※Github Actions, CircleCIの細かい文法に関しては説明しません。

ビルド

name: Build

on:
  pull_request:
    branches: [ master ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - name: set up JDK 1.8
        uses: actions/setup-java@v1
        with:
          java-version: 1.8
      - name: Build with Gradle
        run: ./gradlew compileDebugSources

ここは簡単
純粋にGithub Actionsの処理を書いてあげればOK

Lintチェック

name: Build

on:
  pull_request:
    branches: [ master ]

jobs:
  build:

    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - name: set up JDK 1.8
        uses: actions/setup-java@v1
        with:
          java-version: 1.8
      - name: Build with Gradle
        run: ./gradlew lintDebug

本当はマルチモジュールでかつDangerを使ってGithubのコメントに結果を表示していますが、ここでは割愛
詳しくは以下の記事へ
https://qiita.com/dosukoi_android/items/75b9fe01aa021296d586

テスト

ここからが本題
先にGithub Actionsのワークフローを全て掲載し最後にCircleCIのワークフローを掲載します(CircleCIはconfig.ymlで一元管理しているため)
今回はレビュワーがapproveをしたらテストを走らせます。

Github Actions

name: UnitTest

on:
  pull_request_review:
    types: [ submitted ]

jobs:
  test:
    # ここでapproveされたらというハンドリング&Git-flowを使用している場合はブランチ名でもハンドリング
    if: ${{ github.event.review.state == 'approved' && github.event.pull_request.base.ref == 'master' }}
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
      - name: Call CircleCI API
        # ここでCircleCIのAPIを叩く
        run: |
          curl \
          -X POST \
          -H "Content-Type: application/json" \
          -d '{ "branch": "master", "parameters": { "build_variant": "dev", "task": "test", "pull_request_title": "${{ github.event.pull_request.title }}", "pull_request_html_url": "${{ github.event.pull_request.html_url }}", "pull_request_user": "${{ github.event.pull_request.user.login }}", "pull_request_url": "${{ github.event.pull_request.url }}", "pull_request_sha": "${{ github.event.pull_request.head.sha }}"}}' \
          https://circleci.com/api/v2/project/github/${{ github.event.repository.full_name }}/pipeline

デプロイ

Github Actions


name: Deploy

on:
  pull_request:
    types: [closed]
    branches: [master]

jobs:
  build:
    # PRがcloseかつmergedがtrueの時だけ→つまりマージされた時だけ起動
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-18.04

    steps:
      - uses: actions/checkout@v1
      - name: Call CircleCI API
        run: |
          curl \
          -X POST \
          -H "Content-Type: application/json" \
          -H "Circle-Token: ${{ secrets.CIRCLE_TOKEN }}" \
          -d '{ "branch": "master", "parameters": { "build_variant": "dev", "task": "deploy", "pull_request_title": "${{ github.event.pull_request.title }}" } }' \
          https://circleci.com/api/v2/project/github/${{ github.event.repository.full_name }}/pipeline

CircleCI


version: 2.1

parameters:
  # これでどのジョブを起動するかハンドリング
  task:
    type: enum
    enum: ["deploy", "test"]
    default: "deploy"
  # 環境ごとにも変えられる
  build_variant:
    type: enum
    enum: ["dev", "stg", "production"]
    default: "dev"
  # ここから下はSlack通知で使用
  pull_request_title:
    type: string
    default: ""
  pull_request_html_url:
    type: string
    default: ""
  pull_request_url:
    type: string
    default: ""
  pull_request_user:
    type: string
    default: ""
  pull_request_sha:
    type: string
    default: ""

executors:
  android:
    docker:
      - image: circleci/android:api-30
    environment:
      JVM_OPTS: -Xmx1536m
      GRADLE_OPTS: '-Dorg.gradle.daemon=false -Dorg.gradle.jvmargs="-Xmx1536m -XX:+HeapDumpOnOutOfMemoryError" -Dorg.gradle.configureondemand=true -Dkotlin.compiler.execution.strategy=in-process -Dkotlin.incremental=false'


orbs:
  android: circleci/[email protected]

jobs:

  deploy:
    executor: android
    steps:
      - checkout
      - run:
          # aabファイルを作成する
          name: Build with Gradle
          command: |
            ENV=(<< pipeline.parameters.build_variant >>)
            ENV_UPPER_CASE=${ENV[@]~}
            ./gradlew ":navigation:phone:bundle${ENV_UPPER_CASE}Release"
      - run:
          # DeployGateのAPIを叩く
          name: Distribute App
          command: |
            curl \
            -H "Authorization: token $DEPLOY_GATE_API_KEY" \
            -F "file=@navigation/phone/build/outputs/bundle/<< pipeline.parameters.build_variant >>Release/phone-<< pipeline.parameters.build_variant >>-release.aab" \
            -F "message=<< pipeline.parameters.pull_request_title >>" \
            -v "https://deploygate.com/api/users/$DEPLOY_GATE_USER_NAME/apps"


  test:
    executor: android
    steps:
      - checkout
      - run:
          name: Unit Test
          command: |
            ENV=(<< pipeline.parameters.build_variant >>)
            ENV_UPPER_CASE=${ENV[@]~}
            ./gradlew "test${ENV_UPPER_CASE}DebugUnitTest"
      - run:
          # 失敗したらSlack通知
          when: on_fail
          name: Unit Test Failure Notification
          command: |
            curl \
            -X POST \
            -H "Content-Type: application/json" \
            -d '{"attachments": [{"color": "#D73A49", "title": "<< pipeline.parameters.pull_request_title >>", "title_link": "<< pipeline.parameters.pull_request_html_url >>", "text": "Failure Unit Test", "author_name": "<< pipeline.parameters.pull_request_user >>"}]}' \
            $SLACK_WEBHOOK

  auto_merge:
    executor: android
    steps:
      - run:
          name: Wait For Status Check
          command: sleep 5s
      - run:
          # GithubのマージのAPIを叩く
          name: Auto Merge
          command: |
            curl \
            -X PUT \
            -H "Authorization: token $PERSONAL_ACCESSTOKEN" \
            -H "Content-Type: application/json" \
            -d '{"sha": "<< pipeline.parameters.pull_request_sha >>", "merged": "true", "message": "Pull Request successfully merged"}' \
            "<< pipeline.parameters.pull_request_url >>/merge"


workflows:
  version: 2.1
  build_and_deploy:
    # APIを叩くときのパラメータがdeployの時にこのジョブを起動
    when:
      equal: [deploy, << pipeline.parameters.task >>]
    jobs:
      - deploy:
          name: Deploy

  test:
    # APIを叩くときのパラメータがtestの時にこのジョブを起動
    when:
      equal: [test, << pipeline.parameters.task >>]
    jobs:
      - test
      - auto_merge:
          name: Auto Merge
          requires:
            - test
     # testというジョブが終わったらauto_mergeというジョブを起動

まとめ

以上となります。

ビルドからデプロイまでほぼ自動化し、マンパワーで行うことはレビューだけです。
コンフリクトさえしてなければ、レビュワーがLGTM!とするだけで、テスト、マージ、デプロイまで全て自動化してくれます。

自動化って男のロマンじゃないですか??