GCP + ネイティブアプリでのGitLab flow構築

# はじめに

これは今さらな2021年振り返りカレンダーの2日目の記事です．
[トゥイター](https://twitter.com/mahiiiki)も良かったらフォローしていただけると嬉しいです🐦


前回はCloud Run上で動くサービス用にGitHub flowでのCD環境を構築する話でした．

https://qiita.com/sheep96/items/e8e09de094949ce63509



前回の最後やいろんな記事で述べられているとおり，GitHub flowは審査が必要なネイティブアプリなど，アプリケーションの
リリースタイミングをこちら側でコントロールできないケースで不都合が発生します（モノレポ想定です）．


例えばあるアプリについて，後方互換のない新機能が入ったバージョンv2がmainにマージされ，stg環境に
上がっているとします．
この時，prdのネイティブ以外の部分でなんらかのバグが見つかり，すぐに修正を行いたいとき，通常のサービスならば，
修正PRをmainにマージし，新規機能とともにリリースすることができます．
しかしネイティブアプリの場合，アプリの審査がリリース前に挟まれるため，修正をデプロイするには，
審査を待つか，既にmainに入っている新機能をrevertするしかありません．

対策としては，ネイティブアプリのレポジトリを分けることが考えられますが，
自分のいたチームでは全員が機能開発に対してバックエンド，アプリを両方書いていたため，
PRの管理やリリースバージョンの統一が面倒になるなど別の問題が想定されました．

そこで，今回はGitHub flowをやめ，GitLab flowを採用することとしました．
GitLab flowの詳細については以下に詳しく書いてあります．

https://postd.cc/gitlab-flow


要点をまとめると以下のような感じです．

- mainブランチに加え，環境（stg, prdなど）ごとにブランチを用意する．
- 新規の変更はmainからブランチを切って，mainにマージする．
- 検証環境にデプロイする際はmainをそのブランチへマージし，デプロイする
- 検証が終わったら，検証ブランチを本番ブランチにマージし，デプロイ
- 本番に近いブランチは，必ずその前の全ての検証ブランチを通っているようにする


# 仕様


今回はクライアントはiOSアプリ，バックエンドはCloud Runに乗せている形を想定します．
また，環境は一旦stg環境とprd環境の2つとします．

そして，仕様は次のような形です．

1. 新規の変更はmainブランチから切り，mainブランチにマージ．また，検証用ブランチとしてstaging，本番ブランチとしてprductionを用意
2. 常にmainからstgへのPRを生成し，リリースのタイミングでそれをstagingへマージし，リリースコマンドを打つ．このタイミングで，stagingからproductionへのPRを生成．
3. staging上での検証が終わったら，stgからprdへのPRをマージ．アプリをビルドし，審査へ提出．審査にapiの変更が必要な場合はこのタイミングでデプロイ．
4. 審査が通ったら，必要に応じてmigrationを行い，apiをデプロイ．

また，hotfixの流れは以下のような形です，

1. stgからhotfixブランチを切り，stgにマージし動作確認．確認が終わったら，ブランチをmainとstgにマージ．
2. stgをprdにマージし，審査&デプロイ．


# 実装

ディレクトリ構成は前回と同じです．
CDフローに必要なファイルなどは，release下に，環境ごとに用意します．

```
root/
├── .github/workflows
│  ├── release.yaml
│  ├── hotfix.yaml
│  └── prerelease.yaml
├── api
├── ui
├── release
│  ├── dev
│  └── prd
└── release.sh

```

## 新しいバージョンのリリースとstagingへのデプロイ

新規バージョンのリリースの際はまず，mainからstgingブランチへ差分をマージします．
この差分PRを常時生成するために，
git-pr-releaseを用いました．


https://github.com/x-motemen/git-pr-release

git-pr-releaseでは，ブランチ同士を比較して，
差分がある場合はPRの自動生成と差分コミットの詳細を作ってくれます．

設定ファイルは以下のような感じです．
GIT_PR_RELEASE_BRANCH_STAGINGにはマージ元を，GIT_PR_RELEASE_BRANCH_PRODUCTIONにマージ先を設定します．
また，GIT_PR_RELEASE_TEMPLATEには生成するPRのテンプレートを設定することができます．


```yaml
# prerelease.yaml
on:
  push:
    branches: [main]

jobs:
  create-release-pr:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0

      - name: Setup Ruby
        uses: actions/setup-ruby@v1
        with:
          ruby-version: 2.7.x

      - name: Create Staging release pull Request
        env:
          GIT_PR_RELEASE_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GIT_PR_RELEASE_BRANCH_PRODUCTION: staging
          GIT_PR_RELEASE_BRANCH_STAGING: main
          GIT_PR_RELEASE_LABELS: staging
          GIT_PR_RELEASE_TEMPLATE: .github/workflows/prerelease_template
        run: |
          gem install -N git-pr-release -v "1.9.0"
          git-pr-release --no-fetch --squashed

```


次に，新規バージョンタグをプッシュした際の動きを実装します．
その際には以下を行います．

- リリースノートの生成
- stagingからproductionへのPR生成
- staging環境へのデプロイ

リリースノートの生成は，前回と同様以下を用います．


https://zenn.dev/seita/articles/d1dba77043be8fd50eeb


stagingから，productionへのPR生成は，上記のrelease.yamlに以下の動作を追加し行います．

リリース用のブランチ（名前はrelease/v0.0.0）を生成したのち，そのブランチからproductionへ
向けたPRを生成しています．


```yaml
# release.yaml

on:
  push:
    tags:
      - "v*"

name: Create Release

jobs:
  build:
    name: Create Release
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v2
        with:
          fetch-depth: 0

      - name: Get version
        id: get_version
        run: echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/}

     ############### 省略 ###############
      
      # リリース用のブランチを生成
      - name: Create Release Branch
        id: create_release_branch
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          git checkout pre-production
          git checkout -b release/${{ steps.get_version.outputs.VERSION }}
          git push origin release/${{ steps.get_version.outputs.VERSION }}

     # 作成したリリースブランチからprdブランチへのPRを生成
      - name: Create Prd Release Pull Request
        id: create_release_pr
        uses: repo-sync/pull-request@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          pr_title: Production Release ${{ steps.get_version.outputs.VERSION }}
          pr_label: production-deploy
          destination_branch: production
          source_branch: release/${{ steps.get_version.outputs.VERSION }}
```


staging環境へのデプロイは，前回と同じく，cloud buildのトリガーを用います．
また，iosアプリの方のCDについてはまた次回以降で書く予定です．

## productionへのデプロイ

productionへのデプロイは，以下の2つのステップに分かれています．

- ネイティブアプリの審査提出
- apiのデプロイ

ネイティブアプリの審査への提出は常に行われますが，apiのデプロイは
ケースによって審査に必須であったり必要なかったりします（バージョン整合のため）．

なので，アプリの審査提出は自動化し，apiのデプロイは手動で行うこととします．

ネイティブアプリの審査提出に関しては，また次回以降で解説します．

apiのデプロイに関しては，前回と同様，release/prd下のスクリプトを叩くことで行います．


```sh
#!/bin/bash

function deploy() {
  gcloud beta builds triggers run --tag $2 service-prd-api-trigger
}

$1 $@
```

```
cd release/prd
sh deploy.sh deploy v0.0.0
```


これで通常のCDフローは完了です．


## hotfix

次はhotfixへの対応です．
hotfixというprefixを持つブランチがpushされた時，自動でmainとstagingブランチへのPRを生成するようにします
（ぼーっとしてるとどっちかを入れ忘れるため）．
コードは以下のような形です．


```yaml
# hotfix.yaml
on:
  push:
    branches: ["hotfix/*"]

env:
  BRANCH_NAME: $(echo ${GITHUB_REF#refs/heads/})

jobs:
  create_hotfix_pr:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 0

      - name: Main Create Hotfix Pull Request
        id: main_create_hotfix_pr
        uses: repo-sync/pull-request@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          pr_title: "Main ${{ env.BRANCH_NAME }}"
          pr_label: "hotfix"
          destination_branch: main

      - name: Pre-Production Create Hotfix Pull Request
        id: preproduction_create_hotfix_pr
        uses: repo-sync/pull-request@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          pr_title: "Pre-Production ${{ env.BRANCH_NAME }}"
          pr_label: "hotfix"
          destination_branch: staging
```

これで全体の構築が完了です．


# その他細かい知見など

- git-pr-releaseはsquashマージと通常マージの両方に対応可能．
- mainからstagingブランチへのマージをsquashにしてしまうと，リリースノートがsquashコミットだけになって
差分の確認がしづらくなるので，ここは通常マージにしたほうが良い（めんどいので，もっといい方法ないかな．．．）．
- hotfixとmainでコンフリクトが起きた時の対処がよくわかっていない．．．


# 最後に

GitHub flowで起きる問題を解決した，GitLab flowの実装解説をしました．
といっても今回解説したのだと，stagingにアプリが上がってるときにhotfixが必要になった場合
またリバートしなくてはならないので，本当にこの問題を対策するときはstagingやmainへのマージを慎重に行ったり，apiデプロイするようにしたりなど追加の検討が必要そうです．
ただ柔軟性が高く拡張しやすいので，アプリならば最初から採用しておくのが楽かな〜という所感でした．

省略したアプリのCD周りなどについては，次回以降に書く予定です．