【カバレッジ警察出動】GitHub Actionsでカバレッジをチェック → 保護ルールでマージ制限してみた

# はじめに
pushやcommit前にテストの実行などを強制するツールに、huskyやlint stagedなどがあります。

これらは有用で良い感じなのですが、ローカルに閉じてしまう仕組みです。
チーム開発においては「誰がどこで詰まってるか？」といった状況が見えづらいのが難点です。

**「テストやカバレッジをクラウド上（CI）で検証し、その結果をGitHub上で自動的に可視化・共有できたら便利だな～」** と思い、今回の取り組みを試してみました。


# 今回の構成
![9739e5de-ddc4-4a31-bc35-c89fb4c14d44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/9c56fab0-98bd-4e75-b2bc-ecfd4aa2bdcc.png)

今回の構成です。

1. GitHub ActionsでCIを構成し、ブランチにpushされたタイミングでカバレッジをチェックするテストを実行します
1. GitHubのブランチ保護ルールを使い、mainブランチへのPRが“上記のテストにパスしていること”を必須条件とします
1. たぶん良い感じになるはずです

# 準備
## コードを用意する
こんな感じのプロジェクトを用意します。細かい部分は以下で補足します。
```
github-action-hello-world
  ├── _test_
  │   └── math.test.ts
  │
  ├── .github
  │   └── workflows
  │       └── node.js.yml
  │
  ├── node_modules
  │
  ├── src
  │   └── math.ts
  │
  ├── .gitignore
  ├── package-lock.json
  ├── package.json
  ├── README.md
  ├── tsconfig.json
  └── vitest.config.ts
```


### Vitest（Nodejs）でテストコード用意
本筋ではないので適当に用意します。

**src/math.ts**
テスト対象コードです。3つの関数を持ちます。
```src/math.ts
export function add(a: number, b: number): number {
    return a + b
}

export function subtract(a: number, b: number): number {
    return a - b
}

export function multiply(a: number, b: number): number {
    return a * b
}
```

**__test__/math.test.ts**
テストコードです。```src/math.ts``` 内の関数をテストしますが、意図的にカバレッジを下げています。
```__test__/math.test.ts
import { describe, it, expect } from 'vitest'
import { add, multiply } from '../src/math'

describe('math functions', () => {
    it('adds two numbers', () => {
        expect(add(2, 3)).toBe(5)
    })

    it('multiplies two numbers', () => {
        expect(multiply(4, 5)).toBe(20)
    })
})
```

**vitest.config.ts**
今回使用しているテスティングライブラリの設定ファイルです。

```vitest.config.ts
/// <reference types="vitest" />
import { defineConfig } from 'vitest/config'

export default defineConfig({
  test: {
    globals: true,       // describe, it, expect をグローバルに使えるように
    environment: 'node', // または 'jsdom'（ブラウザをエミュレートする場合）
    coverage: {
      reporter: ['text', 'json', 'html'],
      thresholds: {
        functions: 90,
      }
    },
  },
})
```

細かいところは無視して、大丈夫です。
カバレッジに関数網羅率90％を設定している、以下の部分が大事です！
```
      thresholds: {
        functions: 90,
      }
```

その他設定ファイルです。

<details><summary>設定ファイル</summary>


```package.json
{
  "name": "vitest-sample",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "test": "vitest",
    "coverage": "vitest run --coverage"
  },
  "devDependencies": {
    "typescript": "^5.0.0",
    "vitest": "^3.1.1",
    "@vitest/coverage-v8": "^3.1.1"
  }
}
```

```tsconfig.json
{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "Node",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "types": [
      "vitest/globals"
    ],
    "baseUrl": ".",
    "paths": {
      "@/*": [
        "src/*"
      ]
    }
  },
  "include": [
    "src",
    "tests"
  ]
}
```

</details>

### カバレッジを測定してみる
```npm run coverage``` でvitestを実行します

```bash
$ npm run coverage

> vitest-sample@1.0.0 coverage
> vitest run --coverage


 RUN  v3.1.1 /mnt/c/work/repo/github-action-hello-world
      Coverage enabled with v8

 ✓ __test__/math.test.ts (2 tests) 10ms
   ✓ math functions > adds two numbers 5ms
   ✓ math functions > multiplies two numbers 1ms

 Test Files  1 passed (1)
      Tests  2 passed (2)
   Start at  11:49:43
   Duration  6.91s (transform 510ms, setup 0ms, collect 541ms, tests 10ms, environment 1ms, prepare 3.55s)
 % Coverage report from v8
----------|---------|----------|---------|---------|-------------------
File      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
----------|---------|----------|---------|---------|-------------------
All files |   77.77 |      100 |   66.66 |   77.77 | 
 math.ts  |   77.77 |      100 |   66.66 |   77.77 | 6-7
----------|---------|----------|---------|---------|-------------------
ERROR: Coverage for functions (66.66%) does not meet global threshold (90%)
```

>ERROR: Coverage for functions (66.66%) does not meet global threshold (90%)

良い感じですね

### GitHub Actions用のymlを作成する
```.github/workflows/coverage.yml``` を作成します
[ここらへん](https://github.com/actions/starter-workflows/blob/main/ci/node.js.yml)を参考に作成しました

```.github/workflows/coverage.yml
name: check-coverage

on:
  push:
    branches:
      - main
      - develop

jobs:
  coverage:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Use Node.js 22.x
      uses: actions/setup-node@v4
      with:
        node-version: '22.x'
        cache: 'npm'
    - run: npm ci
    - run: npm run coverage
```

特にコメントがない普通？のymlです
Node22を使用しています

### gitにpushしてみる
```bash
$ git push
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To github.com:suiwave/github-action-hello-world.git
   2945d1a..ba771a7  develop -> develop
```
いざpush！

![cap1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/aa180184-6377-4092-9824-10ee9ffc0fb1.png)

GitHub上で確認します

![cap2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/4404b3e7-be00-4696-93e1-7871fa73c3f9.png)

良い感じに落ちてますね

![cap3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/ba10ab65-4cc1-4cb6-bd31-30da3b5a7a64.png)

developからmainにプルリクを作成してみました
当然、マージできてしまいます。危険！

## Githubでブランチに保護ルールを設定する
GitHub Actionsでカバレッジをチェックできるようになりました
しかし、チェックがfailでもマージ可能であることも同時に確認できました
よくないので、ブランチ保護ルールで保護します！！

### 保護ルールを設定する
**設定タブに移動**

![cap4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/8a7c4139-7540-4065-af58-1ef42bdec785.png)

移動します

**Branch設定に移動**

![cap4_5 - コピー.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/7fb3a2d6-27ae-4aba-88a0-d46ce52bc780.png)

移動します。ドキュメントへのリンクもありますね。

https://docs.github.com/ja/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets

https://docs.github.com/ja/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches

**ブランチルールセットを追加する**

![cap4_5 - コピー (2).PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/ddc1eb79-3e7f-49e9-a489-579474758e6d.png)

「Add branch ruleset」をクリックします


**ルールセット名とルールの有効化を設定する**

![cap5.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/d2abe8a1-a394-4521-81cd-dca0ff925aae.png)

ルールセット名は適当に「Require coverage checks to pass」
Enforcement statusを「Active」に設定し、ルールを有効化します

**バイパスリストと対象ブランチを設定する**

![cap6.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/dd7538f0-7a37-4c53-a7a8-61b79dd673e9.png)

検証なのでバイパスリストは空としておきます
対象ブランチは、ルールで守るブランチを指定します
今回はマージを取り込むブランチであるdefaultブランチを指定します


**Branch rulesを設定する**

![cap7.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/9d611ea3-5dad-4c7b-a3cd-50b9decc4adb.png)

いろいろありますね
今回のルールセットは、Coverageのパスのみを要求するものなので、いったんすべてのチェックをオフにします
（直push禁止などは他のルールセットで設定する想定です）

**Require status checks to passを設定する**

![cap8.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/54f36501-dec3-44e5-95f8-3497a5747817.png)

Branch rulesの中の、Require status checks to passが今回追加するルールです
>コミットはまずチェックが通る別の参照にプッシュされなければなりません。

mainブランチの更新は、チェックをパスしたブランチのプルリクで行いなさい、的な意味です（た、たぶん・・・）


**追加設定を行う**

![cap9.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/9591bf1e-86c4-4d24-ae42-f6e38dd217ef.png)

「Require branches to be up to date before merging」のチェックもしておきます。基本ですね
また、Add checksから、作成したGitHubActionsのJobを選択します


**ルールセットを作成する**

![cap10.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/6fcc7627-1666-46fe-86c6-c3344b5db0e3.png)

再度設定内容を確認後、「create」をクリックします

**喜ぶ**

![cap11.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/1d232e4e-932a-4417-8714-19b1294f594d.png)

やったールールセットが作成されました
これで、プルリクマージ前に、カバレッジチェックのpassを必須とすることができました。


# 検証
## 状況確認
先ほどマージ可能だったプルリクを見に行きます

![cap12.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/9930f427-3ef2-4cbc-9eba-25bd8b7a04b4.png)

きたーーーーーーーーーーーーーーーーーーー

>Merging is blocked due to failing merge requirements

良い感じですね

## パスするブランチを作成してみる
パスするブランチも作ってみます。そこからプルリクをなげます

### ブランチ作成
```develop``` から```feature/remove-subtract-function``` ブランチを生やしてみます

```
$ git checkout -b feature/remove-subtract-function
Switched to a new branch 'feature/remove-subtract-function'
```

### ファイル編集
**src/math.ts**
テストしていない関数を削除します（テストを増やせよ）
```diff_typescript
export function add(a: number, b: number): number {
    return a + b
}

- export function subtract(a: number, b: number): number {
-     return a - b
- }

export function multiply(a: number, b: number): number {
    return a * b
}
```

**.github/workflows/coverage.yml**
対象ブランチを拡張します
```diff_yaml
on:
  push:
    branches:
      - main
      - develop
+      - feature/*
```

### push結果確認
pushしてGitHubActionsが実行されたことを確認します

![cap13.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/e96e393c-b314-41fe-b50d-c4e3af40f620.png)

きた！無事、対象ブランチが拡張され、またカバレッジチェックもpassしています

### プルリク作ってマージできるか確認
プルリクつくって、確認します

![cap14.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/52c8e4b3-66d2-43e3-9edf-dc1d9ec1c1a9.png)

はい、チェックパスしてるので、マージ可能です！
ブランチ保護ルールがしっかり働いていそうですね！


# 感想

![9739e5de-ddc4-4a31-bc35-c89fb4c14d47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3654730/7658aac5-b6be-46da-a1f9-174ce53bf3eb.png)

普段はCodeCommitやCodebuild,CodepipelineでCI/CDをしているのですが、なんとなくGitHub Actionsに入門してみました。

まずはゆるくCI前のプルリクフェーズで使ってみました。慣れるとActions実行結果画面のUIが良い感じで、良いなと感じました！
CI/CDワークフローなどもおいおい勉強したいです。

今回のリポジトリ

https://github.com/suiwave/github-action-hello-world