GitHub Actions でプルリクレビューを快適にする

2023/12/05に公開

概要

これは Luup Advent Calendar 2023 の 5 日目の記事です。

Luupのサーバーチームの渡部です。
GitHubでプルリクレビューを円滑にするためのGitHub Actionsの実装を紹介します。

はじめに

多くのチームがGitHubのプルリクエストテンプレートを活用し、レビュープロセスをスムーズに進めていますよね。
でも、せっかくのテンプレートも、いつしか面倒に感じて、デフォルトの内容のまま書き換えることなく使われることが増えてしまいがちです。
この状態が続くと、テンプレートはただの形式的なものになってしまいます。
レビュワーとしても、プルリクエストの記載内容を理由に戻すのは気が重いものです。
そんな課題に対処するため、今回はプルリクエストのマークダウン本文を静的解析し、各項目がテンプレートから書き換えられているかを自動でチェックするGitHub Actionsを作成してみました。

必要なこと

  • プルリクエストの内容がテンプレートからちゃんと変更されているか自動でチェック。
    • タイトルや概要欄だけが記載されているのではダメで、各項目が書き換えられることをチェック。
  • ドラフトのプルリクはチェックしない。
  • チェックに通らないとマージできないようにする。

実装の流れ

プルリクエストのテンプレートを用意 (pull_request_template.md)

参考までにLuupのサーバーチームが使っているテンプレートを記載します。

<!--
レビューのルールなど
-->

## 🛴 概要
<!--
変更の目的 もしくは 関連する Issue 番号
-->

## 🛠 変更内容

## 👀 重点的にレビューしてほしい箇所
<!-- 非機能要件やコーディング時につらみを感じた部分があれば記載 -->

## ☕ 注意事項など補足
<!-- 
ex.「先にFucntionsの#...をDeployする必要がある」etc.
こちらに注意事項の記載がない場合はレビューリクエストを送った時点で、レビュワーがMergeする可能性があります。
-->

## 🧪 テスト
<!--
コードの検証をどのように行ったかを簡潔記載
-->

GitHub Actionsの設定 (check-pr.yml)

name: Check PR Content

on:
  pull_request:
    types: [opened, edited, reopened, ready_for_review]

jobs:
  check-content:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v2

      - name: Set up Node.js
        uses: actions/setup-node@v2
        with:
          node-version: 18

      - name: Install dependencies
        run: npm install @actions/core @actions/github marked typescript ts-node @types/node

      - name: Check content against template
        run: npx ts-node ./.github/scripts/check-pr.ts
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

TypeScript スクリプト (check-pr.ts)

import { getInput, setFailed } from "@actions/core";
import { getOctokit, context } from "@actions/github";
import * as fs from 'fs';
import marked from "marked";

function getSectionText(sections: marked.TokensList, startIndex: number): string {
  let text = '';
  for (let i = startIndex + 1; i < sections.length; i++) {
    if (sections[i].type === 'heading') break;
    text += (sections[i] as marked.Tokens.Text).text;
  }
  return text;
}

async function run() {
  try {
    const token = process.env.GITHUB_TOKEN;
    if (!token) {
      throw new Error("GITHUB_TOKEN environment variable is not provided");
    }
    const octokit = getOctokit(token);
    const prNumber = context.payload.pull_request?.number;

    if (!prNumber) {
      throw new Error("Could not get PR number");
    }

    const { data: pr } = await octokit.rest.pulls.get({
      owner: context.repo.owner,
      repo: context.repo.repo,
      pull_number: prNumber,
    });
    if (pr.draft) {
      console.log('This is a draft PR, skipping checks');
      return;
    }
    const body = pr.body ?? "";

    const templateContent = fs.readFileSync('./.github/pull_request_template.md', 'utf8'); //ここでプルリクエストのテンプレートのパスを指定
    const templateSections = marked.lexer(templateContent) as marked.TokensList;
    const prSections = marked.lexer(body) as marked.TokensList;

    templateSections.forEach(async (templateSection, index) => {
      if (templateSection.type === 'heading') { // headingごとに区切って、各項目がデフォルトのままではないかチェックします。
        const templateHeadingText = templateSection.text;
        const prSectionIndex = prSections.findIndex(prSection => prSection.type === 'heading' && prSection.text === templateHeadingText);
        if (prSectionIndex !== -1) {
          const templateText = getSectionText(templateSections, index);
          const prText = getSectionText(prSections, prSectionIndex);

          if (templateText === prText) {
            const errorMessage = `${templateSection.text}」を記載してください`; // デフォルトから書き換えられていない項目を自動的にコメントで指摘する
            await octokit.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: prNumber,
              body: errorMessage
            });
            setFailed(errorMessage);
          }
        }
      }
    });

  } catch (error) {
    setFailed((error as Error).message);
  }
}

run();

まとめ

このGitHub Actionsを導入することで、プルリクエストの作成者とレビュワーの間で、コミュニケーションコストをかけずにレビューをスムーズに行うことができるようになります。
今後はマークダウン形式で各項目が埋めてあるかのチェックだけではなく、内容もChatGPTのAPIなどにかけてチェックできるとより面白いかもしれません。

Luup Developers Blog

Discussion