sunabox

自動生成コマンドの叩き忘れをCIで検知する

とあるファイルAを変更した時に、自動生成コマンドを使って変更後の内容に基づいて別のファイルBを変更するものがあったとする。
この時ファイルAを変更しても自動生成コマンドを叩き忘れればファイルBは変更されない。

これをCI上で検知するためのGitHub Actionsを書いたのでまとめておく。
今回はCI上でエラーにするだけでなく、PR上にコメントでメッセージを出せるようにもした。
差分があった場合はこんな感じでコメントを発行してくれるようになった。

PR上でのBotによるコメントの画像

背景

改めて何がしたいかを記載する
ファイルAを変更後、scriptsにある自動生成コマンドを叩くとファイルBが変更されるとする

package.json
"scripts": {
    "generate": "tsx ./scripts/generate.ts"
}

このgenerateコマンドを叩き忘れるとファイルBが変更されない。
これをCI上で検知したい。

CI上で差分検知する

方針としてはシンプルで、CI上でgenerateコマンドを叩いてそれによって差分があるかどうかを検知できればよい。
差分検知はgit diffを使えばよさそう。

問題はgit diffを行った時に差分があった場合はCIを落としたいのでエラーにする必要がある。
オプションを見てみると--exit-codeなるものがあって、これを付与すると意図した通り差分があった場合はエラーにしてくれる。

ここまでを一旦CIとして記述してみると以下の様になった。
(普段はyarnを使っているが、後に記述する事情からここではpnpmを利用している)

.github/workflows/ci.yml
name: CI
on:
  pull_request:
 
jobs:
  check-code-generation:
    runs-on: ubuntu-latest
    name: 'Check Code Generation'
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - name: Install pnpm
        uses: pnpm/[email protected]
        with:
          version: 7
      - name: Set node version to 18
        uses: actions/setup-node@v3
        with:
          node-version: 18
          cache: 'pnpm'
      - name: Install deps
        run: pnpm install
      - name: Generate code
        run: pnpm run generate
      - name: Check diff
        run: |
          git add .
          git diff --cached --exit-code

generateコマンドを叩いてからgit addしてgit diff --cached --exit-codeで差分があった場合はエラーにしている。
そのままgit diffせずにわざわざgit addしている理由は、既存のファイルを編集した場合のみだけでなく、新しく作成したファイルや削除したファイルに関しても差分検知の対象とする必要があったため。

CI上でエラーにするだけならこれで完成。

エラーになった時にPR上にコメントを出す

CIが落ちた時には実際にgenerateコマンドを叩いて出てきた差分をコミットする必要がある。
これをPR上のコメントで表示させることでユーザーに何をすべきかを提示する様にしたい。

PR上にコメントを出すのはgithub-scriptを使用する。
エラーになった際にこの処理を行う必要があるので、エラーでも処理を続ける必要がある。
これを実装すると以下の様になった。

.github/workflows/ci.yml
- name Check diff
  id: diff
  run: |
    git add .
    git diff --cached --exit-code
  continue-on-error: true
- name: Comment
  uses: actions/github-script@v6
    with:
      script: |
        const script = require('${{ github.workspace }}/.github/workflows/commentCodeGeneration.js')
        await script(github, context, ${{ steps.diff.outcome == 'success' }})
- name: Status
  if: ${{ steps.diff.outcome == 'failure' }}
  run: exit 1

6行目でdiffを検知した際にエラーだったとしても処理を続けるようにしている。

その後コメントするためのscriptを実行している。
この内容は後述する。

最後にdiffがあった場合はCIをちゃんと落とすためにexit 1でコード1で終了する。

肝心のscript部分がこちら。

JavaScript Icon
.github/workflows/commentCodeGeneration.js
module.exports = async (github, context, isSuccess) => {
  const { data: comments } = await github.rest.issues.listComments({
    owner: context.repo.owner,
    repo: context.repo.repo,
    issue_number: context.issue.number,
  });
 
  const body = `Uncommitted changes were detected after runnning <code>generate</code> command.\nPlease run <code>pnpm run generate</code> to generate/update the related files, and commit them.`;
 
  const botComment = comments.find(
    (comment) => comment.user?.type === 'Bot' && comment.body?.includes(body)
  );
 
  if (isSuccess) {
    if (!botComment) return;
    await github.rest.issues.deleteComment({
      owner: context.repo.owner,
      repo: context.repo.repo,
      comment_id: botComment.id,
    });
    return;
  }
 
  if (!botComment) {
    await github.rest.issues.createComment({
      issue_number: context.issue.number,
      owner: context.repo.owner,
      repo: context.repo.repo,
      body,
    });
  }
};

引数として処理に必要なgithubcontextを受け取る。
それに加えてisSuccessというbooleanを受け取っていて、差分があった(=isSuccessがfalse)場合は以下のコメントをPR上に発行し、差分がなくなった(=isSuccessがtrue)場合はPR上のコメントを消すようにしている。

Uncommitted changes were detected after runnning generate command.
Please run pnpm run generate to generate/update the related files, and commit them.

内容は若干違うが、差分があった場合はこんな感じでコメントをしてくれるようになった。

PR上でのBotによるコメントの画像

スクリプトファイルのTS化

github-scriptの実装はTSで書かれていて型が付いている。
GitHub Actions上で記載する部分はテンプレートとして書かないとなので型の恩恵は得られないが、今回の様にスクリプトファイルに切り出してそれを実行する場合はスクリプトファイルの方はTSにできる。
ということでやってみた。

TypeScript Icon
.github/workflows/commentCodeGeneration.ts
import type { context as ctx, GitHub } from '@actions/github/lib/utils';
 
module.exports = async (
  github: InstanceType<typeof GitHub>,
  context: typeof ctx,
  isSuccess: boolean
) => {
  ...
}

型の内容は大元のコードを読んでそれと同じものにした。

github-scriptでtsファイルは実行できなさそうだったので、上記のTSファイルは事前にトランスパイルしてから実行する必要があるため、以下のstepを追加。

.github/workflows/ci.yml
- name: Transpile ts
  run: pnpm exec tsc .github/workflows/commentCodeGeneration.ts --outDir .github/workflows

これでTSファイルを用意しておけばトランスパイルしたJSファイルを実行できる。

おまけ

実は今回のこのCI、fakerというライブラリのissueでこの機能が欲しいというものがあったので、実際にPRを作ったものである。
(このライブラリがpnpmを使っているため、今回はpnpmで記述した)

Add CI steps to test execution of generate scripts · Issue #813 · faker-js/fakerClear and concise description of the problem We need to add CI steps for the execution of some generate scripts, so we guarantee that these will not break Suggested solution Add steps Alternative N...
favicongithub.com

このPRがマージされた後、実際にこのCIによって差分を検知できたらしく、メンテナーの方からフィードバックをもらえた。
これがめちゃくちゃ嬉しくて本当にやってよかったなと思えた!

I just want to report you back I LOVE THIS NEW FEATURE 😍

infra: move the check-code-generation job to pr.yml by sunadoi · Pull Request #1419 · faker-js/fakercheck-code-generation CI fails in main branch. This PR Moves the job to pr.yml and run it only for PRs and not main. #1405 (comment)
favicongithub.com

まとめ

普段から使用しているライブラリに対して役立つ機能を実装できたので、本当にやってよかった!

以前もfakerにPR出してマージされたことはあったが、めちゃくちゃ簡単な修正だったのであまりやった感なかった。笑
今回はちゃんとOSSコントリビュート出来たと思えたのと、PR上で色々やりとりしてかなり勉強になった。

実装したCIもわりと汎用性あって別の箇所でも使えそうだなと思えたので、今後機会があれば使っていこうと思う。

Buy Me A Coffeeのbutton

目次