githubのヒストリーを楽にきれいに保つためにしていること

Webアプリなど最新のリポジトリの状態しか参照しない場合、github flowの様にmainブランチに対してどんどんコミットしていくスタイルで良いと思います。
ですが、パッケージ開発の様にリリースしたバージョンを複数管理している場合、git flowをベースにリポジトリを管理していくことになります。

ただ、git glowをベースに管理していく場合、色々ルールが多く、ルールを知らないためにいつの間にか逸脱していたり、すべき作業が忘れられている場合があります。
このようなことを防ぐために行っている設定を記載します。

# 前提

以下の様にgit flowをベースに、developブランチで開発を行い、releaseブランチでリリース前作業、mainブランチで過去バージョンの保管を行っています。

![branch_rule.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3944066/24cb2be1-7063-4db0-9b2e-294e898aa281.png)

このreleaseブランチではTypeScriptのyarn.lockやRustのCargo.lockなど孫参照のOSSなども含めて固定するためにコミットし、将来的にもmainブランチでビルドする際に依存するOSSなど含めて再現できるようにします。
developでこれらのlockファイルをコミットして運用している場合や依存するOSSなどがない場合、releaseブランチは必要ないです。

# ブランチルール

「Settings > Rules > Rulesets」 には以下の3つの定義を行っています。
細かな規則については[使用できる規則](https://docs.github.com/ja/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets)を参照してください。

## develop

developブランチをターゲットとしてはブランチルールを指定しています。

- Restrict deletions
  - 削除を制限
  - デフォルトブランチの場合は削除できませんが、念の為
- Require linear history
  - featureブランチなどをマージコミットすることを制限します
  - developにマージコミットを行うとヒストリが複雑化するので、障害等が発生したときに履歴を訴求しやすくします
- Require a pull request before merging
  - プルリクエストの作成を必須とします
  - チームで行っている場合はRequired approvalsで必須のレビュアーの人数を指定しておきましょう
  - Require conversation resolution before mergingで指摘については必ず解決するようにしましょう
  - Allowed merge methodsでマージ方法を「Squash」に限定しましょう。「Rebase and merge」でも構いませんが、プルリクエスト内の誤字レベルのコミットまでdevelopに残すとメンテナンス性が悪化するので、プルリクエスト単位でdevelopには残しておきます
- Require status checks to pass
  - 更新時に必要なステータスチェックを追加します
  - Require branches to be up to date before mergingにチェックをいれておくと、マージ前に一度rebaseしてからステータスのチェックが行われるので、他のプルリクエストとの齟齬での障害発生などが防げます
  - Add checksでチェックするGithub Actionsなどのチェックを指定できます
- Block force pushes
  - 強制的な上書きを阻止します

## main

mainブランチ、releaseブランチをターゲットとしてはブランチルールを指定しています。

- Restrict deletions
  - 削除を制限
- Require a pull request before merging
  - プルリクエストの作成を必須とします
  - チームで行っている場合はRequired approvalsで必須のレビュアーの人数を指定しておきましょう
  - Allowed merge methodsでマージ方法を「Merge」に限定しましょう
- Block force pushes
  - 強制的な上書きを阻止します

## exclude

ルールに従わないブランチの作成を防ぐため、以下のルールを作成しておきます。

ターゲットにExclude by patternで以下を登録します。

- main
- release
- develop
- feature/*
  - JIRAなどと連携している場合、ブランチ名にキーを含めると思うのでfeature/\<プロジェクト名\>-*などでより細かい制限を付与すると良いです。
- bugfix/*

ルールとしては以下の通り。

- Restrict creations
  - 作成を制限
- Restrict updates
  -  更新を制限
- Restrict deletions
  - 削除を制限

## 注意事項

Admin権限やバイパスリストに入っているメンバーはこのルールを貫通するので上記を理解しているメンバーにのみ権限を渡しましょう

# 定例操作

リリース時、定型的にする必要がある操作についてはGithub Actionsでgithub-scriptを使用して自動でプルリクエストを作成すると良いです。

以下には例を記載していきます。

## ロックファイルの自動追加

developからreleaseにマージした際に自動でロックファイルのブランチを更新するプルリクエストを作成する方法としては以下です。

```yaml
name: update-lockfiles

on:
  pull_request:
    types:
      - closed
    branches:
      - release

jobs:
  update-lockfiles:
    if: github.event.pull_request.base.ref == 'release' && github.event.pull_request.head.ref == 'develop' && github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      id-token: write
      contents: write
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          ref: release
          fetch-depth: 0

      - name: Checkout new branch
        run: |
          git checkout -b feature/update-lockfiles

      - name: Remove yarn.lock and Cargo.lock from .gitignore
        run: |
          sed -i '/yarn.lock/d' .gitignore

      - name: Commit .gitignore changes
        run: |
          git add .gitignore
          git commit -m 'chore: remove yarn.lock and Cargo.lock from .gitignore' || echo 'No changes to commit'

      - name: Remove old lockfiles
        run: |
          rm -f yarn.lock

      - name: Build project (make build)
        run: make build

      - name: Commit new lockfiles
        run: |
          git add yarn.lock
          git commit -m 'chore: update lockfiles after build' || echo 'No changes to commit'

      - name: Push changes
        run: |
          git push origin feature/update-lockfiles --force

      - name: Create Pull Request
        uses: actions/github-script@v6
        with:
          github-token: ${{ secrets.GIT_TOKEN }}
          script: |
            try {
              const result = await github.rest.pulls.create({
                owner: context.repo.owner,
                repo: context.repo.repo,
                title: process.env.TITLE || 'release: lockファイル自動更新',
                head: 'feature/update-lockfiles',
                base: 'release',
                body: process.env.BODY || ''
              });
              core.info('Pull request created.');
              core.info(JSON.stringify(result, null, 2));
            } catch (error) {
              core.setFailed(`Failed to create PR: ${error.message}\n${JSON.stringify(error, null, 2)}`);
            }
        env:
          TITLE: "release: lockファイル自動更新"
          BODY: |
            # 対処内容

            yarn.lockを自動更新します。
```

## mainブランチへのマージ

developからreleaseにマージした際にreleaseからmainブランチへの更新プルリクエストを作成する方法としては以下です。

```yaml
name: merge-release-to-main

on:
  pull_request:
    types:
      - closed
    branches:
      - release

jobs:
  merge-release-to-main:
    if: github.event.pull_request.base.ref == 'release' && github.event.pull_request.head.ref == 'develop' && github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      id-token: write
      contents: read
    steps:
      - name: Checkout main branch
        uses: actions/checkout@v4
        with:
          ref: release
          fetch-depth: 0

      - name: Create Pull Request to main
        uses: actions/github-script@v6
        with:
          github-token: ${{ secrets.GIT_TOKEN }}
          script: |
            try {
              const result = await github.rest.pulls.create({
                owner: context.repo.owner,
                repo: context.repo.repo,
                title: process.env.TITLE || 'Merge release to main',
                head: 'release',
                base: 'main',
                body: process.env.BODY || ''
              });
              core.info('Pull request created.');
              core.info(JSON.stringify(result, null, 2));
            } catch (error) {
              core.setFailed(`Failed to create PR: ${error.message}\n${JSON.stringify(error, null, 2)}`);
            }
        env:
          TITLE: "release: 製品のリリース"
          BODY: |
            # 対処内容

            製品のリリース作業を実施
```

## 内部バージョンの更新

Rustなどのパッケージが内部でバージョンを保持しているようなものについては以下のように自動で更新するプルリクエストを作成するとよいです。

```yaml
name: bump-version

on:
  pull_request:
    types:
      - closed
    branches:
      - release

jobs:
  bump-version:
    if: github.event.pull_request.base.ref == 'release' && github.event.pull_request.head.ref == 'develop' && github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    permissions:
      pull-requests: write
      id-token: write
      contents: write
    steps:
      - name: Checkout develop branch
        uses: actions/checkout@v4
        with:
          ref: develop
          fetch-depth: 0

      - name: Bump Cargo.toml version (minor up)
        id: bump_version
        run: |
          FILE=Cargo.toml
          OLD_VERSION=$(grep '^version' $FILE | head -n1 | cut -d '"' -f2)
          MAJOR=$(echo $OLD_VERSION | cut -d. -f1)
          MINOR=$(echo $OLD_VERSION | cut -d. -f2)
          PATCH=$(echo $OLD_VERSION | cut -d. -f3)
          NEW_MINOR=$((MINOR + 1))
          NEW_VERSION="$MAJOR.$NEW_MINOR.0"
          sed -i "s/^version = \"$OLD_VERSION\"/version = \"$NEW_VERSION\"/" $FILE

      - name: Commit and push version bump
        run: |
          git checkout -b feature/bump-version-${{ steps.bump_version.outputs.new_version }}
          git add Cargo.toml
          git commit -m "chore: Bump Cargo.toml version to v${{ steps.bump_version.outputs.new_version }}"
          git push origin feature/bump-version-${{ steps.bump_version.outputs.new_version }}

      - name: Create Pull Request
        uses: actions/github-script@v6
        with:
          github-token: ${{ secrets.GIT_TOKEN }}
          script: |
            try {
              const result = await github.rest.pulls.create({
                owner: context.repo.owner,
                repo: context.repo.repo,
                title: process.env.TITLE || 'release: Cargo.tomlバージョン自動更新',
                head: `feature/bump-version-${process.env.NEW_VERSION}`,
                base: 'develop',
                body: process.env.BODY || ''
              });
              core.info('Pull request created.');
              core.info(JSON.stringify(result, null, 2));
            } catch (error) {
              core.setFailed(`Failed to create PR: ${error.message}\n${JSON.stringify(error, null, 2)}`);
            }
        env:
          TITLE: "release: Cargo.tomlバージョンを${{ steps.bump_version.outputs.new_version }}に自動更新"
          BODY: |
            # 対処内容

            developブランチのCargo.tomlのバージョンをv${{ steps.bump_version.outputs.old_version }}→v${{ steps.bump_version.outputs.new_version }}に自動更新します。
          NEW_VERSION: ${{ steps.bump_version.outputs.new_version }}
```

## バージョンのタグ付

release時のmainへのタグ付けも一緒にやってあげると良いでしょう。

```yaml
name: tag-version

on:
  pull_request:
    types:
      - closed
    branches:
      - main

jobs:
  tag-version:
    if: github.event.pull_request.base.ref == 'main' && github.event.pull_request.head.ref == 'release' && github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    steps:
      - name: Checkout develop branch
        uses: actions/checkout@v4
        with:
          ref: main
          fetch-depth: 0

      - name: Extract version from Cargo.toml
        id: get_version
        run: |
          VERSION=$(grep '^version' backend/Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/')
          echo "version=$VERSION" >> $GITHUB_OUTPUT

      - name: Create tag
        id: create_tag
        run: |
          git tag v${{ steps.get_version.outputs.version }}
          git push origin v${{ steps.get_version.outputs.version }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```